yx 完成DjangoBlog项目全面代码注释

yx_branch
yx 6 months ago
parent fd8fd44d0e
commit d045c563d2

@ -1,114 +1,173 @@
# 导入Django表单模块
from django import forms
# 导入Django管理后台模块
from django.contrib import admin
# 导入获取用户模型函数
from django.contrib.auth import get_user_model
# 导入URL反向解析
from django.urls import reverse
# 导入HTML格式化工具
from django.utils.html import format_html
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# Register your models here.
# 在此注册模型
# 导入博客模型
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# 文章表单类
class ArticleForm(forms.ModelForm):
# 可以在此自定义字段,例如使用特定的编辑器部件
# body = forms.CharField(widget=AdminPagedownWidget())
# 表单元数据配置
class Meta:
# 指定关联的模型
model = Article
# 包含所有字段
fields = '__all__'
# 发布文章的管理动作函数
def makr_article_publish(modeladmin, request, queryset):
# 将选中的文章状态更新为发布
queryset.update(status='p')
# 将文章设为草稿的管理动作函数
def draft_article(modeladmin, request, queryset):
# 将选中的文章状态更新为草稿
queryset.update(status='d')
# 关闭文章评论的管理动作函数
def close_article_commentstatus(modeladmin, request, queryset):
# 将选中的文章评论状态更新为关闭
queryset.update(comment_status='c')
# 打开文章评论的管理动作函数
def open_article_commentstatus(modeladmin, request, queryset):
# 将选中的文章评论状态更新为打开
queryset.update(comment_status='o')
# 设置管理动作的显示名称
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
# 文章管理类
class ArticlelAdmin(admin.ModelAdmin):
# 每页显示数量
list_per_page = 20
# 搜索字段
search_fields = ('body', 'title')
# 使用的表单类
form = ArticleForm
# 列表页面显示的字段
list_display = (
'id',
'title',
'author',
'link_to_category',
'link_to_category', # 自定义链接字段
'creation_time',
'views',
'status',
'type',
'article_order')
# 列表页面可点击的链接字段
list_display_links = ('id', 'title')
# 右侧过滤器字段
list_filter = ('status', 'type', 'category')
# 日期层次导航
date_hierarchy = 'creation_time'
# 水平筛选器字段
filter_horizontal = ('tags',)
# 排除的表单字段
exclude = ('creation_time', 'last_modify_time')
# 是否显示"在站点查看"按钮
view_on_site = True
# 管理动作列表
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# 原始ID字段用于优化大表查询
raw_id_fields = ('author', 'category',)
# 自定义分类链接字段方法
def link_to_category(self, obj):
# 获取分类模型的元信息
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 生成分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# 返回格式化的HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# 设置自定义字段的显示名称
link_to_category.short_description = _('category')
# 获取表单的方法
def get_form(self, request, obj=None, **kwargs):
# 调用父类方法获取表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 限制作者字段只能选择超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
# 保存模型的方法
def save_model(self, request, obj, form, change):
# 调用父类保存方法
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 获取站点查看URL的方法
def get_view_on_site_url(self, obj=None):
if obj:
# 返回文章的完整URL
url = obj.get_full_url()
return url
else:
# 如果没有指定对象,返回当前站点域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# 标签管理类
class TagAdmin(admin.ModelAdmin):
# 排除的表单字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 分类管理类
class CategoryAdmin(admin.ModelAdmin):
# 列表页面显示的字段
list_display = ('name', 'parent_category', 'index')
# 排除的表单字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 友情链接管理类
class LinksAdmin(admin.ModelAdmin):
# 排除的表单字段
exclude = ('last_mod_time', 'creation_time')
# 侧边栏管理类
class SideBarAdmin(admin.ModelAdmin):
# 列表页面显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# 排除的表单字段
exclude = ('last_mod_time', 'creation_time')
# 博客设置管理类
class BlogSettingsAdmin(admin.ModelAdmin):
pass
# 使用默认配置
pass

@ -1,5 +1,8 @@
# 导入Django应用配置类
from django.apps import AppConfig
# 定义博客应用的配置类
class BlogConfig(AppConfig):
name = 'blog'
# 指定应用的名称
name = 'blog'

@ -1,43 +1,76 @@
# 导入日志模块
import logging
# 导入Django时区工具
from django.utils import timezone
# 导入缓存工具和获取博客设置函数
from djangoblog.utils import cache, get_blog_setting
# 导入分类和文章模型
from .models import Category, Article
# 获取日志器
logger = logging.getLogger(__name__)
# SEO处理器函数用于生成模板上下文
def seo_processor(requests):
# 缓存键名
key = 'seo_processor'
# 尝试从缓存获取数据
value = cache.get(key)
if value:
# 如果缓存存在,直接返回缓存数据
return value
else:
# 缓存不存在,记录日志并生成新数据
logger.info('set processor cache.')
# 获取博客设置
setting = get_blog_setting()
# 构建上下文数据字典
value = {
# 网站名称
'SITE_NAME': setting.site_name,
# 是否显示谷歌广告
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
# 谷歌广告代码
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
# 网站SEO描述
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
# 网站描述
'SITE_DESCRIPTION': setting.site_description,
# 网站关键词
'SITE_KEYWORDS': setting.site_keywords,
# 网站基础URL
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
# 文章摘要长度
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
# 导航分类列表
'nav_category_list': Category.objects.all(),
# 导航页面(类型为页面的已发布文章)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
# 是否开启网站评论
'OPEN_SITE_COMMENT': setting.open_site_comment,
# 备案号
'BEIAN_CODE': setting.beian_code,
# 网站统计代码
'ANALYTICS_CODE': setting.analytics_code,
# 公安备案号
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
# 是否显示公安备案
"SHOW_GONGAN_CODE": setting.show_gongan_code,
# 当前年份
"CURRENT_YEAR": timezone.now().year,
# 全局头部内容
"GLOBAL_HEADER": setting.global_header,
# 全局尾部内容
"GLOBAL_FOOTER": setting.global_footer,
# 评论是否需要审核
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
# 将数据存入缓存有效期10小时
cache.set(key, value, 60 * 60 * 10)
return value
# 返回数据
return value

@ -1,26 +1,41 @@
# 导入时间模块
import time
# 导入Elasticsearch客户端
import elasticsearch.client
# 导入Django配置
from django.conf import settings
# 导入Elasticsearch DSL相关类
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
# 导入Elasticsearch连接管理
from elasticsearch_dsl.connections import connections
# 导入文章模型
from blog.models import Article
# 检查是否启用Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# 如果启用Elasticsearch创建连接
if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# 导入Elasticsearch客户端
from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 导入Ingest客户端
from elasticsearch.client import IngestClient
# 创建Ingest客户端
c = IngestClient(es)
# 检查并创建geoip管道
try:
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果geoip管道不存在创建它
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -33,91 +48,140 @@ if ELASTICSEARCH_ENABLED:
}''')
# 定义GeoIP内部文档类
class GeoIp(InnerDoc):
# 大洲名称
continent_name = Keyword()
# 国家ISO代码
country_iso_code = Keyword()
# 国家名称
country_name = Keyword()
# 地理位置坐标
location = GeoPoint()
# 定义用户代理浏览器内部文档类
class UserAgentBrowser(InnerDoc):
# 浏览器家族
Family = Keyword()
# 浏览器版本
Version = Keyword()
# 定义用户代理操作系统内部文档类继承自UserAgentBrowser
class UserAgentOS(UserAgentBrowser):
pass
# 定义用户代理设备内部文档类
class UserAgentDevice(InnerDoc):
# 设备家族
Family = Keyword()
# 设备品牌
Brand = Keyword()
# 设备型号
Model = Keyword()
# 定义用户代理内部文档类
class UserAgent(InnerDoc):
# 浏览器信息对象
browser = Object(UserAgentBrowser, required=False)
# 操作系统信息对象
os = Object(UserAgentOS, required=False)
# 设备信息对象
device = Object(UserAgentDevice, required=False)
# 原始用户代理字符串
string = Text()
# 是否为机器人
is_bot = Boolean()
# 定义耗时文档类继承自Document
class ElapsedTimeDocument(Document):
# URL地址
url = Keyword()
# 耗时(毫秒)
time_taken = Long()
# 日志时间
log_datetime = Date()
# IP地址
ip = Keyword()
# GeoIP信息对象
geoip = Object(GeoIp, required=False)
# 用户代理信息对象
useragent = Object(UserAgent, required=False)
# 索引配置
class Index:
# 索引名称
name = 'performance'
# 索引设置
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
# 元数据配置
class Meta:
# 文档类型
doc_type = 'ElapsedTime'
# 耗时文档管理器类
class ElaspedTimeDocumentManager:
# 构建索引的静态方法
@staticmethod
def build_index():
from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 检查索引是否存在
res = client.indices.exists(index="performance")
# 如果索引不存在,初始化索引
if not res:
ElapsedTimeDocument.init()
# 删除索引的静态方法
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略400和404错误
es.indices.delete(index='performance', ignore=[400, 404])
# 创建文档的静态方法
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# 确保索引存在
ElaspedTimeDocumentManager.build_index()
# 创建用户代理对象
ua = UserAgent()
# 设置浏览器信息
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
# 设置操作系统信息
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
# 设置设备信息
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
# 设置原始用户代理字符串
ua.string = useragent.ua_string
# 设置是否为机器人
ua.is_bot = useragent.is_bot
# 创建耗时文档
doc = ElapsedTimeDocument(
meta={
# 使用当前时间戳作为文档ID
'id': int(
round(
time.time() *
@ -127,61 +191,88 @@ class ElaspedTimeDocumentManager:
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
# 保存文档使用geoip管道处理
doc.save(pipeline="geoip")
# 定义文章文档类继承自Document
class ArticleDocument(Document):
# 正文字段使用IK分词器
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 标题字段使用IK分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 作者对象字段
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 分类对象字段
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 标签对象字段
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 发布时间字段
pub_time = Date()
# 状态字段
status = Text()
# 评论状态字段
comment_status = Text()
# 类型字段
type = Text()
# 浏览量字段
views = Integer()
# 文章排序字段
article_order = Integer()
# 索引配置
class Index:
# 索引名称
name = 'blog'
# 索引设置
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
# 元数据配置
class Meta:
# 文档类型
doc_type = 'Article'
# 文章文档管理器类
class ArticleDocumentManager():
# 初始化方法
def __init__(self):
# 创建索引
self.create_index()
# 创建索引方法
def create_index(self):
ArticleDocument.init()
# 删除索引方法
def delete_index(self):
from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略400和404错误
es.indices.delete(index='blog', ignore=[400, 404])
# 将文章转换为文档的方法
def convert_to_doc(self, articles):
# 使用列表推导式将文章列表转换为文档列表
return [
ArticleDocument(
meta={
'id': article.id},
'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
@ -193,7 +284,7 @@ class ArticleDocumentManager():
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
'id': t.id} for t in article.tags.all()], # 遍历所有标签
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
@ -201,13 +292,20 @@ class ArticleDocumentManager():
views=article.views,
article_order=article.article_order) for article in articles]
# 重建索引方法
def rebuild(self, articles=None):
# 初始化索引
ArticleDocument.init()
# 如果没有提供文章,获取所有文章
articles = articles if articles else Article.objects.all()
# 将文章转换为文档
docs = self.convert_to_doc(articles)
# 保存所有文档
for doc in docs:
doc.save()
# 更新文档方法
def update_docs(self, docs):
# 遍历并保存所有文档
for doc in docs:
doc.save()
doc.save()

@ -1,19 +1,31 @@
# 导入日志模块
import logging
# 导入Django表单模块
from django import forms
# 导入Haystack搜索表单基类
from haystack.forms import SearchForm
# 获取日志器
logger = logging.getLogger(__name__)
# 博客搜索表单类继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# 查询数据字段,设置为必填
querydata = forms.CharField(required=True)
# 搜索方法重写
def search(self):
# 调用父类的搜索方法获取基础搜索结果
datas = super(BlogSearchForm, self).search()
# 检查表单是否有效
if not self.is_valid():
# 如果表单无效,返回无查询结果
return self.no_query_found()
# 如果查询数据存在,记录日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas
# 返回搜索结果
return datas

@ -1,18 +1,29 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入Elasticsearch相关文档和管理器
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
# 构建搜索索引的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'build search index'
# 命令处理主方法
def handle(self, *args, **options):
# 检查Elasticsearch是否启用
if ELASTICSEARCH_ENABLED:
# 构建耗时文档索引
ElaspedTimeDocumentManager.build_index()
# 初始化耗时文档管理器
manager = ElapsedTimeDocument()
manager.init()
# 创建文章文档管理器实例
manager = ArticleDocumentManager()
# 删除现有索引
manager.delete_index()
manager.rebuild()
# 重新构建索引
manager.rebuild()

@ -1,13 +1,20 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入博客模型
from blog.models import Tag, Category
# TODO 参数化
# 构建搜索词的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'build search words'
# 命令处理主方法
def handle(self, *args, **options):
# 从所有标签和分类中获取名称,合并并去重创建集合
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))
# 打印所有搜索词,每个词占一行
print('\n'.join(datas))

@ -1,11 +1,18 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入缓存工具
from djangoblog.utils import cache
# 清理缓存的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'clear the whole cache'
# 命令处理主方法
def handle(self, *args, **options):
# 清理所有缓存
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
# 输出成功信息
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,40 +1,60 @@
# 导入获取用户模型函数
from django.contrib.auth import get_user_model
# 导入密码加密函数
from django.contrib.auth.hashers import make_password
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入博客模型
from blog.models import Article, Tag, Category
# 创建测试数据的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'create test datas'
# 命令处理主方法
def handle(self, *args, **options):
# 获取或创建测试用户
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 获取或创建父类目
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# 获取或创建子类目
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# 保存子类目
category.save()
# 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 循环创建20篇测试文章
for i in range(1, 20):
# 获取或创建文章
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# 创建新标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 为文章添加标签
article.tags.add(tag)
article.tags.add(basetag)
# 保存文章
article.save()
# 导入缓存工具
from djangoblog.utils import cache
# 清理缓存
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))
# 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,50 +1,77 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入蜘蛛通知工具和获取当前站点函数
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
# 导入博客模型
from blog.models import Article, Tag, Category
# 获取当前站点域名
site = get_current_site().domain
# 通知百度URL的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'notify baidu url'
# 添加命令行参数
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
'data_type', # 参数名称
type=str, # 参数类型
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
'all', # 所有类型
'article', # 仅文章
'tag', # 仅标签
'category'], # 仅分类
help='article : all article,tag : all tag,category: all category,all: All of these') # 帮助信息
# 获取完整URL的方法
def get_full_url(self, path):
# 构建完整的HTTPS URL
url = "https://{site}{path}".format(site=site, path=path)
return url
# 命令处理主方法
def handle(self, *args, **options):
# 获取数据类型参数
type = options['data_type']
# 输出开始信息
self.stdout.write('start get %s' % type)
# 初始化URL列表
urls = []
# 处理文章URL
if type == 'article' or type == 'all':
# 获取所有已发布的文章
for article in Article.objects.filter(status='p'):
# 添加文章的完整URL
urls.append(article.get_full_url())
# 处理标签URL
if type == 'tag' or type == 'all':
# 获取所有标签
for tag in Tag.objects.all():
# 获取标签的相对URL
url = tag.get_absolute_url()
# 添加标签的完整URL
urls.append(self.get_full_url(url))
# 处理分类URL
if type == 'category' or type == 'all':
# 获取所有分类
for category in Category.objects.all():
# 获取分类的相对URL
url = category.get_absolute_url()
# 添加分类的完整URL
urls.append(self.get_full_url(url))
# 输出开始通知信息
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用百度蜘蛛通知
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))
# 输出完成信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,47 +1,81 @@
# 导入requests库用于HTTP请求
import requests
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入静态文件URL处理
from django.templatetags.static import static
# 导入保存用户头像的工具函数
from djangoblog.utils import save_user_avatar
# 导入OAuth用户模型
from oauth.models import OAuthUser
# 导入根据类型获取OAuth管理器的函数
from oauth.oauthmanager import get_manager_by_type
# 同步用户头像的管理命令类
class Command(BaseCommand):
# 命令帮助信息
help = 'sync user avatar'
# 测试图片URL是否可访问的方法
def test_picture(self, url):
try:
# 发送GET请求测试图片URL设置2秒超时
if requests.get(url, timeout=2).status_code == 200:
# 返回True表示图片可访问
return True
except:
# 发生异常时忽略返回None
pass
# 命令处理主方法
def handle(self, *args, **options):
# 获取静态文件基础URL
static_url = static("../")
# 获取所有OAuth用户
users = OAuthUser.objects.all()
# 输出开始同步信息
self.stdout.write(f'开始同步{len(users)}个用户头像')
# 遍历所有用户
for u in users:
# 输出当前同步的用户信息
self.stdout.write(f'开始同步:{u.nickname}')
# 获取用户当前头像URL
url = u.picture
if url:
# 检查是否为静态文件URL
if url.startswith(static_url):
# 测试静态图片是否可访问
if self.test_picture(url):
# 图片可访问,跳过处理
continue
else:
# 静态图片不可访问,尝试从元数据获取
if u.metadata:
# 根据用户类型获取对应的OAuth管理器
manage = get_manager_by_type(u.type)
# 从元数据获取头像URL
url = manage.get_picture(u.metadata)
# 保存用户头像
url = save_user_avatar(url)
else:
# 没有元数据,使用默认头像
url = static('blog/img/avatar.png')
else:
# 非静态文件URL直接保存头像
url = save_user_avatar(url)
else:
# 没有头像URL使用默认头像
url = static('blog/img/avatar.png')
# 如果成功获取到头像URL更新用户信息
if url:
# 输出同步完成信息
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 更新用户头像URL
u.picture = url
# 保存用户信息
u.save()
self.stdout.write('结束同步')
# 输出同步结束信息
self.stdout.write('结束同步')

@ -1,42 +1,68 @@
# 导入日志模块
import logging
# 导入时间模块
import time
# 导入获取客户端IP的工具
from ipware import get_client_ip
# 导入用户代理解析工具
from user_agents import parse
# 导入Elasticsearch相关配置和管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# 获取日志器
logger = logging.getLogger(__name__)
# 在线中间件类,用于记录页面渲染时间和用户访问信息
class OnlineMiddleware(object):
# 初始化方法
def __init__(self, get_response=None):
# 保存get_response函数
self.get_response = get_response
# 调用父类初始化
super().__init__()
# 调用方法,处理请求和响应
def __call__(self, request):
''' page render time '''
''' 页面渲染时间统计 '''
# 记录开始时间
start_time = time.time()
# 调用后续中间件和视图,获取响应
response = self.get_response(request)
# 获取用户代理字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 获取客户端IP地址
ip, _ = get_client_ip(request)
# 解析用户代理信息
user_agent = parse(http_user_agent)
# 检查响应是否为流式响应
if not response.streaming:
try:
# 计算渲染耗时
cast_time = time.time() - start_time
# 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# 将耗时转换为毫秒并保留两位小数
time_taken = round((cast_time) * 1000, 2)
# 获取请求的URL路径
url = request.path
# 导入时区工具
from django.utils import timezone
# 创建耗时文档记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
log_datetime=timezone.now(), # 当前时间
useragent=user_agent, # 用户代理信息
ip=ip) # IP地址
# 在响应内容中替换加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
# 记录错误日志
logger.error("Error OnlineMiddleware: %s" % e)
return response
# 返回响应
return response

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由Django 4.1.7于2023-03-02 07:14自动生成
from django.conf import settings
from django.db import migrations, models
@ -8,130 +8,197 @@ import mdeditor.fields
class Migration(migrations.Migration):
# 初始迁移类
initial = True
# 依赖关系
dependencies = [
# 可交换的用户模型依赖
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 迁移操作列表
operations = [
# 创建博客设置模型
migrations.CreateModel(
name='BlogSettings',
fields=[
# 主键ID字段自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 网站名称字段,默认空字符串
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
# 网站描述字段,文本类型
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
# 网站SEO描述字段
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键词字段
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
# 文章摘要长度字段,整数类型
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
# 侧边栏文章数量字段
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
# 侧边栏评论数量字段
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
# 文章页面评论数量字段
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
# 是否显示谷歌广告字段,布尔类型
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 谷歌广告代码字段,可为空
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
# 是否开启网站评论功能字段
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 备案号字段,可为空
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
# 网站统计代码字段
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案字段
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
# 公安备案号字段,可为空
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
'verbose_name': '网站配置', # 单数显示名称
'verbose_name_plural': '网站配置', # 复数显示名称
},
),
# 创建友情链接模型
migrations.CreateModel(
name='Links',
fields=[
# 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称字段,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址字段URL类型
('link', models.URLField(verbose_name='链接地址')),
# 排序字段,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 显示类型字段,选择类型
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
'verbose_name': '友情链接', # 单数显示名称
'verbose_name_plural': '友情链接', # 复数显示名称
'ordering': ['sequence'], # 按排序字段升序排列
},
),
# 创建侧边栏模型
migrations.CreateModel(
name='SideBar',
fields=[
# 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 标题字段
('name', models.CharField(max_length=100, verbose_name='标题')),
# 内容字段,文本类型
('content', models.TextField(verbose_name='内容')),
# 排序字段,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用字段
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
'verbose_name': '侧边栏', # 单数显示名称
'verbose_name_plural': '侧边栏', # 复数显示名称
'ordering': ['sequence'], # 按排序字段升序排列
},
),
# 创建标签模型
migrations.CreateModel(
name='Tag',
fields=[
# 主键ID字段自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 标签名字段,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
# 缩略名字段用于URL
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
'verbose_name': '标签', # 单数显示名称
'verbose_name_plural': '标签', # 复数显示名称
'ordering': ['name'], # 按名称升序排列
},
),
# 创建分类模型
migrations.CreateModel(
name='Category',
fields=[
# 主键ID字段自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 分类名字段,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
# 缩略名字段用于URL
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 权重排序字段,越大越靠前
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
# 父级分类字段,外键自关联,可为空
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
'verbose_name': '分类', # 单数显示名称
'verbose_name_plural': '分类', # 复数显示名称
'ordering': ['-index'], # 按权重降序排列
},
),
# 创建文章模型
migrations.CreateModel(
name='Article',
fields=[
# 主键ID字段自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 标题字段,唯一
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 正文字段使用Markdown编辑器
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间字段
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
# 文章状态字段,选择类型
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
# 评论状态字段,选择类型
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
# 类型字段,选择类型(文章或页面)
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量字段,正整数
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 文章排序字段,数字越大越靠前
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示TOC目录字段
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
# 作者字段,外键关联用户模型
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 分类字段,外键关联分类模型
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
# 标签字段,多对多关联标签模型
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
'verbose_name': '文章', # 单数显示名称
'verbose_name_plural': '文章', # 复数显示名称
'ordering': ['-article_order', '-pub_time'], # 按排序和发布时间降序排列
'get_latest_by': 'id', # 指定获取最新记录的字段
},
),
]
]

@ -1,23 +1,27 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
# 由Django 4.1.7于2023-03-29 06:08自动生成
from django.db import migrations, models
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 依赖blog应用的0001_initial迁移文件
('blog', '0001_initial'),
]
# 迁移操作列表
operations = [
# 添加全局尾部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# 添加全局头部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]
]

@ -1,17 +1,21 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
# 由Django 4.2.1于2023-05-09 07:45自动生成
from django.db import migrations, models
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 依赖blog应用的0002_blogsettings_global_footer_and_more迁移文件
('blog', '0002_blogsettings_global_footer_and_more'),
]
# 迁移操作列表
operations = [
# 添加评论审核字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]
]

@ -1,27 +1,33 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
# 由Django 4.2.1于2023-05-09 07:51自动生成
from django.db import migrations
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 依赖blog应用的0003_blogsettings_comment_need_review迁移文件
('blog', '0003_blogsettings_comment_need_review'),
]
# 迁移操作列表
operations = [
# 重命名字段analyticscode -> analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
# 重命名字段beiancode -> beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
# 重命名字段sitename -> site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]
]

@ -1,300 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -1,17 +1,20 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
# 由Django 4.2.7于2024-01-26 02:41自动生成
from django.db import migrations
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 依赖blog应用的0005迁移文件
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# 迁移操作列表
operations = [
# 修改BlogSettings模型的元选项
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]
]

@ -1,122 +1,180 @@
# 导入日志模块
import logging
# 导入正则表达式模块
import re
# 导入抽象方法装饰器
from abc import abstractmethod
# 导入Django配置
from django.conf import settings
# 导入验证错误异常
from django.core.exceptions import ValidationError
# 导入数据库模型
from django.db import models
# 导入URL反向解析
from django.urls import reverse
# 导入时区工具
from django.utils.timezone import now
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入Markdown编辑器字段
from mdeditor.fields import MDTextField
# 导入slug生成工具
from uuslug import slugify
# 导入缓存装饰器和缓存工具
from djangoblog.utils import cache_decorator, cache
# 导入获取当前站点函数
from djangoblog.utils import get_current_site
# 获取日志器
logger = logging.getLogger(__name__)
# 链接显示类型选择类
class LinkShowType(models.TextChoices):
# 首页显示
I = ('i', _('index'))
# 列表页显示
L = ('l', _('list'))
# 文章页面显示
P = ('p', _('post'))
# 全站显示
A = ('a', _('all'))
# 幻灯片显示
S = ('s', _('slide'))
# 基础模型抽象类
class BaseModel(models.Model):
# 主键ID字段自增
id = models.AutoField(primary_key=True)
# 创建时间字段
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# 保存方法重写
def save(self, *args, **kwargs):
# 检查是否为文章视图更新
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 如果是视图更新,直接更新数据库
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 如果有slug字段自动生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# 调用父类保存方法
super().save(*args, **kwargs)
# 获取完整URL的方法
def get_full_url(self):
# 获取当前站点域名
site = get_current_site().domain
# 构建完整HTTPS URL
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
# 元数据配置 - 抽象类
class Meta:
abstract = True
# 抽象方法 - 获取绝对URL
@abstractmethod
def get_absolute_url(self):
pass
# 文章模型类继承自BaseModel
class Article(BaseModel):
"""文章"""
"""文章模型"""
# 状态选择项
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
)
# 评论状态选择项
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
('o', _('Open')), # 打开评论
('c', _('Close')), # 关闭评论
)
# 类型选择项
TYPE = (
('a', _('Article')),
('p', _('Page')),
('a', _('Article')), # 文章
('p', _('Page')), # 页面
)
# 标题字段,唯一
title = models.CharField(_('title'), max_length=200, unique=True)
# 正文字段使用Markdown编辑器
body = MDTextField(_('body'))
# 发布时间字段
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# 状态字段
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# 评论状态字段
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# 类型字段
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# 浏览量字段,正整数
views = models.PositiveIntegerField(_('views'), default=0)
# 作者字段,外键关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# 文章排序字段
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# 是否显示目录字段
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# 分类字段,外键关联分类模型
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# 标签字段,多对多关联标签模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# 将正文转换为字符串
def body_to_string(self):
return self.body
# 对象的字符串表示
def __str__(self):
return self.title
# 元数据配置
class Meta:
# 按排序和发布时间降序排列
ordering = ['-article_order', '-pub_time']
# 单数显示名称
verbose_name = _('article')
# 复数显示名称
verbose_name_plural = verbose_name
# 指定获取最新记录的字段
get_latest_by = 'id'
# 获取绝对URL的方法
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
@ -125,83 +183,109 @@ class Article(BaseModel):
'day': self.creation_time.day
})
# 获取分类树,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
# 获取分类树
tree = self.category.get_category_tree()
# 提取分类名称和URL
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
# 保存方法
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# 增加浏览量方法
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
# 获取评论列表,带缓存
def comment_list(self):
# 缓存键
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
# 如果缓存存在,记录日志并返回
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 从数据库获取评论并缓存
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# 获取管理后台URL
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# 获取下一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# 获取上一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
# 获取文章第一张图片URL
def get_first_image_url(self):
"""
Get the first image url from article.body.
从文章正文中获取第一张图片URL
:return:
"""
# 使用正则表达式匹配Markdown图片语法
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
# 分类模型类继承自BaseModel
class Category(BaseModel):
"""文章分类"""
# 分类名称字段,唯一
name = models.CharField(_('category name'), max_length=30, unique=True)
# 父级分类字段,外键自关联
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# slug字段用于URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 排序索引字段
index = models.IntegerField(default=0, verbose_name=_('index'))
# 元数据配置
class Meta:
# 按索引降序排列
ordering = ['-index']
# 单数显示名称
verbose_name = _('category')
# 复数显示名称
verbose_name_plural = verbose_name
# 获取绝对URL
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# 对象的字符串表示
def __str__(self):
return self.name
# 获取分类树,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
@ -210,6 +294,7 @@ class Category(BaseModel):
"""
categorys = []
# 递归解析函数
def parse(category):
categorys.append(category)
if category.parent_category:
@ -218,6 +303,7 @@ class Category(BaseModel):
parse(self)
return categorys
# 获取子分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
@ -227,6 +313,7 @@ class Category(BaseModel):
categorys = []
all_categorys = Category.objects.all()
# 递归解析函数
def parse(category):
if category not in categorys:
categorys.append(category)
@ -240,137 +327,200 @@ class Category(BaseModel):
return categorys
# 标签模型类继承自BaseModel
class Tag(BaseModel):
"""文章标签"""
# 标签名称字段,唯一
name = models.CharField(_('tag name'), max_length=30, unique=True)
# slug字段用于URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 对象的字符串表示
def __str__(self):
return self.name
# 获取绝对URL
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# 获取文章数量,带缓存
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
# 元数据配置
class Meta:
# 按名称升序排列
ordering = ['name']
# 单数显示名称
verbose_name = _('tag')
# 复数显示名称
verbose_name_plural = verbose_name
# 友情链接模型类
class Links(models.Model):
"""友情链接"""
# 链接名称字段,唯一
name = models.CharField(_('link name'), max_length=30, unique=True)
# 链接地址字段
link = models.URLField(_('link'))
# 排序字段,唯一
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用字段
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# 显示类型字段
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# 创建时间字段
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 元数据配置
class Meta:
# 按排序升序排列
ordering = ['sequence']
# 单数显示名称
verbose_name = _('link')
# 复数显示名称
verbose_name_plural = verbose_name
# 对象的字符串表示
def __str__(self):
return self.name
# 侧边栏模型类
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
# 标题字段
name = models.CharField(_('title'), max_length=100)
# 内容字段
content = models.TextField(_('content'))
# 排序字段,唯一
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用字段
is_enable = models.BooleanField(_('is enable'), default=True)
# 创建时间字段
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 元数据配置
class Meta:
# 按排序升序排列
ordering = ['sequence']
# 单数显示名称
verbose_name = _('sidebar')
# 复数显示名称
verbose_name_plural = verbose_name
# 对象的字符串表示
def __str__(self):
return self.name
# 博客设置模型类
class BlogSettings(models.Model):
"""blog的配置"""
# 网站名称字段
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# 网站描述字段
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# 网站SEO描述字段
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# 网站关键词字段
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# 文章摘要长度字段
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# 侧边栏文章数量字段
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# 侧边栏评论数量字段
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# 文章页面评论数量字段
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# 是否显示谷歌广告字段
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# 谷歌广告代码字段
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# 是否开启网站评论字段
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# 全局头部内容字段
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# 全局尾部内容字段
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# 备案号字段
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 网站统计代码字段
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# 是否显示公安备案号字段
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# 公安备案号字段
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 评论是否需要审核字段
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
# 元数据配置
class Meta:
# 单数显示名称
verbose_name = _('Website configuration')
# 复数显示名称
verbose_name_plural = verbose_name
# 对象的字符串表示
def __str__(self):
return self.site_name
# 清理验证方法
def clean(self):
# 确保只能有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
# 保存方法
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# 导入缓存工具
from djangoblog.utils import cache
cache.clear()
# 保存后清理缓存
cache.clear()

@ -1,13 +1,22 @@
# 导入Haystack搜索索引相关模块
from haystack import indexes
# 导入文章模型
from blog.models import Article
# 文章搜索索引类继承自SearchIndex和Indexable
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# 主搜索字段document=True表示这是主要搜索内容字段
# use_template=True表示使用模板文件来定义字段内容
text = indexes.CharField(document=True, use_template=True)
# 获取索引对应的模型类
def get_model(self):
# 返回文章模型类
return Article
# 定义索引查询集,指定哪些记录需要被索引
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')
# 只索引状态为已发布('p')的文章
return self.get_model().objects.filter(status='p')

@ -1,42 +1,69 @@
# 导入操作系统模块
import os
# 导入Django配置和文件上传相关模块
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
# 导入管理命令调用
from django.core.management import call_command
# 导入分页器
from django.core.paginator import Paginator
# 导入静态文件处理
from django.templatetags.static import static
# 导入测试相关模块
from django.test import Client, RequestFactory, TestCase
# 导入URL反向解析
from django.urls import reverse
# 导入时区工具
from django.utils import timezone
# 导入账户模型
from accounts.models import BlogUser
# 导入博客表单
from blog.forms import BlogSearchForm
# 导入博客模型
from blog.models import Article, Category, Tag, SideBar, Links
# 导入博客模板标签
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
# 导入工具函数
from djangoblog.utils import get_current_site, get_sha256
# 导入OAuth模型
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
# 在此创建测试
# 文章测试类
class ArticleTest(TestCase):
# 测试前置设置方法
def setUp(self):
# 创建测试客户端
self.client = Client()
# 创建请求工厂
self.factory = RequestFactory()
# 测试文章验证功能
def test_validate_article(self):
# 获取当前站点域名
site = get_current_site().domain
# 获取或创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
# 设置用户密码
user.set_password("liangliangyy")
# 设置为员工和超级用户
user.is_staff = True
user.is_superuser = True
user.save()
# 测试用户详情页访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试管理后台页面访问
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
@ -44,30 +71,36 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
# 创建分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# 创建标签
tag = Tag()
tag.name = "nicetag"
tag.save()
# 创建文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 验证初始标签数量为0
self.assertEqual(0, article.tags.count())
# 添加标签
article.tags.add(tag)
article.save()
# 验证标签数量为1
self.assertEqual(1, article.tags.count())
# 批量创建20篇文章
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,96 +112,136 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# 检查Elasticsearch是否启用
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
# 构建搜索索引
call_command("build_index")
# 测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试搜索页面
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 测试文章标签模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
# 用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试分页功能 - 所有文章
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
# 测试分页功能 - 标签归档
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
# 测试分页功能 - 作者归档
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
# 测试分页功能 - 分类归档
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# 测试搜索表单
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
# 测试百度蜘蛛通知
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试Gravatar相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
# 创建友情链接
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
# 测试友情链接页面
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 测试RSS订阅
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# 测试网站地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 测试管理后台页面
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
# 分页检查方法
def check_pagination(self, p, type, value):
# 遍历所有分页
for page in range(1, p.num_pages + 1):
# 加载分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
# 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
# 测试图片上传功能
def test_image(self):
import requests
# 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
# 保存图片到本地
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# 测试无签名上传应该返回403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# 生成签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 测试带签名的图片上传
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
@ -176,17 +249,23 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
# 清理测试文件
os.remove(imagepath)
# 测试工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
# 测试错误页面
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
# 测试管理命令
def test_commands(self):
# 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -195,12 +274,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# 创建OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -212,6 +293,7 @@ class ArticleTest(TestCase):
}'''
u.save()
# 创建另一个OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -222,11 +304,12 @@ class ArticleTest(TestCase):
}'''
u.save()
# 测试各种管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
call_command("build_index") # 构建搜索索引
call_command("ping_baidu", "all") # 百度推送
call_command("create_testdata") # 创建测试数据
call_command("clear_cache") # 清理缓存
call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") # 构建搜索词

@ -1,62 +1,93 @@
# 导入Django URL路由相关模块
from django.urls import path
# 导入缓存页面装饰器
from django.views.decorators.cache import cache_page
# 导入当前应用的视图模块
from . import views
# 定义应用命名空间
app_name = "blog"
# 定义URL模式列表
urlpatterns = [
# 首页URL使用IndexView类视图
path(
r'',
views.IndexView.as_view(),
name='index'),
# 首页分页URL支持页码参数
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# 文章详情页URL包含年月日和文章ID参数
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# 分类详情页URL使用分类名称slug参数
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# 分类详情分页URL包含分类名称和页码参数
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# 作者详情页URL使用作者名称参数
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# 作者详情分页URL包含作者名称和页码参数
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# 标签详情页URL使用标签名称slug参数
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# 标签详情分页URL包含标签名称和页码参数
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# 归档页面URL使用缓存装饰器缓存1小时
path(
'archives.html',
cache_page(
60 * 60)(
60 * 60)( # 缓存1小时60分钟 * 60秒
views.ArchivesView.as_view()),
name='archives'),
# 友情链接页面URL
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# 文件上传URL使用函数视图
path(
r'upload',
views.fileupload,
name='upload'),
# 清理缓存URL使用函数视图
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
]

@ -1,64 +1,92 @@
# 导入日志模块
import logging
# 导入操作系统模块
import os
# 导入UUID生成模块
import uuid
# 导入Django配置
from django.conf import settings
# 导入分页器
from django.core.paginator import Paginator
# 导入HTTP响应类
from django.http import HttpResponse, HttpResponseForbidden
# 导入快捷函数
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入静态文件处理
from django.templatetags.static import static
# 导入时区工具
from django.utils import timezone
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入CSRF豁免装饰器
from django.views.decorators.csrf import csrf_exempt
# 导入通用视图类
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
# 导入Haystack搜索视图
from haystack.views import SearchView
# 导入博客模型
from blog.models import Article, Category, LinkShowType, Links, Tag
# 导入评论表单
from comments.forms import CommentForm
# 导入插件管理钩子
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 导入工具函数
from djangoblog.utils import cache, get_blog_setting, get_sha256
# 获取日志器
logger = logging.getLogger(__name__)
# 文章列表视图基类继承自ListView
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
# 指定使用的模板名称
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
# 指定上下文对象名称(在模板中使用的变量名
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
# 页面类型,用于分类目录或标签列表等
page_type = ''
# 每页显示数量,从设置中获取
paginate_by = settings.PAGINATE_BY
# 页码参数名
page_kwarg = 'page'
# 链接显示类型
link_type = LinkShowType.L
# 获取视图缓存键的方法
def get_view_cache_key(self):
return self.request.get['pages']
# 页码属性
@property
def page_number(self):
page_kwarg = self.page_kwarg
# 从URL参数或GET参数获取页码默认为1
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
# 获取查询集缓存键的抽象方法,需要子类重写
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
# 获取查询集数据的抽象方法,需要子类重写
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
# 从缓存获取查询集数据的方法
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
@ -67,14 +95,17 @@ class ArticleListView(ListView):
'''
value = cache.get(cache_key)
if value:
# 如果缓存存在,记录日志并返回
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
# 从数据库获取数据并设置缓存
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
# 重写获取查询集的方法,使用缓存
def get_queryset(self):
'''
重写默认从缓存获取数据
@ -84,43 +115,61 @@ class ArticleListView(ListView):
value = self.get_queryset_from_cache(key)
return value
# 获取上下文数据的方法
def get_context_data(self, **kwargs):
# 添加链接类型到上下文
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
# 首页视图继承自ArticleListView
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
# 设置友情链接显示类型为首页
link_type = LinkShowType.I
# 获取查询集数据的方法
def get_queryset_data(self):
# 获取所有已发布的文章
article_list = Article.objects.filter(type='a', status='p')
return article_list
# 获取查询集缓存键的方法
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# 文章详情视图继承自DetailView
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
# 指定模板名称
template_name = 'blog/article_detail.html'
# 指定模型
model = Article
# URL参数中的主键名
pk_url_kwarg = 'article_id'
# 上下文对象名称
context_object_name = "article"
# 获取上下文数据的方法
def get_context_data(self, **kwargs):
# 创建评论表单实例
comment_form = CommentForm()
# 获取文章评论列表
article_comments = self.object.comment_list()
# 获取父级评论
parent_comments = article_comments.filter(parent_comment=None)
# 获取博客设置
blog_setting = get_blog_setting()
# 对父级评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# 获取评论页码
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
@ -131,25 +180,32 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页的评论
p_comments = paginator.page(page)
# 计算下一页和上一页
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 构建评论分页URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 添加上下文数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
# 添加上下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
@ -157,24 +213,31 @@ class ArticleDetailView(DetailView):
return context
# 分类详情视图继承自ArticleListView
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
# 获取查询集数据的方法
def get_queryset_data(self):
# 从URL参数获取分类slug
slug = self.kwargs['category_name']
# 获取分类对象不存在则返回404
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 获取这些分类下的所有已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# 获取查询集缓存键的方法
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
@ -184,10 +247,11 @@ class CategoryDetailView(ArticleListView):
categoryname=categoryname, page=self.page_number)
return cache_key
# 获取上下文数据的方法
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
# 处理分类名称,取最后一部分
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
@ -196,25 +260,31 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs)
# 作者详情视图继承自ArticleListView
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
# 获取查询集缓存键的方法
def get_queryset_cache_key(self):
from uuslug import slugify
# 对作者名称进行slugify处理
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
# 获取查询集数据的方法
def get_queryset_data(self):
author_name = self.kwargs['author_name']
# 获取该作者的所有已发布文章
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
# 获取上下文数据的方法
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
@ -222,21 +292,26 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs)
# 标签详情视图继承自ArticleListView
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
# 获取查询集数据的方法
def get_queryset_data(self):
slug = self.kwargs['tag_name']
# 获取标签对象不存在则返回404
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
# 获取该标签下的所有已发布文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
# 获取查询集缓存键的方法
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
@ -246,41 +321,51 @@ class TagDetailView(ArticleListView):
tag_name=tag_name, page=self.page_number)
return cache_key
# 获取上下文数据的方法
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
# 归档视图继承自ArticleListView
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
# 不分页
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
# 获取查询集数据的方法
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
# 获取查询集缓存键的方法
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
# 友情链接列表视图继承自ListView
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
# 获取查询集的方法
def get_queryset(self):
# 只返回启用的链接
return Links.objects.filter(is_enable=True)
# Elasticsearch搜索视图继承自Haystack的SearchView
class EsSearchView(SearchView):
# 获取上下文数据的方法
def get_context(self):
# 构建分页
paginator, page = self.build_page()
context = {
"query": self.query,
@ -289,6 +374,7 @@ class EsSearchView(SearchView):
"paginator": paginator,
"suggestion": None,
}
# 添加拼写建议
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
@ -296,6 +382,7 @@ class EsSearchView(SearchView):
return context
# 文件上传视图使用CSRF豁免
@csrf_exempt
def fileupload(request):
"""
@ -304,30 +391,42 @@ def fileupload(request):
:return:
"""
if request.method == 'POST':
# 获取签名参数
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
# 验证签名
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# 遍历所有上传的文件
for filename in request.FILES:
# 生成时间目录
timestr = timezone.now().strftime('%Y/%m/%d')
# 图片扩展名列表
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
# 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 构建保存路径
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# 生成唯一文件名
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全检查
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 如果是图片,进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
# 生成静态文件URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
@ -336,6 +435,7 @@ def fileupload(request):
return HttpResponse("only for post")
# 404错误页面视图
def page_not_found_view(
request,
exception,
@ -350,6 +450,7 @@ def page_not_found_view(
status=404)
# 500服务器错误页面视图
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
@ -358,6 +459,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
# 403权限拒绝页面视图
def permission_denied_view(
request,
exception,
@ -370,6 +472,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403)
# 清理缓存视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
return HttpResponse('ok')

@ -1,49 +1,76 @@
# 导入Django管理后台模块
from django.contrib import admin
# 导入URL反向解析
from django.urls import reverse
# 导入HTML格式化工具
from django.utils.html import format_html
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 禁用评论状态的管理动作函数
def disable_commentstatus(modeladmin, request, queryset):
# 将选中的评论设置为禁用状态
queryset.update(is_enable=False)
# 启用评论状态的管理动作函数
def enable_commentstatus(modeladmin, request, queryset):
# 将选中的评论设置为启用状态
queryset.update(is_enable=True)
# 设置管理动作的显示名称
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# 评论管理类
class CommentAdmin(admin.ModelAdmin):
# 每页显示数量
list_per_page = 20
# 列表页面显示的字段
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'link_to_userinfo', # 自定义用户信息链接字段
'link_to_article', # 自定义文章链接字段
'is_enable',
'creation_time')
# 列表页面可点击的链接字段
list_display_links = ('id', 'body', 'is_enable')
# 右侧过滤器字段
list_filter = ('is_enable',)
# 排除的表单字段
exclude = ('creation_time', 'last_modify_time')
# 管理动作列表
actions = [disable_commentstatus, enable_commentstatus]
# 原始ID字段用于优化大表查询
raw_id_fields = ('author', 'article')
# 搜索字段
search_fields = ('body',)
# 自定义用户信息链接字段方法
def link_to_userinfo(self, obj):
# 获取用户模型的元信息
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 生成用户编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回格式化的HTML链接显示用户昵称或邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# 自定义文章链接字段方法
def link_to_article(self, obj):
# 获取文章模型的元信息
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回格式化的HTML链接显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
link_to_article.short_description = _('Article')

@ -1,5 +1,8 @@
# 导入Django应用配置类
from django.apps import AppConfig
# 定义评论应用的配置类
class CommentsConfig(AppConfig):
name = 'comments'
# 指定应用的名称
name = 'comments'

@ -1,13 +1,21 @@
# 导入Django表单模块
from django import forms
# 导入模型表单基类
from django.forms import ModelForm
# 导入评论模型
from .models import Comment
# 评论表单类继承自ModelForm
class CommentForm(ModelForm):
# 父级评论ID字段使用隐藏输入控件非必填
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
# 表单元数据配置
class Meta:
# 指定关联的模型
model = Comment
fields = ['body']
# 表单中包含的字段,只包含评论正文
fields = ['body']

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由Django 4.1.7于2023-03-02 07:14自动生成
from django.conf import settings
from django.db import migrations, models
@ -7,32 +7,45 @@ import django.utils.timezone
class Migration(migrations.Migration):
# 初始迁移类
initial = True
# 依赖关系
dependencies = [
# 依赖博客应用的0001_initial迁移文件
('blog', '0001_initial'),
# 可交换的用户模型依赖
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 迁移操作列表
operations = [
# 创建评论模型
migrations.CreateModel(
name='Comment',
fields=[
# 主键ID字段自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 评论正文字段最大长度300字符
('body', models.TextField(max_length=300, verbose_name='正文')),
# 创建时间字段,默认使用当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段,默认使用当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 是否启用字段,控制评论是否显示
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 文章字段,外键关联文章模型
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 作者字段,外键关联用户模型
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 父级评论字段,外键自关联,支持评论回复功能
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
'verbose_name': '评论', # 单数显示名称
'verbose_name_plural': '评论', # 复数显示名称
'ordering': ['-id'], # 默认按ID降序排列
'get_latest_by': 'id', # 指定获取最新记录的字段
},
),
]
]

@ -1,18 +1,21 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
# 由Django 4.1.7于2023-04-24 13:48自动生成
from django.db import migrations, models
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 依赖comments应用的0001_initial迁移文件
('comments', '0001_initial'),
]
# 迁移操作列表
operations = [
# 修改Comment模型的is_enable字段的默认值
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]
]

@ -1,4 +1,4 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# 由Django 4.2.5于2023-09-06 13:13自动生成
from django.conf import settings
from django.db import migrations, models
@ -7,54 +7,67 @@ import django.utils.timezone
class Migration(migrations.Migration):
# 迁移依赖关系
dependencies = [
# 可交换的用户模型依赖
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
# 依赖blog应用的0005迁移文件
('blog', '0005_alter_article_options_alter_category_options_and_more'),
# 依赖comments应用的0002迁移文件
('comments', '0002_alter_comment_is_enable'),
]
# 迁移操作列表
operations = [
# 修改Comment模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
# 删除created_time字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 删除last_mod_time字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 新增creation_time字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 新增last_modify_time字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# 修改article字段的verbose_name
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
# 修改author字段的verbose_name
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
# 修改is_enable字段的verbose_name
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
# 修改parent_comment字段的verbose_name
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
),
]
]

@ -1,39 +1,59 @@
# 导入Django配置
from django.conf import settings
# 导入数据库模型
from django.db import models
# 导入时区工具
from django.utils.timezone import now
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入文章模型
from blog.models import Article
# Create your models here.
# 在此创建模型
# 评论模型类
class Comment(models.Model):
# 评论正文字段最大长度300字符
body = models.TextField('正文', max_length=300)
# 创建时间字段,默认使用当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段,默认使用当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 作者字段,外键关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# 文章字段,外键关联文章模型
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# 父级评论字段,外键自关联,支持评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
# 是否启用字段,控制评论是否显示
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
# 模型元数据配置
class Meta:
# 默认按ID降序排列
ordering = ['-id']
# 单数显示名称
verbose_name = _('comment')
# 复数显示名称(与单数相同)
verbose_name_plural = verbose_name
# 指定获取最新记录的字段
get_latest_by = 'id'
# 对象的字符串表示方法
def __str__(self):
return self.body
# 使用评论正文作为对象的字符串表示
return self.body

@ -1,80 +1,114 @@
# 导入Django测试相关模块
from django.test import Client, RequestFactory, TransactionTestCase
# 导入URL反向解析
from django.urls import reverse
# 导入账户模型
from accounts.models import BlogUser
# 导入博客模型
from blog.models import Category, Article
# 导入评论模型
from comments.models import Comment
# 导入评论模板标签
from comments.templatetags.comments_tags import *
# 导入工具函数
from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
# 在此创建测试
# 评论测试类继承自TransactionTestCase
class CommentsTest(TransactionTestCase):
# 测试前置设置方法
def setUp(self):
# 创建测试客户端
self.client = Client()
# 创建请求工厂
self.factory = RequestFactory()
# 导入博客设置模型
from blog.models import BlogSettings
value = BlogSettings()
# 设置评论需要审核
value.comment_need_review = True
value.save()
# 创建超级用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# 更新文章评论状态的方法
def update_article_comment_status(self, article):
# 获取文章的所有评论
comments = article.comment_set.all()
# 将所有评论设置为启用状态
for comment in comments:
comment.is_enable = True
comment.save()
# 测试评论验证功能
def test_validate_comment(self):
# 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建分类
category = Category()
category.name = "categoryccc"
category.save()
# 创建文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# 提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
# 断言重定向响应
self.assertEqual(response.status_code, 302)
# 重新获取文章对象
article = Article.objects.get(pk=article.pk)
# 断言评论列表为空(因为评论需要审核)
self.assertEqual(len(article.comment_list()), 0)
# 更新评论状态为启用
self.update_article_comment_status(article)
# 断言评论列表长度为1
self.assertEqual(len(article.comment_list()), 1)
# 提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
# 断言重定向响应
self.assertEqual(response.status_code, 302)
# 重新获取文章对象并更新评论状态
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
# 断言评论列表长度为2
self.assertEqual(len(article.comment_list()), 2)
# 获取第一条评论的ID作为父评论ID
parent_comment_id = article.comment_list()[0].id
# 提交带Markdown格式的回复评论
response = self.client.post(comment_url,
{
'body': '''
@ -90,20 +124,29 @@ class CommentsTest(TransactionTestCase):
''',
'parent_comment_id': parent_comment_id
'parent_comment_id': parent_comment_id # 设置父评论ID
})
# 断言重定向响应
self.assertEqual(response.status_code, 302)
# 更新评论状态并重新获取文章
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
# 断言评论列表长度为3
self.assertEqual(len(article.comment_list()), 3)
# 获取父评论对象
comment = Comment.objects.get(id=parent_comment_id)
# 解析评论树
tree = parse_commenttree(article.comment_list(), comment)
# 断言评论树长度为1
self.assertEqual(len(tree), 1)
# 测试显示评论项模板标签
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# 测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
# 测试发送评论邮件功能
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment)

@ -1,11 +1,17 @@
# 导入Django URL路由相关模块
from django.urls import path
# 导入当前应用的视图模块
from . import views
# 定义应用命名空间
app_name = "comments"
# 定义URL模式列表
urlpatterns = [
# 文章评论提交URL包含文章ID参数
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]
'article/<int:article_id>/postcomment', # URL路径模式
views.CommentPostView.as_view(), # 使用CommentPostView类视图处理
name='postcomment'), # URL名称
]

@ -1,17 +1,26 @@
# 导入日志模块
import logging
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入工具函数
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# 获取日志器
logger = logging.getLogger(__name__)
# 发送评论邮件函数
def send_comment_email(comment):
# 获取当前站点域名
site = get_current_site().domain
# 邮件主题
subject = _('Thanks for your comment')
# 构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 构建感谢评论的邮件HTML内容
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
@ -19,10 +28,15 @@ def send_comment_email(comment):
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
# 获取评论作者的邮箱
tomail = comment.author.email
# 发送感谢评论邮件
send_email([tomail], subject, html_content)
# 尝试发送回复通知邮件(如果这是对某条评论的回复)
try:
if comment.parent_comment:
# 构建回复通知的邮件HTML内容
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
@ -32,7 +46,10 @@ def send_comment_email(comment):
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# 获取被回复评论作者的邮箱
tomail = comment.parent_comment.author.email
# 发送回复通知邮件
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)
# 记录发送回复通知邮件时的错误
logger.error(e)

@ -1,63 +1,105 @@
# Create your views here.
# 在此创建视图
# 导入验证错误异常
from django.core.exceptions import ValidationError
# 导入HTTP重定向响应
from django.http import HttpResponseRedirect
# 导入快捷函数
from django.shortcuts import get_object_or_404
# 导入方法装饰器
from django.utils.decorators import method_decorator
# 导入CSRF保护装饰器
from django.views.decorators.csrf import csrf_protect
# 导入表单视图基类
from django.views.generic.edit import FormView
# 导入账户模型
from accounts.models import BlogUser
# 导入博客模型
from blog.models import Article
# 导入评论表单
from .forms import CommentForm
# 导入评论模型
from .models import Comment
# 评论提交视图类继承自FormView
class CommentPostView(FormView):
# 指定使用的表单类
form_class = CommentForm
# 指定模板名称
template_name = 'blog/article_detail.html'
# 使用CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# GET请求处理方法
def get(self, request, *args, **kwargs):
# 从URL参数获取文章ID
article_id = self.kwargs['article_id']
# 获取文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 获取文章绝对URL
url = article.get_absolute_url()
# 重定向到文章详情页的评论区域
return HttpResponseRedirect(url + "#comments")
# 表单验证失败时的处理方法
def form_invalid(self, form):
# 从URL参数获取文章ID
article_id = self.kwargs['article_id']
# 获取文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 重新渲染模板,显示表单错误
return self.render_to_response({
'form': form,
'article': article
})
# 表单验证成功时的处理方法
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# 获取当前用户
user = self.request.user
# 获取用户对象
author = BlogUser.objects.get(pk=user.pk)
# 从URL参数获取文章ID
article_id = self.kwargs['article_id']
# 获取文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 检查文章评论状态
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# 创建评论对象但不保存到数据库
comment = form.save(False)
# 设置评论关联的文章
comment.article = article
# 获取博客设置
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
# 如果不需要审核,直接启用评论
if not settings.comment_need_review:
comment.is_enable = True
# 设置评论作者
comment.author = author
# 处理父级评论(回复评论的情况)
if form.cleaned_data['parent_comment_id']:
# 获取父级评论对象
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
# 设置父级评论
comment.parent_comment = parent_comment
# 保存评论到数据库
comment.save(True)
# 重定向到文章详情页的特定评论位置
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
(article.get_absolute_url(), comment.pk))
Loading…
Cancel
Save