组员修改代码文件

master
施周易 3 months ago
parent 23ea3c67d1
commit 4583e5cc70

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,64 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,18 +1,25 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.documents模块导入需要用到的文档类和管理器以及Elasticsearch启用状态常量
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_index 时会显示
help = 'build search index'
def handle(self, *args, **options):
# 检查是否启用了Elasticsearch功能
if ELASTICSEARCH_ENABLED:
# 构建时间文档索引
ElaspedTimeDocumentManager.build_index()
# 创建ElapsedTimeDocument实例并初始化
manager = ElapsedTimeDocument()
manager.init()
# 创建ArticleDocumentManager实例删除现有索引后重新构建
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()
manager.rebuild()

@ -1,13 +1,18 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入Tag和Category模型
from blog.models import Tag, Category
# TODO 参数化
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_search_words 时会显示
help = 'build search words'
def handle(self, *args, **options):
# 收集所有标签(Tag)和分类(Category)的名称并用set去重
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,16 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从djangoblog.utils模块导入缓存工具
from djangoblog.utils import cache
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help clear_cache 时会显示
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,61 @@
# 导入获取用户模型的函数用于操作Django内置的用户认证系统
from django.contrib.auth import get_user_model
# 导入密码加密函数,用于安全地存储用户密码
from django.contrib.auth.hashers import make_password
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help create_testdata 时会显示
help = 'create test datas'
def handle(self, *args, **options):
# 获取或创建一个测试用户邮箱为test@test.com用户名为"测试用户",密码经过加密处理
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]
# 获取或创建一个子分类,名称为"子类目"父级分类为上面创建的pcategory
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# 保存分类虽然get_or_create已经保存过这里再次显式调用save
category.save()
# 创建一个基础标签对象,名称为"标签"
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 循环创建19篇文章序号从1到19
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
# 从djangoblog.spider_notify模块导入SpiderNotify类用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
# 从djangoblog.utils模块导入get_current_site函数用于获取当前站点信息
from djangoblog.utils import get_current_site
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
# 获取当前站点的域名
site = get_current_site().domain
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help ping_baidu 时会显示
help = 'notify baidu url'
# 添加命令行参数
def add_arguments(self, parser):
# 添加data_type参数指定要通知的数据类型
parser.add_argument(
'data_type',
type=str,
# 限制参数值只能是以下几种选项
choices=[
'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'):
for article in Article.objects.filter(status='p'): # status='p'表示已发布(published)
urls.append(article.get_full_url())
# 如果是指定标签或全部则获取所有标签页面的URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
url = tag.get_absolute_url() # 获取标签的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 如果是指定分类或全部则获取所有分类页面的URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
url = category.get_absolute_url() # 获取分类的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 输出即将通知的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用百度通知接口推送所有URL
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))
# 输出完成通知的信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,47 +1,84 @@
# 导入requests库用于发送HTTP请求测试图片链接有效性
import requests
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 导入static函数用于获取静态文件的URL
from django.templatetags.static import static
# 从djangoblog.utils模块导入save_user_avatar函数用于保存用户头像
from djangoblog.utils import save_user_avatar
# 从oauth.models模块导入OAuthUser模型用于操作OAuth用户
from oauth.models import OAuthUser
# 从oauth.oauthmanager模块导入get_manager_by_type函数用于获取对应类型的OAuth管理器
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help sync_user_avatar 时会显示
help = 'sync user avatar'
# 测试图片URL是否有效的方法
def test_picture(self, url):
try:
# 发送GET请求测试URL设置2秒超时
if requests.get(url, timeout=2).status_code == 200:
# 如果返回状态码为200说明图片有效返回True
return True
except:
# 捕获异常如网络错误或超时直接pass
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
# 如果用户有头像URL
if url:
# 如果头像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 = save_user_avatar(url)
else:
# 如果用户没有头像,则使用默认头像
url = static('blog/img/avatar.png')
# 如果获取到了有效的头像URL
if url:
# 输出同步完成的信息和新的头像URL
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 更新用户的头像URL并保存
u.picture = url
u.save()
self.stdout.write('结束同步')
# 输出同步结束信息
self.stdout.write('结束同步')

@ -1,137 +1,219 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由Django 4.1.7在2023年3月2日生成的初始数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
# 标记这是一个初始迁移文件
initial = True
# 定义依赖关系,依赖于用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义具体的操作
operations = [
# 创建BlogSettings模型用于存储网站配置信息
migrations.CreateModel(
name='BlogSettings',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 网站名称最大长度200默认为空字符串
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
# 网站描述文本字段最大长度1000默认为空字符串
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
# 网站SEO描述文本字段最大长度1000默认为空字符串
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键字文本字段最大长度1000默认为空字符串
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
# 文章摘要长度整数类型默认300
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
# 侧边栏文章数目整数类型默认10
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
# 侧边栏评论数目整数类型默认5
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
# 文章页面默认显示评论数目整数类型默认5
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
# 是否显示谷歌广告布尔类型默认False
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 广告内容文本字段最大长度2000可为空默认为空字符串
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
# 是否打开网站评论功能布尔类型默认True
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 备案号字符字段最大长度2000可为空默认为空字符串
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
# 网站统计代码文本字段最大长度1000默认为空字符串
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案号布尔类型默认False
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
# 公安备案号文本字段最大长度2000可为空默认为空字符串
('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': '网站配置', # 复数形式的可读名称
},
),
# 创建Links模型用于存储友情链接信息
migrations.CreateModel(
name='Links',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址URL字段
('link', models.URLField(verbose_name='链接地址')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否显示布尔类型默认True
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 显示类型字符字段最大长度1可选值包括首页、列表页、文章页面、全站、友情链接页面默认为首页
('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'], # 默认排序按sequence字段升序排列
},
),
# 创建SideBar模型用于存储侧边栏信息
migrations.CreateModel(
name='SideBar',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 标题最大长度100
('name', models.CharField(max_length=100, verbose_name='标题')),
# 内容,文本字段
('content', models.TextField(verbose_name='内容')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用布尔类型默认True
('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'], # 默认排序按sequence字段升序排列
},
),
# 创建Tag模型用于存储文章标签
migrations.CreateModel(
name='Tag',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 标签名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('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'], # 默认排序按name字段升序排列
},
),
# 创建Category模型用于存储文章分类
migrations.CreateModel(
name='Category',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 分类名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 权重排序整数类型默认0数值越大越靠前
('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'], # 默认排序按index字段降序排列
},
),
# 创建Article模型用于存储文章信息
migrations.CreateModel(
name='Article',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 标题最大长度200唯一约束
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 正文使用MDTextField类型Markdown编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间,日期时间字段,默认为当前时间
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
# 文章状态字符字段最大长度1可选值为草稿和发表默认为发表
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
# 评论状态字符字段最大长度1可选值为打开和关闭默认为打开
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
# 类型字符字段最大长度1可选值为文章和页面默认为文章
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量正整数类型默认0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 文章排序整数类型默认0数值越大越靠前
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示toc目录布尔类型默认False
('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模型级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
# 标签集合多对多关系关联到Tag模型可为空
('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'], # 默认排序先按article_order降序再按pub_time降序
'get_latest_by': 'id', # latest()方法使用的默认字段
},
),
]
]

@ -1,23 +1,28 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
# 由Django 4.1.7在2023年3月29日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0001_initial
dependencies = [
('blog', '0001_initial'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段global_footer公共尾部
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
model_name='blogsettings', # 目标模型名称
name='global_footer', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共尾部"
),
# 向BlogSettings模型添加新字段global_header公共头部
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
model_name='blogsettings', # 目标模型名称
name='global_header', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共头部"
),
]
]

@ -1,17 +1,21 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0002_blogsettings_global_footer_and_more
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段comment_need_review评论是否需要审核
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
model_name='blogsettings', # 目标模型名称
name='comment_need_review', # 新增字段名
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # 字段定义布尔字段默认值为False显示名为"评论是否需要审核"
),
]
]

@ -1,27 +1,35 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0003_blogsettings_comment_need_review
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# 定义具体的操作
operations = [
# 重命名BlogSettings模型中的字段analyticscode -> analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
model_name='blogsettings', # 目标模型名称
old_name='analyticscode', # 原字段名
new_name='analytics_code', # 新字段名
),
# 重命名BlogSettings模型中的字段beiancode -> beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
model_name='blogsettings', # 目标模型名称
old_name='beiancode', # 原字段名
new_name='beian_code', # 新字段名
),
# 重命名BlogSettings模型中的字段sitename -> site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
model_name='blogsettings', # 目标模型名称
old_name='sitename', # 原字段名
new_name='site_name', # 新字段名
),
]
]

@ -1,172 +1,216 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# 由Django 4.2.5在2023年9月6日生成的数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
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 = [
# 修改Article模型的选项设置
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# 修改Category模型的选项设置
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# 修改Links模型的选项设置
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# 修改SideBar模型的选项设置
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# 修改Tag模型的选项设置
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 移除Article模型中的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
# 移除Article模型中的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# 移除Category模型中的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
# 移除Category模型中的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# 移除Links模型中的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# 移除SideBar模型中的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# 移除Tag模型中的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
# 移除Tag模型中的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# 为Article模型添加creation_time字段创建时间
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Article模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Category模型添加creation_time字段创建时间
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Category模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Links模型添加creation_time字段创建时间
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为SideBar模型添加creation_time字段创建时间
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加creation_time字段创建时间
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型的article_order字段显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
# 修改Article模型的author字段显示名称
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'),
),
# 修改Article模型的body字段显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
# 修改Article模型的category字段显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
# 修改Article模型的comment_status字段选项和显示名称
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'),
),
# 修改Article模型的pub_time字段显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
# 修改Article模型的show_toc字段显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
# 修改Article模型的status字段选项和显示名称
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
# 修改Article模型的tags字段显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
# 修改Article模型的title字段显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
# 修改Article模型的type字段选项和显示名称
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
# 修改Article模型的views字段显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的多个字段显示名称英文化
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
@ -222,6 +266,8 @@ class Migration(migrations.Migration):
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的多个字段显示名称
migrations.AlterField(
model_name='category',
name='index',
@ -237,6 +283,8 @@ class Migration(migrations.Migration):
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型的多个字段显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
@ -267,6 +315,8 @@ class Migration(migrations.Migration):
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'),
),
# 修改SideBar模型的多个字段显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
@ -292,9 +342,11 @@ class Migration(migrations.Migration):
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的name字段显示名称
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年1月26日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0005_alter_article_options_alter_category_options_and_more
dependencies = [
('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'},
name='blogsettings', # 目标模型名称
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, # 新的模型选项:将单数和复数形式的显示名称都改为"Website configuration"
),
]
]

@ -1,107 +1,156 @@
#zf导入所需的Python标准库
#zf用于生成MD5哈希值
import hashlib
#zf用于日志记录
import logging
#zf用于随机选择
import random
#zf用于URL编码
import urllib
#zf导入Django相关模块
#zfDjango模板系统
from django import template
#zfDjango配置
from django.conf import settings
#zfDjango查询对象
from django.db.models import Q
#zf获取对象或返回404
from django.shortcuts import get_object_or_404
#zf字符串过滤器装饰器
from django.template.defaultfilters import stringfilter
#zf静态文件处理
from django.templatetags.static import static
#zfURL反向解析
from django.urls import reverse
#zf标记安全字符串
from django.utils.safestring import mark_safe
#zf导入项目相关模块
#zf博客模型
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
#zf评论模型
from comments.models import Comment
#zfMarkdown处理和HTML清理
from djangoblog.utils import CommonMarkdown, sanitize_html
#zf缓存工具
from djangoblog.utils import cache
#zf获取当前站点
from djangoblog.utils import get_current_site
#zfOAuth用户模型
from oauth.models import OAuthUser
#zf插件管理
from djangoblog.plugin_manage import hooks
#zf创建日志记录器
logger = logging.getLogger(__name__)
#zf注册模板标签库
register = template.Library()
#zf定义head_meta简单标签用于在模板中插入头部元信息
@register.simple_tag(takes_context=True)
def head_meta(context):
#zf应用插件过滤器返回安全的HTML字符串
return mark_safe(hooks.apply_filters('head_meta', '', context))
#zf定义timeformat简单标签用于格式化时间
@register.simple_tag
def timeformat(data):
try:
#zf使用settings中定义的时间格式格式化数据
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义datetimeformat简单标签用于格式化日期时间
@register.simple_tag
def datetimeformat(data):
try:
#zf使用settings中定义的日期时间格式格式化数据
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义custom_markdown过滤器用于将Markdown内容转换为HTML
@register.filter()
@stringfilter
def custom_markdown(content):
#zf使用CommonMarkdown工具转换Markdown并标记为安全HTML
return mark_safe(CommonMarkdown.get_markdown(content))
#zf定义get_markdown_toc简单标签用于获取Markdown内容的目录
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
#zf获取Markdown内容和目录
body, toc = CommonMarkdown.get_markdown_with_toc(content)
#zf标记目录为安全HTML并返回
return mark_safe(toc)
#zf定义comment_markdown过滤器用于处理评论中的Markdown内容
@register.filter()
@stringfilter
def comment_markdown(content):
#zf转换Markdown内容
content = CommonMarkdown.get_markdown(content)
#zf清理HTML并标记为安全
return mark_safe(sanitize_html(content))
#zf定义truncatechars_content过滤器用于截取文章内容摘要
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
:param content: 文章内容
:return: 截取后的摘要
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
#zf获取博客设置
blogsetting = get_blog_setting()
#zf根据设置的文章摘要长度截取内容
return truncatechars_html(content, blogsetting.article_sub_length)
#zf定义truncate过滤器用于截取内容前150个字符
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
#zf去除HTML标签并截取前150个字符
return strip_tags(content)[:150]
#zf定义load_breadcrumb包含标签用于加载文章面包屑导航
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
:param article: 文章对象
:return: 面包屑数据
"""
#zf获取文章的分类树
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
#zf获取博客设置和当前站点
blogsetting = get_blog_setting()
site = get_current_site().domain
#zf添加站点名称到面包屑
names.append((blogsetting.site_name, '/'))
#zf反转面包屑顺序
names = names[::-1]
return {
@ -111,18 +160,22 @@ def load_breadcrumb(article):
}
#zf定义load_articletags包含标签用于加载文章标签列表
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
:param article: 文章对象
:return: 标签列表数据
"""
#zf获取文章的所有标签
tags = article.tags.all()
tags_list = []
for tag in tags:
#zf获取标签URL和文章数量
url = tag.get_absolute_url()
count = tag.get_article_count()
#zf添加标签信息到列表随机选择Bootstrap颜色
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
@ -131,214 +184,17 @@ def load_articletags(article):
}
#zf定义load_sidebar包含标签用于加载侧边栏内容
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
:return: 侧边栏数据
"""
#zf尝试从缓存获取侧边栏数据
value = cache.get("sidebar" + linktype)
if value:
#zf如果缓存存在添加用户信息并返回
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
else

@ -0,0 +1,247 @@
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
if isinstance(instance, LogEntry):
return
is_update_views = update_fields == {'views'}
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()

@ -1,3 +1,4 @@
#zr 初始数据库迁移文件:创建评论表结构
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
@ -5,29 +6,42 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 初始迁移
initial = True
#zr 依赖关系
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#zr 迁移操作
operations = [
#zr 创建评论表
migrations.CreateModel(
name='Comment',
fields=[
#zr 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#zr 评论正文字段
('body', models.TextField(max_length=300, verbose_name='正文')),
#zr 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#zr 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#zr 是否显示字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#zr 文章外键关联
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#zr 作者外键关联
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#zr 父评论自关联
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#zr 模型元选项
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',

@ -1,18 +1,23 @@
#zr 数据库迁移文件:修改评论是否显示字段的默认值
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
('comments', '0001_initial'),
]
#zr 迁移操作
operations = [
#zr 修改comment模型的is_enable字段
migrations.AlterField(
model_name='comment',
name='is_enable',
#zr 将默认值改为False并更新显示名称
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -1,3 +1,4 @@
#zr 数据库迁移文件:更新评论模型字段和选项
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
@ -5,56 +6,68 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#zr 迁移操作列表
operations = [
#zr 更新评论模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#zr 移除旧的创建时间字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#zr 移除旧的最后修改时间字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#zr 添加新的创建时间字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#zr 添加新的最后修改时间字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#zr 更新文章外键字段配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#zr 更新作者外键字段配置
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'),
),
#zr 更新是否启用字段配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#zr 更新父评论外键字段配置
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,2 +1 @@
# szy:此文件用于将当前目录识别为一个Python包
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,4 +1,3 @@
# szy功能描述自定义Django后台管理站点并注册各个模型
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
@ -17,16 +16,14 @@ from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# szy自定义Django后台管理站点并注册各个模型
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
# szy初始化管理站点设置站点名称
def __init__(self, name='admin'):
super().__init__(name)
# szy检查用户权限是否为超级管理员
def has_permission(self, request):
return request.user.is_superuser
@ -40,7 +37,7 @@ class DjangoBlogAdminSite(AdminSite):
# ]
# return urls + my_urls
# szy注册各个模型到后台管理
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)

@ -1,11 +1,9 @@
from django.apps import AppConfig
# szyDjango应用配置类用于加载插件
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
# szy应用准备时加载插件
def ready(self):
super().ready()
# Import and load plugins here

@ -1,6 +1,6 @@
# szy定义Django信号并处理相关业务逻辑
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
@ -9,11 +9,13 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
@ -23,7 +25,110 @@ oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
# szy处理发送邮件的信号
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
@ -51,7 +156,7 @@ def send_email_signal_handler(sender, **kwargs):
log.send_result = False
log.save()
# szy处理OAuth用户登录信号
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
@ -74,22 +179,44 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
clearcache = False
if isinstance(instance, LogEntry):
return
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
if not is_update_views:
clearcache = True
is_update_views = update_fields == {'views'}
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
@ -110,9 +237,6 @@ def model_post_save_callback(
_thread.start_new_thread(send_comment_email, (instance,))
if clearcache:
cache.clear()
@receiver(user_logged_in)
@receiver(user_logged_out)

@ -10,7 +10,7 @@ from blog.models import Article
logger = logging.getLogger(__name__)
# szy定义Elasticsearch后端处理索引和查询
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
@ -21,46 +21,38 @@ class ElasticSearchBackend(BaseSearchBackend):
self.manager = ArticleDocumentManager()
self.include_spelling = True
# szy获取要索引的模型数据
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
# szy创建索引
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
# szy删除索引
def _delete(self, models):
for m in models:
m.delete()
return True
# szy重组索引
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
# szy更新索引
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
# szy移除索引
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
# szy清空索引
def clear(self, models=None, commit=True):
self.remove(None)
# szy获取搜索建议词
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
@ -79,7 +71,6 @@ class ElasticSearchBackend(BaseSearchBackend):
return ' '.join(keywords)
# szy执行搜索并返回结果
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
@ -93,13 +84,10 @@ class ElasticSearchBackend(BaseSearchBackend):
else:
suggestion = query_string
# szy构建查询条件匹配标题或正文设置最小匹配度
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
# szy执行搜索查询过滤已发布的状态和文章类型
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
@ -109,8 +97,6 @@ class ElasticSearchBackend(BaseSearchBackend):
results = search.execute()
hits = results['hits'].total
raw_results = []
# szy处理搜索结果构建SearchResult对象
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
@ -126,8 +112,6 @@ class ElasticSearchBackend(BaseSearchBackend):
**additional_fields)
raw_results.append(result)
facets = {}
# szy设置拼写建议如果查询词与建议词不同则返回建议词
spelling_suggestion = None if query_string == suggestion else suggestion
return {
@ -137,7 +121,7 @@ class ElasticSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy定义Elasticsearch查询类
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
@ -145,7 +129,6 @@ class ElasticSearchQuery(BaseSearchQuery):
else:
return force_str(date.strftime('%Y%m%d000000'))
# szy清理查询片段处理保留字和特殊字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -171,35 +154,30 @@ class ElasticSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
# szy构建查询片段
def build_query_fragment(self, field, filter_type, value):
return value.query_string
# szy获取查询结果数量
def get_count(self):
results = self.get_results()
return len(results) if results else 0
# szy获取拼写建议
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
# szy构建查询参数
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
# szy定义Elasticsearch模型搜索表单
class ElasticSearchModelSearchForm(ModelSearchForm):
# szy重写搜索方法支持建议搜索功能
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
# szy定义Elasticsearch搜索引擎
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -6,44 +6,35 @@ from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# szy定义Django博客的RSS订阅类
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
# szy设置Feed的描述、标题和链接
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
# szy获取作者名称博客主昵称
def author_name(self):
return get_user_model().objects.first().nickname
# szy获取作者链接地址
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
# szy获取要包含在Feed中的文章项
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# szy获取文章项的标题
def item_title(self, item):
return item.title
# szy获取文章项的描述使用Markdown渲染正文
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
# szy设置Feed的版权信息
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
# szy获取文章项的链接地址
def item_link(self, item):
return item.get_absolute_url()
# szy获取文章项的全局唯一标识符
def item_guid(self, item):
return

@ -7,26 +7,21 @@ from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
# szy定义日志条目管理类
class LogEntryAdmin(admin.ModelAdmin):
# szy设置列表页过滤器字段
list_filter = [
'content_type'
]
# szy设置搜索字段
search_fields = [
'object_repr',
'change_message'
]
# szy设置列表页可点击链接的字段
list_display_links = [
'action_time',
'get_change_message',
]
# szy设置列表页显示的字段
list_display = [
'action_time',
'user_link',
@ -35,22 +30,18 @@ class LogEntryAdmin(admin.ModelAdmin):
'get_change_message',
]
# szy禁用添加权限
def has_add_permission(self, request):
return False
# szy设置修改权限仅超级用户或有特定权限的用户可以查看
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
# szy禁用删除权限
def has_delete_permission(self, request, obj=None):
return False
# szy创建对象链接如果是删除操作则不生成链接
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
@ -71,7 +62,6 @@ class LogEntryAdmin(admin.ModelAdmin):
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
# szy创建用户链接链接到用户编辑页面
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
@ -90,12 +80,10 @@ class LogEntryAdmin(admin.ModelAdmin):
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
# szy优化查询集预取关联的内容类型数据
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
# szy重写获取actions的方法移除删除选中项的action
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:

@ -1,22 +1,45 @@
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
# szy基础插件类所有插件都应该继承此类
class BasePlugin:
# szy插件元数据定义
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
PLUGIN_AUTHOR = None
# 插件配置
SUPPORTED_POSITIONS = [] # 支持的显示位置
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
# szy插件初始化方法
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 设置插件路径
self.plugin_dir = self._get_plugin_directory()
self.plugin_slug = self._get_plugin_slug()
self.init_plugin()
self.register_hooks()
# szy插件初始化逻辑子类可重写实现特定初始化
def _get_plugin_directory(self):
"""获取插件目录路径"""
import inspect
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件标识符(目录名)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑
@ -24,7 +47,6 @@ class BasePlugin:
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
# szy注册插件钩子子类可重写实现特定钩子注册
def register_hooks(self):
"""
注册插件钩子
@ -32,7 +54,129 @@ class BasePlugin:
"""
pass
# szy获取插件信息
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据位置渲染插件组件
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
dict: {'html': 'HTML内容', 'priority': 优先级} None
"""
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查条件显示
if not self.should_display(position, context, **kwargs):
return None
# 调用具体的位置渲染方法
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
html = getattr(self, method_name)(context, **kwargs)
if html:
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示
子类可重写此方法实现条件显示逻辑
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: 是否显示
"""
return True
# === 各位置渲染方法 - 子类重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件模板
Args:
template_name: 模板文件名
context: 模板上下文
Returns:
HTML字符串
"""
if context is None:
context = {}
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
return render_to_string(template_path, context)
except TemplateDoesNotExist:
logger.warning(f"Plugin template not found: {template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""获取插件静态文件URL"""
from django.templatetags.static import static
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""获取插件CSS文件列表"""
return []
def get_js_files(self):
"""获取插件JavaScript文件列表"""
return []
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""
def get_plugin_info(self):
"""
获取插件信息
@ -41,5 +185,10 @@ class BasePlugin:
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -1,10 +1,22 @@
# szy文章详情加载事件钩子名称
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# szy文章创建事件钩子名称
ARTICLE_CREATE = 'article_create'
# szy文章更新事件钩子名称
ARTICLE_UPDATE = 'article_update'
# szy文章删除事件钩子名称
ARTICLE_DELETE = 'article_delete'
# szy文章内容处理钩子名称
ARTICLE_CONTENT_HOOK_NAME = "the_content"
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量
POSITION_HOOKS = {
'article_top': 'article_top_widgets',
'article_bottom': 'article_bottom_widgets',
'sidebar': 'sidebar_widgets',
'header': 'header_widgets',
'footer': 'footer_widgets',
'comment_before': 'comment_before_widgets',
'comment_after': 'comment_after_widgets',
}
# 资源注入钩子
HEAD_RESOURCES_HOOK = 'head_resources'
BODY_RESOURCES_HOOK = 'body_resources'

@ -1,7 +1,7 @@
import logging
logger = logging.getLogger(__name__)
# szy全局钩子字典存储所有注册的钩子
_hooks = {}
@ -34,15 +34,11 @@ def apply_filters(hook_name: str, value, *args, **kwargs):
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
# szy检查钩子是否存在存在则依次应用所有过滤器
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
# szy将当前值传递给回调函数进行处理并更新值
value = callback(value, *args, **kwargs)
except Exception as e:
# szy捕获并记录过滤器执行过程中的异常
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# szy返回处理后的最终值
return value

@ -4,21 +4,61 @@ from django.conf import settings
logger = logging.getLogger(__name__)
# 全局插件注册表
_loaded_plugins = []
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
# szy遍历settings中配置的激活插件列表
global _loaded_plugins
_loaded_plugins = []
for plugin_name in settings.ACTIVE_PLUGINS:
# szy构建插件路径
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# szy检查插件目录是否存在且包含plugin.py文件
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# szy动态导入插件模块
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
# 导入插件模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 获取插件实例
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
# szy记录插件导入失败的错误信息
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""获取所有已加载的插件"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""根据名称获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""根据slug获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""获取所有插件的信息"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""获取支持指定位置的插件"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -13,35 +13,47 @@ import os
import sys
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
# szy将环境变量转换为布尔值
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# szy构建项目基础路径
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
_ENV_SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Default to DEBUG=True only when no DJANGO_SECRET_KEY is provided, so local development works out of the box.
DEBUG = env_to_bool('DJANGO_DEBUG', _ENV_SECRET_KEY is None)
def get_secret_key():
if _ENV_SECRET_KEY:
return _ENV_SECRET_KEY
if DEBUG:
# Provide a deterministic key for convenience in local development.
return 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required when DEBUG=False')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
SECRET_KEY = get_secret_key()
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# Allow configuring host whitelist via environment, fallback to safe defaults
_default_hosts = '127.0.0.1,localhost'
ALLOWED_HOSTS = [host.strip() for host in os.environ.get('DJANGO_ALLOWED_HOSTS', _default_hosts).split(',') if host.strip()]
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# szy已安装的应用列表
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
@ -64,7 +76,6 @@ INSTALLED_APPS = [
'djangoblog'
]
# szy中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
@ -84,7 +95,6 @@ MIDDLEWARE = [
ROOT_URLCONF = 'djangoblog.urls'
# szy模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -108,21 +118,22 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# szyMySQL数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '12345678',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
'CONN_MAX_AGE': int(os.environ.get('DJANGO_DB_CONN_MAX_AGE', 60)),
}}
# szy密码验证配置
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@ -141,26 +152,23 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# szy国际化配置 - 支持的语言
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
# szy本地化文件路径
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
# szy默认语言代码
LANGUAGE_CODE = 'zh-hans'
# szy时区设置
TIME_ZONE = 'Asia/Shanghai'
# szy启用国际化
USE_I18N = True
# szy启用本地化
USE_L10N = True
# szy使用时区支持
USE_TZ = False
# Static files (CSS, JavaScript, Images)
@ -184,10 +192,15 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# szy同时收集项目静态目录与插件资源避免部署缺文件
STATICFILES_DIRS = [
STATICFILES,
os.path.join(BASE_DIR, 'plugins'),
]
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
# szy时间和日期格式
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
@ -196,13 +209,11 @@ BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# szy分页设置
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# szy缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -218,12 +229,11 @@ if os.environ.get("DJANGO_REDIS_URL"):
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
# szy站点ID
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# szy邮件配置
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
@ -240,7 +250,6 @@ ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# szy日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
@ -297,11 +306,6 @@ LOGGING = {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
@ -312,26 +316,66 @@ STATICFILES_FINDERS = (
# other
'compressor.finders.CompressorFinder',
)
# szy启用压缩
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
# 压缩输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
# 压缩文件名模板 - 包含哈希值用于缓存破坏
COMPRESS_CSS_HASHING_METHOD = 'mtime'
COMPRESS_JS_HASHING_METHOD = 'mtime'
# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
# CSS压缩器 - 高压缩等级
'compressor.filters.cssmin.CSSCompressorFilter',
]
# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
# JS压缩器 - 高压缩等级
'compressor.filters.jsmin.SlimItFilter',
]
# szy媒体文件配置
# 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default'
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
# 预压缩配置
COMPRESS_PRECOMPILERS = (
# 支持SCSS/SASS
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
# 压缩等级配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件缓存配置
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# 浏览器缓存配置(通过中间件或服务器配置)
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
AVATAR_ROOT = os.path.join(MEDIA_ROOT, 'avatars')
AVATAR_URL = f'{MEDIA_URL}avatars/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
@ -346,7 +390,6 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
},
}
# szy插件系统配置
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
@ -354,5 +397,7 @@ ACTIVE_PLUGINS = [
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]
'seo_optimizer',
'image_lazy_loading',
'article_recommendation',
]

@ -3,72 +3,57 @@ from django.urls import reverse
from blog.models import Article, Category, Tag
# szy静态视图站点地图类
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
# szy定义包含在站点地图中的项
def items(self):
return ['blog:index', ]
# szy获取项的URL位置
def location(self, item):
return reverse(item)
# szy文章站点地图类
class ArticleSiteMap(Sitemap):
# szy设置文章更新频率和优先级
changefreq = "monthly"
priority = "0.6"
# szy获取所有已发布的文章
def items(self):
return Article.objects.filter(status='p')
# szy获取文章的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy分类站点地图类
class CategorySiteMap(Sitemap):
# szy设置分类更新频率和优先级
changefreq = "Weekly"
priority = "0.6"
# szy获取所有分类
def items(self):
return Category.objects.all()
# szy获取分类的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy标签站点地图类
class TagSiteMap(Sitemap):
# szy设置标签更新频率和优先级
changefreq = "Weekly"
priority = "0.3"
# szy获取所有标签
def items(self):
return Tag.objects.all()
# szy获取标签的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy用户站点地图类
class UserSiteMap(Sitemap):
# szy设置用户更新频率和优先级
changefreq = "Weekly"
priority = "0.3"
# szy获取所有有文章的作者去重
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
# szy获取用户的注册时间
def lastmod(self, obj):
return obj.date_joined

@ -6,21 +6,16 @@ from django.conf import settings
logger = logging.getLogger(__name__)
# szy蜘蛛爬虫通知类用于向搜索引擎推送URL
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
# szy将URL列表转换为换行分隔的字符串
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# szy记录推送结果日志
logger.info(result.text)
except Exception as e:
# szy记录推送异常日志
logger.error(e)
# szy推送单个URL的便捷方法
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -3,18 +3,13 @@ from django.test import TestCase
from djangoblog.utils import *
# szyDjangoBlog测试类用于测试工具函数
class DjangoBlogTest(TestCase):
# szy测试初始化设置
def setUp(self):
pass
# szy测试工具函数功能
def test_utils(self):
# szy测试SHA256加密函数
md5 = get_sha256('test')
self.assertIsNotNone(md5)
# szy测试Markdown解析函数
c = CommonMarkdown.get_markdown('''
# Title1
@ -29,8 +24,6 @@ class DjangoBlogTest(TestCase):
''')
self.assertIsNotNone(c)
# szy测试字典转URL参数字符串函数
d = {
'd': 'key1',
'd2': 'key2'

@ -20,6 +20,8 @@ from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
@ -27,7 +29,6 @@ from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# szy定义站点地图配置
sitemaps = {
'blog': ArticleSiteMap,
@ -37,45 +38,42 @@ sitemaps = {
'static': StaticViewSitemap
}
# szy定义错误处理视图
# szy自定义错误页面,保证异常提示保持博客风格
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
handler403 = 'blog.views.permission_denied_view'
def health_check(request):
"""
健康检查接口
简单返回服务健康状态
"""
return JsonResponse({
'status': 'healthy',
'timestamp': time.time()
})
# szy基础URL模式配置
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('health/', health_check, name='health_check'),
]
# szy国际化URL模式配置
urlpatterns += i18n_patterns(
# szy管理员后台URL
re_path(r'^admin/', admin_site.urls),
# szy博客应用URL
re_path(r'', include('blog.urls', namespace='blog')),
# szyMarkdown编辑器URL
re_path(r'mdeditor/', include('mdeditor.urls')),
# szy评论系统URL
re_path(r'', include('comments.urls', namespace='comment')),
# szy账户管理URL
re_path(r'', include('accounts.urls', namespace='account')),
# szyOAuth认证URL
re_path(r'', include('oauth.urls', namespace='oauth')),
# szy站点地图URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# szyFeed订阅URL
re_path(r'^feed/$', DjangoBlogFeed()),
# szyRSS订阅URL
re_path(r'^rss/$', DjangoBlogFeed()),
# szy搜索功能URL使用ElasticSearch视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# szy服务器管理URL
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# szy位置追踪URL
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# szy调试模式下添加媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -19,18 +19,18 @@ from django.templatetags.static import static
logger = logging.getLogger(__name__)
# szy获取最大文章ID和评论ID
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
# szy计算字符串的SHA256哈希值
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
# szy缓存装饰器用于函数结果缓存
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
@ -67,7 +67,6 @@ def cache_decorator(expiration=3 * 60):
return wrapper
# szy刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
@ -92,15 +91,14 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
return True
return False
# szy获取当前站点信息带缓存
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
# szy通用Markdown处理类
class CommonMarkdown:
# szy转换Markdown文本为HTML
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
@ -115,21 +113,17 @@ class CommonMarkdown:
toc = md.toc
return body, toc
# szy获取带目录的Markdown内容
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
# szy获取Markdown内容
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
# szy发送邮件函数
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
@ -138,13 +132,12 @@ def send_email(emailto, title, content):
title=title,
content=content)
# szy生成6位随机数字验证码
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
# szy将字典转换为URL参数字符串
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
@ -152,7 +145,6 @@ def parse_dict_to_url(dict):
return url
# szy获取博客设置
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
@ -181,7 +173,6 @@ def get_blog_setting():
return value
# szy保存用户头像到本地
def save_user_avatar(url):
'''
保存用户头像
@ -191,26 +182,25 @@ def save_user_avatar(url):
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
basedir = settings.AVATAR_ROOT
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
os.makedirs(basedir, exist_ok=True)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
avatar_path = os.path.join(basedir, save_filename)
with open(avatar_path, 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
return f'{settings.AVATAR_URL}{save_filename}'
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
# szy删除侧边栏缓存
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
@ -219,14 +209,12 @@ def delete_sidebar_cache():
cache.delete(k)
# szy删除视图缓存
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
# szy获取资源URL
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
@ -236,9 +224,49 @@ def get_resource_url():
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
'h2', 'p', 'span', 'div']
# 安全的class值白名单 - 只允许代码高亮相关的class
ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""自定义class属性过滤器"""
if name == 'class':
# 只允许预定义的安全class值
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False
return value
# 安全的属性白名单
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'span': class_filter,
'div': class_filter,
'pre': class_filter,
'code': class_filter
}
# 安全的协议白名单 - 防止javascript:等危险协议
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
# szy清洗HTML内容移除不安全的标签和属性
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
"""
安全的HTML清理函数
使用bleach库进行白名单过滤防止XSS攻击
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
strip=True, # 移除不允许的标签而不是转义
strip_comments=True # 移除HTML注释
)

@ -40,7 +40,6 @@ except ImportError:
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
# szy处理最低版本要求
# Handle minimum requirement.
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
@ -48,13 +47,12 @@ if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
# Bubble up the correct error.
# szy日期时间正则表达式用于解析日期字符串
DATETIME_REGEX = re.compile(
'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
LOCALS = threading.local()
LOCALS.RAM_STORE = None
# szy自定义Whoosh HTML格式化器用于保持跨后端一致的高亮结果
class WhooshHtmlFormatter(HtmlFormatter):
"""
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
@ -64,7 +62,6 @@ class WhooshHtmlFormatter(HtmlFormatter):
template = '<%(tag)s>%(t)s</%(tag)s>'
# szyWhoosh搜索后端主类
class WhooshSearchBackend(BaseSearchBackend):
# Word reserved by Whoosh for special use.
RESERVED_WORDS = (
@ -82,7 +79,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'[', ']', '^', '"', '~', '*', '?', ':', '.',
)
# szy初始化Whoosh后端
def __init__(self, connection_alias, **connection_options):
super(
WhooshSearchBackend,
@ -107,7 +103,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.log = logging.getLogger('haystack')
# szy设置Whoosh索引和配置
def setup(self):
"""
Defers loading until needed.
@ -115,7 +110,6 @@ class WhooshSearchBackend(BaseSearchBackend):
from haystack import connections
new_index = False
# szy确保索引目录存在如果不存在则创建
# Make sure the index is there.
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
@ -126,7 +120,6 @@ class WhooshSearchBackend(BaseSearchBackend):
"The path to your Whoosh index '%s' is not writable for the current user/group." %
self.path)
# szy根据配置选择文件存储或内存存储
if self.use_file_storage:
self.storage = FileStorage(self.path)
else:
@ -141,7 +134,6 @@ class WhooshSearchBackend(BaseSearchBackend):
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema)
# szy创建或打开索引
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
@ -152,7 +144,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True
# szy构建Whoosh schema定义字段类型
def build_schema(self, fields):
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True),
@ -208,7 +199,6 @@ class WhooshSearchBackend(BaseSearchBackend):
return (content_field_name, Schema(**schema_fields))
# szy更新索引
def update(self, index, iterable, commit=True):
if not self.setup_complete:
self.setup()
@ -216,7 +206,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
writer = AsyncWriter(self.index)
# szy遍历对象并更新索引
for obj in iterable:
try:
doc = index.full_prepare(obj)
@ -255,7 +244,6 @@ class WhooshSearchBackend(BaseSearchBackend):
# otherwise.
writer.commit()
# szy从索引中移除对象
def remove(self, obj_or_string, commit=True):
if not self.setup_complete:
self.setup()
@ -278,7 +266,6 @@ class WhooshSearchBackend(BaseSearchBackend):
e,
exc_info=True)
# szy清空索引
def clear(self, models=None, commit=True):
if not self.setup_complete:
self.setup()
@ -316,8 +303,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
# szy删除整个索引
def delete_index(self):
# Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files.
@ -326,11 +311,9 @@ class WhooshSearchBackend(BaseSearchBackend):
elif not self.use_file_storage:
self.storage.clean()
# szy重新创建所有内容
# Recreate everything.
self.setup()
# szy优化索引
def optimize(self):
if not self.setup_complete:
self.setup()
@ -338,14 +321,12 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
self.index.optimize()
# szy计算分页信息
def calculate_page(self, start_offset=0, end_offset=None):
# Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0.
if end_offset is not None and end_offset <= 0:
end_offset = 1
# szy确定页码
# Determine the page.
page_num = 0
@ -364,8 +345,6 @@ class WhooshSearchBackend(BaseSearchBackend):
page_num += 1
return page_num, page_length
# szy执行搜索查询
@log_query
def search(
self,
@ -409,8 +388,6 @@ class WhooshSearchBackend(BaseSearchBackend):
reverse = False
# szy处理排序
if sort_by is not None:
# Determine if we need to reverse the results and if Whoosh can
# handle what it's being asked to sort by. Reversing is an
@ -583,8 +560,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy实现"更多类似此结果"功能
def more_like_this(
self,
model_instance,
@ -700,8 +675,6 @@ class WhooshSearchBackend(BaseSearchBackend):
return results
# szy处理原始搜索结果转换为Haystack格式
def _process_results(
self,
raw_page,
@ -794,8 +767,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy创建拼写建议
def create_spelling_suggestion(self, query_string):
spelling_suggestion = None
reader = self.index.reader()
@ -848,8 +819,6 @@ class WhooshSearchBackend(BaseSearchBackend):
value = force_str(value)
return value
# szy将Whoosh值转换为Python原生值
def _to_python(self, value):
"""
Converts values from Whoosh to native Python values.
@ -901,7 +870,6 @@ class WhooshSearchBackend(BaseSearchBackend):
return value
# szyWhoosh搜索查询类
class WhooshSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
@ -909,8 +877,6 @@ class WhooshSearchQuery(BaseSearchQuery):
else:
return force_str(date.strftime('%Y%m%d000000'))
# szy清理查询片段处理保留字和特殊字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -936,8 +902,6 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
# szy构建查询片段
def build_query_fragment(self, field, filter_type, value):
from haystack import connections
query_frag = ''
@ -1075,7 +1039,6 @@ class WhooshSearchQuery(BaseSearchQuery):
# value = self.backend._from_python(value)
# szyWhoosh搜索引擎类
class WhooshEngine(BaseEngine):
backend = WhooshSearchBackend
query = WhooshSearchQuery

@ -11,8 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
# szy设置Django的默认设置模块环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# szy获取WSGI应用实例用于服务器部署
application = get_wsgi_application()

@ -0,0 +1,183 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
def author_name(self):
return get_user_model().objects.first().nickname
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
return item.get_absolute_url()
def item_guid(self, item):
return

@ -0,0 +1,91 @@
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
list_filter = [
'content_type'
]
search_fields = [
'object_repr',
'change_message'
]
list_display_links = [
'action_time',
'get_change_message',
]
list_display = [
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
pass
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -1,9 +1,26 @@
# 导入Django管理后台模块 #zqx: 引入Django的admin模块用于注册和管理模型
from django.contrib import admin
from .models import OwnTrackLog
# 注册你的模型到管理后台(待实现) #zqx: 这是一个占位注释,提示需要注册模型到管理后台
# 定义OwnTrackLogs模型在Django管理后台中的配置类 #zqx: 创建OwnTrackLogsAdmin类继承自ModelAdmin用于配置OwnTrackLog模型在管理后台的行为
class OwnTrackLogsAdmin(admin.ModelAdmin):
# 目前为空,后续可以添加管理后台的自定义配置 #zqx: 当前类体为空,预留空间用于添加管理后台的自定义配置选项
pass
@admin.register(OwnTrackLog)
class OwnTrackLogAdmin(admin.ModelAdmin):
"""
OwnTrackLog模型管理配置
"""
list_display = ('tid', 'lat', 'lon', 'creation_time', 'accuracy')
list_filter = ('tid', 'creation_time')
search_fields = ('tid',)
date_hierarchy = 'creation_time'
readonly_fields = ('creation_time',)
fieldsets = (
('基本信息', {
'fields': ('tid', 'creation_time')
}),
('位置信息', {
'fields': ('lat', 'lon', 'accuracy', 'battery')
}),
)
def get_queryset(self, request):
"""优化查询,减少数据库访问"""
return super().get_queryset(request).select_related()

@ -1,32 +1,70 @@
# 导入Django数据库模型模块 #zqx: 引入Django的models模块用于定义数据库模型
from django.db import models
# 从Django时区工具中导入now函数用于获取当前时间 #zqx: 从django.utils.timezone导入now函数用于设置默认时间值
from django.utils.timezone import now
from django.core.validators import MinValueValidator, MaxValueValidator
# Create your models here. #zqx: Django模型定义的标准注释标记模型定义区域开始
# 定义OwnTrackLog数据模型继承自Django的Model基类 #zqx: 定义OwnTrackLog类继承自models.Model创建一个数据库模型
class OwnTrackLog(models.Model):
# 用户标识字段字符类型最大长度100不允许为空 #zqx: 定义tid字段类型为CharField最大长度100null=False表示不允许为空verbose_name设置字段显示名称
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
# 纬度字段,浮点数类型 #zqx: 定义lat字段类型为FloatFieldverbose_name设置字段显示名称
lat = models.FloatField(verbose_name='纬度')
# 经度字段,浮点数类型 #zqx: 定义lon字段类型为FloatFieldverbose_name设置字段显示名称
lon = models.FloatField(verbose_name='经度')
# 创建时间字段,日期时间类型,默认值为当前时间 #zqx: 定义creation_time字段类型为DateTimeField第一个参数是字段名default设置默认值为now函数
creation_time = models.DateTimeField('创建时间', default=now)
"""
OwnTracks位置数据模型
用于存储移动设备上报的GPS位置信息
"""
# 添加更严格的字段验证
tid = models.CharField(
max_length=100,
null=False,
verbose_name='用户ID',
help_text='设备或用户唯一标识'
)
lat = models.FloatField(
verbose_name='纬度',
validators=[
MinValueValidator(-90.0),
MaxValueValidator(90.0)
],
help_text='纬度坐标,范围-90到90'
)
lon = models.FloatField(
verbose_name='经度',
validators=[
MinValueValidator(-180.0),
MaxValueValidator(180.0)
],
help_text='经度坐标,范围-180到180'
)
creation_time = models.DateTimeField(
'创建时间',
default=now,
db_index=True, # zqx 添加索引提升查询性能
help_text='数据创建时间'
)
# zqx: 添加额外有用字段
accuracy = models.FloatField(
'精度',
null=True,
blank=True,
help_text='GPS定位精度(米)'
)
battery = models.FloatField(
'电量',
null=True,
blank=True,
help_text='设备电量百分比'
)
# 定义对象的字符串表示方法返回用户的tid #zqx: 定义__str__方法返回对象的tid属性用于在管理后台等地方显示对象信息
def __str__(self):
return self.tid
return f"用户{self.tid}{self.creation_time.strftime('%Y-%m-%d %H:%M')}的位置"
def get_coordinates(self):
"""获取坐标元组"""
return (self.lat, self.lon)
# 定义模型的元数据选项 #zqx: 定义Meta内部类用于配置模型的元数据选项
class Meta:
# 设置查询结果的默认排序方式,按创建时间升序排列 #zqx: 设置ordering属性指定查询结果按creation_time字段升序排列
ordering = ['creation_time']
# 设置模型在管理后台显示的单数名称 #zqx: 设置verbose_name属性指定模型在管理后台的单数显示名称
verbose_name = "OwnTrackLogs"
# 设置模型在管理后台显示的复数名称,这里与单数名称相同 #zqx: 设置verbose_name_plural属性指定模型在管理后台的复数显示名称这里与单数名称相同
verbose_name_plural = verbose_name
# 设置获取最新记录时依据的字段 #zqx: 设置get_latest_by属性指定获取最新记录时使用的字段为creation_time
verbose_name = "位置记录"
verbose_name_plural = "位置记录"
get_latest_by = 'creation_time'
indexes = [
models.Index(fields=['tid', 'creation_time']),
models.Index(fields=['creation_time']),
]

@ -0,0 +1,36 @@
# 高德地图API配置
AMAP_API_KEY = os.getenv('AMAP_API_KEY', 'your-default-key-here')
# 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
#management / commands / cleanup_old_locations.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from owntracks.models import OwnTrackLog
class Command(BaseCommand):
help = '清理过期的位置记录'
def add_arguments(self, parser):
parser.add_argument(
'--days',
type=int,
default=365,
help='保留多少天内的数据'
)
def handle(self, *args, **options):
cutoff_date = timezone.now() - timezone.timedelta(days=options['days'])
deleted_count, _ = OwnTrackLog.objects.filter(
creation_time__lt=cutoff_date
).delete()
self.stdout.write(
self.style.SUCCESS(f'成功删除 {deleted_count} 条过期记录')
)

@ -1,83 +1,101 @@
# 导入json模块用于处理JSON数据 #zqx: 引入json模块用于处理JSON格式数据的编码和解码
import json
# 从Django测试模块导入测试客户端、请求工厂和测试用例基类 #zqx: 从django.test导入Client(测试客户端)、RequestFactory(请求工厂)和TestCase(测试用例基类)
from django.test import Client, RequestFactory, TestCase
# 从accounts应用导入BlogUser模型 #zqx: 从accounts应用的models模块导入BlogUser用户模型
from accounts.models import BlogUser
# 从当前应用导入OwnTrackLog模型 #zqx: 从当前应用(.)的models模块导入OwnTrackLog模型
from datetime import datetime
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from .models import OwnTrackLog
# Create your tests here. #zqx: Django测试文件的标准注释标记测试代码区域开始
User = get_user_model()
# 定义OwnTrackLogTest测试类继承自Django的TestCase #zqx: 定义OwnTrackLogTest测试类继承Django的TestCase类用于测试OwnTrackLog相关功能
class OwnTrackLogTest(TestCase):
# 测试初始化方法,在每个测试方法执行前运行 #zqx: setUp方法在每个测试方法执行前自动调用用于初始化测试环境
"""OwnTrackLog模型和视图测试"""
def setUp(self):
# 创建测试客户端实例 #zqx: 创建Client实例用于模拟HTTP请求
self.client = Client()
# 创建请求工厂实例 #zqx: 创建RequestFactory实例用于创建测试请求对象
self.factory = RequestFactory()
# 测试owntracks功能的主要测试方法 #zqx: 定义test_own_track_log测试方法用于测试owntracks功能
def test_own_track_log(self):
# 创建包含完整位置信息的测试数据 #zqx: 创建包含tid、lat、lon字段的字典对象作为完整位置信息测试数据
o = {
'tid': 12, #zqx: 用户ID字段值为12
'lat': 123.123, #zqx: 纬度字段值为123.123
'lon': 134.341 #zqx: 经度字段值为134.341
}
self.superuser = User.objects.create_superuser(
email="admin@example.com",
username="admin",
password="testpassword123"
)
self.normal_user = User.objects.create_user(
email="user@example.com",
username="user",
password="testpassword123"
)
# 使用客户端发送POST请求将位置数据以JSON格式发送到/logtracks端点 #zqx: 使用client.post方法向/owntracks/logtracks路径发送POST请求数据为JSON格式
self.client.post(
'/owntracks/logtracks', #zqx: 请求的目标URL路径
json.dumps(o), #zqx: 将字典o转换为JSON字符串
content_type='application/json') #zqx: 设置请求的内容类型为application/json
# 检查数据库中OwnTrackLog记录数量是否为1 #zqx: 查询OwnTrackLog模型的所有记录检查记录数量是否为1
length = len(OwnTrackLog.objects.all()) #zqx: 获取OwnTrackLog所有对象的数量
self.assertEqual(length, 1) #zqx: 断言记录数量等于1
# 创建不完整的位置数据(缺少经度) #zqx: 创建缺少lon字段的字典对象作为不完整位置信息测试数据
o = {
'tid': 12, #zqx: 用户ID字段值为12
'lat': 123.123 #zqx: 纬度字段值为123.123
def test_create_valid_location(self):
"""测试创建有效位置记录"""
data = {
'tid': 'test-user-1',
'lat': 39.9042,
'lon': 116.4074,
'acc': 10.5,
'batt': 85.0
}
# 再次发送POST请求 #zqx: 使用client.post方法再次发送POST请求数据为不完整的JSON格式
self.client.post(
'/owntracks/logtracks', #zqx: 请求的目标URL路径
json.dumps(o), #zqx: 将不完整的字典o转换为JSON字符串
content_type='application/json') #zqx: 设置请求的内容类型为application/json
# 检查数据库记录数量是否仍为1不完整数据应该不被保存 #zqx: 查询OwnTrackLog模型的所有记录检查记录数量是否仍为1
length = len(OwnTrackLog.objects.all()) #zqx: 获取OwnTrackLog所有对象的数量
self.assertEqual(length, 1) #zqx: 断言记录数量仍等于1验证不完整数据未被保存
# 测试未登录用户访问/show_maps端点应该返回302重定向 #zqx: 测试未登录用户访问/show_maps端点的行为
rsp = self.client.get('/owntracks/show_maps') #zqx: 使用client.get方法向/owntracks/show_maps路径发送GET请求
self.assertEqual(rsp.status_code, 302) #zqx: 断言响应状态码为302表示重定向
# 创建超级用户用于测试 #zqx: 使用create_superuser方法创建超级用户用于后续测试
user = BlogUser.objects.create_superuser( #zqx: 调用BlogUser模型的create_superuser方法
email="liangliangyy1@gmail.com", #zqx: 设置用户邮箱
username="liangliangyy1", #zqx: 设置用户名
password="liangliangyy1") #zqx: 设置用户密码
# 使用创建的用户登录 #zqx: 使用client.login方法以创建的用户身份登录
self.client.login(username='liangliangyy1', password='liangliangyy1') #zqx: 使用用户名和密码登录
# 手动创建并保存一个OwnTrackLog实例 #zqx: 手动创建OwnTrackLog对象并保存到数据库
s = OwnTrackLog() #zqx: 创建OwnTrackLog实例
s.tid = 12 #zqx: 设置tid属性为12
s.lon = 123.234 #zqx: 设置lon属性为123.234
s.lat = 34.234 #zqx: 设置lat属性为34.234
s.save() #zqx: 保存对象到数据库
# 测试已登录用户访问各个端点都应该返回200成功状态码 #zqx: 测试已登录用户访问不同端点的响应状态
rsp = self.client.get('/owntracks/show_dates') #zqx: 向/owntracks/show_dates路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/show_maps') #zqx: 向/owntracks/show_maps路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/get_datas') #zqx: 向/owntracks/get_datas路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') #zqx: 向带日期参数的/owntracks/get_datas路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
response = self.client.post(
'/owntracks/logtracks',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 201)
self.assertEqual(OwnTrackLog.objects.count(), 1)
location = OwnTrackLog.objects.first()
self.assertEqual(location.tid, 'test-user-1')
self.assertEqual(location.lat, 39.9042)
def test_create_invalid_location(self):
"""测试创建无效位置记录"""
# zqx 测试缺少必需字段
data = {'tid': 'test-user'}
response = self.client.post(
'/owntracks/logtracks',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
# zqx: 测试无效坐标
data = {'tid': 'test', 'lat': 1000, 'lon': 2000}
response = self.client.post(
'/owntracks/logtracks',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
def test_permission_access(self):
"""测试权限访问控制"""
# zqx: 未登录用户访问受限页面
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 302) # 重定向到登录
# 普通用户登录
self.client.login(username='user', password='testpassword123')
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 403) # 禁止访问
# zqx: 超级用户登录
self.client.login(username='admin', password='testpassword123')
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 200)
def test_get_location_data(self):
"""测试获取位置数据"""
# zqx: 创建测试数据
OwnTrackLog.objects.create(
tid='user1', lat=39.9, lon=116.4
)
OwnTrackLog.objects.create(
tid='user1', lat=39.91, lon=116.41
)
self.client.login(username='admin', password='testpassword123')
response = self.client.get('/owntracks/get_datas')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(len(data[0]['path']), 2)

@ -1,22 +1,29 @@
# 从Django URL模块导入path函数用于定义URL模式 #zqx: 从django.urls模块导入path函数用于定义URL路由模式
from django.urls import path
# 从当前应用导入视图模块 #zqx: 从当前目录(.)导入views模块包含处理请求的视图函数
from . import views
# 定义应用命名空间为"owntracks" #zqx: 设置app_name变量为"owntracks",定义该应用的命名空间
app_name = "owntracks"
# 定义URL模式列表 #zqx: 定义urlpatterns列表包含该应用的所有URL路由模式
urlpatterns = [
# 定义日志跟踪接口URL将请求路由到manage_owntrack_log视图函数 #zqx: 使用path函数定义URL模式将'owntracks/logtracks'路径映射到views.manage_owntrack_log函数命名为'logtracks'
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
# 定义地图展示页面URL将请求路由到show_maps视图函数 #zqx: 使用path函数定义URL模式将'owntracks/show_maps'路径映射到views.show_maps函数命名为'show_maps'
path('owntracks/show_maps', views.show_maps, name='show_maps'),
# 定义数据获取接口URL将请求路由到get_datas视图函数 #zqx: 使用path函数定义URL模式将'owntracks/get_datas'路径映射到views.get_datas函数命名为'get_datas'
path('owntracks/get_datas', views.get_datas, name='get_datas'),
# 定义日期展示页面URL将请求路由到show_log_dates视图函数 #zqx: 使用path函数定义URL模式将'owntracks/show_dates'路径映射到views.show_log_dates函数命名为'show_dates'
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
path('logtracks',
views.manage_owntrack_log,
name='logtracks'),
path('show_maps',
views.show_maps,
name='show_maps'),
path('get_datas',
views.get_datas,
name='get_datas'),
path('show_dates',
views.show_log_dates,
name='show_dates')
]
#zqx: 应该添加API版本控制
# urlpatterns = [
# path('api/v1/tracks', views.manage_owntrack_log, name='log-tracks'),
# path('api/v1/tracks/dates', views.show_log_dates, name='track-dates'),
# path('api/v1/tracks/<str:date>', views.get_datas, name='track-data'),
# ]

@ -1,161 +1,236 @@
# Create your views here. #zqx: Django视图文件标准注释标记视图代码开始
# 导入所需的Python标准库和第三方库 #zqx: 导入项目需要的各种标准库和第三方库
import datetime #zqx: 导入datetime模块用于处理日期时间相关操作
import itertools #zqx: 导入itertools模块用于高效的循环迭代操作
import json #zqx: 导入json模块用于处理JSON数据格式
import logging #zqx: 导入logging模块用于记录日志信息
from datetime import timezone #zqx: 从datetime模块导入timezone用于处理时区相关操作
from itertools import groupby #zqx: 从itertools模块导入groupby用于对数据进行分组操作
import django #zqx: 导入django模块
import requests #zqx: 导入requests库用于发送HTTP请求
# 导入Django的装饰器、HTTP响应类和视图相关模块 #zqx: 导入Django框架的各种视图相关组件
from django.contrib.auth.decorators import login_required #zqx: 从django.contrib.auth.decorators导入login_required装饰器用于限制视图只能由登录用户访问
from django.http import HttpResponse #zqx: 从django.http导入HttpResponse用于返回HTTP响应
from django.http import JsonResponse #zqx: 从django.http导入JsonResponse用于返回JSON格式的HTTP响应
from django.shortcuts import render #zqx: 从django.shortcuts导入render函数用于渲染模板
from django.views.decorators.csrf import csrf_exempt #zqx: 从django.views.decorators导入csrf_exempt装饰器用于免除CSRF验证
# 导入当前应用的OwnTrackLog模型 #zqx: 从当前应用的models模块导入OwnTrackLog数据模型
import datetime
import json
import logging
from typing import Dict, Any, List
from datetime import timezone
import requests
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.db import transaction
from django.core.cache import cache
from django.conf import settings
from .models import OwnTrackLog
# 获取日志记录器实例 #zqx: 获取名为__name__的日志记录器实例
logger = logging.getLogger(__name__)
# 装饰器免除CSRF验证用于接收外部系统POST请求 #zqx: 使用@csrf_exempt装饰器免除该视图函数的CSRF验证允许外部系统POST请求
def is_superuser(user):
"""检查用户是否为超级用户"""
return user.is_superuser
@csrf_exempt
def manage_owntrack_log(request): #zqx: 定义manage_owntrack_log视图函数接收request参数
try: #zqx: 开始异常处理块
# 解析请求体中的JSON数据 #zqx: 解析HTTP请求体中的JSON数据
s = json.loads(request.read().decode('utf-8')) #zqx: 读取请求体内容并解码为utf-8然后解析为JSON对象
tid = s['tid'] #zqx: 从JSON对象中获取tid字段值用户标识
lat = s['lat'] #zqx: 从JSON对象中获取lat字段值纬度
lon = s['lon'] #zqx: 从JSON对象中获取lon字段值经度
# 记录接收到的位置信息日志 #zqx: 记录接收到的位置信息到日志
logger.info( #zqx: 使用logger记录info级别的日志信息
'tid:{tid}.lat:{lat}.lon:{lon}'.format( #zqx: 格式化日志信息字符串
tid=tid, lat=lat, lon=lon)) #zqx: 填充格式化参数
# 验证必要字段是否存在 #zqx: 验证必需的字段是否存在且不为空
if tid and lat and lon: #zqx: 判断tid、lat、lon三个字段是否都存在且不为空
# 创建并保存位置记录 #zqx: 创建OwnTrackLog实例并保存位置记录
m = OwnTrackLog() #zqx: 创建OwnTrackLog模型实例
m.tid = tid #zqx: 设置实例的tid属性
m.lat = lat #zqx: 设置实例的lat属性
m.lon = lon #zqx: 设置实例的lon属性
m.save() #zqx: 保存实例到数据库
return HttpResponse('ok') #zqx: 返回'ok'字符串响应
else: #zqx: 如果必要字段不完整
return HttpResponse('data error') #zqx: 返回'data error'字符串响应
except Exception as e: #zqx: 捕获所有异常
# 记录错误日志并返回错误响应 #zqx: 记录错误日志并返回错误响应
logger.error(e) #zqx: 使用logger记录error级别的异常信息
return HttpResponse('error') #zqx: 返回'error'字符串响应
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
@login_required
def show_maps(request): #zqx: 定义show_maps视图函数接收request参数
# 检查用户是否为超级用户 #zqx: 检查当前登录用户是否为超级用户
if request.user.is_superuser: #zqx: 判断请求用户是否为超级用户
# 设置默认日期为当前UTC日期 #zqx: 设置默认日期为当前UTC日期
defaultdate = str(datetime.datetime.now(timezone.utc).date()) #zqx: 获取当前UTC时间的日期部分并转换为字符串
# 从GET参数获取日期如果没有则使用默认日期 #zqx: 从请求GET参数中获取date参数如果没有则使用默认日期
date = request.GET.get('date', defaultdate) #zqx: 获取GET参数中的date值不存在时使用defaultdate
# 构造上下文数据 #zqx: 构造传递给模板的上下文数据
context = { #zqx: 定义context字典
'date': date #zqx: 将date变量添加到context字典中
}
# 渲染地图展示页面 #zqx: 渲染show_maps.html模板并返回响应
return render(request, 'owntracks/show_maps.html', context) #zqx: 使用render函数渲染模板并返回响应
else: #zqx: 如果用户不是超级用户
# 非超级用户返回403禁止访问 #zqx: 为非超级用户返回403禁止访问响应
from django.http import HttpResponseForbidden #zqx: 从django.http导入HttpResponseForbidden
return HttpResponseForbidden() #zqx: 返回403禁止访问响应
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
@require_http_methods(["POST"])
@transaction.atomic
def manage_owntrack_log(request: HttpRequest) -> HttpResponse:
"""
处理OwnTracks位置数据上报
"""
try:
# 验证内容类型
if request.content_type != 'application/json':
return HttpResponse('Unsupported media type', status=415)
# zqx: 解析请求数据
try:
raw_data = request.body.decode('utf-8')
data = json.loads(raw_data)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
logger.warning(f"JSON解析失败: {e}")
return HttpResponse('Invalid JSON', status=400)
# zqx: 验证必需字段
required_fields = ['tid', 'lat', 'lon']
if not all(field in data for field in required_fields):
missing = [field for field in required_fields if field not in data]
logger.warning(f"缺少必需字段: {missing}")
return HttpResponse(f'Missing required fields: {missing}', status=400)
# zqx: 验证数据类型
try:
tid = str(data['tid'])
lat = float(data['lat'])
lon = float(data['lon'])
except (ValueError, TypeError) as e:
logger.warning(f"数据类型错误: {e}")
return HttpResponse('Invalid data types', status=400)
# zqx: 验证坐标范围
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
logger.warning(f"坐标超出范围: lat={lat}, lon={lon}")
return HttpResponse('Coordinates out of range', status=400)
# zqx: 创建位置记录
track_log = OwnTrackLog(
tid=tid,
lat=lat,
lon=lon,
accuracy=data.get('acc'),
battery=data.get('batt')
)
track_log.full_clean() # 模型验证
track_log.save()
logger.info(f"位置记录创建成功: 用户{tid}在({lat}, {lon})")
return HttpResponse('OK', status=201)
except Exception as e:
logger.error(f"位置记录处理异常: {e}", exc_info=True)
return HttpResponse('Server error', status=500)
@login_required
def show_log_dates(request): #zqx: 定义show_log_dates视图函数接收request参数
# 从数据库获取所有记录的创建时间 #zqx: 从数据库中查询OwnTrackLog模型的所有creation_time字段值
dates = OwnTrackLog.objects.values_list('creation_time', flat=True) #zqx: 使用values_list获取creation_time字段值flat=True返回扁平化结果
# 提取日期部分并去重排序 #zqx: 提取日期部分,去重并排序
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) #zqx: 使用map提取日期格式化字符串set去重sorted排序list转换为列表
# 构造上下文数据 #zqx: 构造传递给模板的上下文数据
context = { #zqx: 定义context字典
'results': results #zqx: 将results变量添加到context字典中
@user_passes_test(is_superuser)
def show_maps(request: HttpRequest) -> HttpResponse:
# zqx:
显示位置地图页面
today = datetime.datetime.now(timezone.utc).date()
date_str = request.GET.get('date', str(today))
# zqx: 验证日期格式
try:
datetime.datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
date_str = str(today)
context = {
'date': date_str,
'map_api_key': getattr(settings, 'AMAP_API_KEY', '')
}
# 渲染日期展示页面 #zqx: 渲染show_log_dates.html模板并返回响应
return render(request, 'owntracks/show_log_dates.html', context) #zqx: 使用render函数渲染模板并返回响应
# 将GPS坐标转换为高德地图坐标批量处理每次30个 #zqx: 定义convert_to_amap函数用于将GPS坐标批量转换为高德地图坐标每次处理30个点
def convert_to_amap(locations): #zqx: 定义convert_to_amap函数接收locations参数位置列表
convert_result = [] #zqx: 初始化转换结果列表
# 创建迭代器 #zqx: 创建locations列表的迭代器
it = iter(locations) #zqx: 使用iter函数创建locations的迭代器
# 每次取30个位置点进行处理 #zqx: 每次从迭代器中取出30个位置点进行处理
item = list(itertools.islice(it, 30)) #zqx: 使用itertools.islice从迭代器中取出前30个元素
while item: #zqx: 当item列表不为空时循环处理
# 将经纬度格式化为高德API需要的格式 #zqx: 将经纬度数据格式化为高德API所需的格式
datas = ';'.join( #zqx: 使用';'连接符连接所有坐标字符串
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) #zqx: 使用map提取每个位置的经度和纬度并格式化set去重join连接
# 高德地图API配置 #zqx: 配置高德地图坐标转换API的参数
key = '8440a376dfc9743d8924bf0ad141f28e' #zqx: 设置高德地图API的key
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' #zqx: 设置高德地图API的URL
query = { #zqx: 定义API请求参数字典
'key': key, #zqx: API密钥参数
'locations': datas, #zqx: 需要转换的坐标数据
'coordsys': 'gps' #zqx: 源坐标系为GPS
return render(request, 'owntracks/show_maps.html', context)
@login_required
@user_passes_test(is_superuser)
def show_log_dates(request: HttpRequest) -> HttpResponse:
# zqx
显示有位置记录的日期列表
cache_key = 'owntracks_dates_list'
dates = cache.get(cache_key)
if dates is None:
# zqx: 使用数据库的日期函数提高性能
dates = OwnTrackLog.objects.dates('creation_time', 'day', order='DESC')
dates = [date.strftime('%Y-%m-%d') for date in dates]
cache.set(cache_key, dates, timeout=3600) # 缓存1小时
context = {'results': dates}
return render(request, 'owntracks/show_log_dates.html', context)
def convert_coordinates_batch(locations: List[OwnTrackLog]) -> str:
# zqx:
批量转换GPS坐标到高德坐标系
if not locations:
return ""
# zqx: 坐标去重
unique_coords = set()
for location in locations:
coord_key = f"{location.lon:.6f},{location.lat:.6f}"
unique_coords.add(coord_key)
coordinates_list = list(unique_coords)
batch_size = 30 # 高德API批量限制
all_converted = []
for i in range(0, len(coordinates_list), batch_size):
batch = coordinates_list[i:i + batch_size]
locations_str = ';'.join(batch)
# zqx: 调用高德坐标转换API
api_key = getattr(settings, 'AMAP_API_KEY', '8440a376dfc9743d8924bf0ad141f28e')
api_url = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
params = {
'key': api_key,
'locations': locations_str,
'coordsys': 'gps'
}
# 发送请求到高德API #zqx: 向高德地图API发送GET请求
rsp = requests.get(url=api, params=query) #zqx: 使用requests.get发送带参数的GET请求
result = json.loads(rsp.text) #zqx: 解析API响应的JSON数据
# 处理API响应结果 #zqx: 处理API返回的结果
if "locations" in result: #zqx: 判断响应结果中是否包含locations字段
convert_result.append(result['locations']) #zqx: 如果包含则将locations值添加到转换结果列表中
# 继续处理下一批数据 #zqx: 继续处理下一批30个数据
item = list(itertools.islice(it, 30)) #zqx: 从迭代器中继续取出下一批30个元素
# 返回转换后的坐标字符串 #zqx: 返回所有转换后的坐标字符串,用分号连接
return ";".join(convert_result) #zqx: 使用";"连接符连接所有转换结果并返回
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
try:
response = requests.get(api_url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('status') == '1' and 'locations' in result:
all_converted.append(result['locations'])
else:
logger.error(f"高德API错误: {result.get('info', 'Unknown error')}")
# 使用原始坐标作为备选
all_converted.append(locations_str)
except requests.RequestException as e:
logger.error(f"坐标转换API请求失败: {e}")
all_converted.append(locations_str) # 使用原始坐标
return ";".join(all_converted)
@login_required
def get_datas(request): #zqx: 定义get_datas视图函数接收request参数
# 获取当前UTC时间并设置为当天0点 #zqx: 获取当前UTC时间并设置为当天的0点0分0秒
now = django.utils.timezone.now().replace(tzinfo=timezone.utc) #zqx: 获取当前时间并设置时区为UTC
querydate = django.utils.timezone.datetime( #zqx: 创建查询开始日期时间对象
now.year, now.month, now.day, 0, 0, 0) #zqx: 设置为当年当月当日的0时0分0秒
# 如果GET参数中有指定日期则使用指定日期 #zqx: 如果请求GET参数中包含date则使用指定日期
if request.GET.get('date', None): #zqx: 判断GET参数中是否存在date参数
date = list(map(lambda x: int(x), request.GET.get('date').split('-'))) #zqx: 将date参数按'-'分割并转换为整数列表
querydate = django.utils.timezone.datetime( #zqx: 根据指定日期创建查询开始日期时间对象
date[0], date[1], date[2], 0, 0, 0) #zqx: 使用指定年月日创建datetime对象
# 计算查询结束时间第二天0点 #zqx: 计算查询结束时间为查询开始时间的下一天0点
nextdate = querydate + datetime.timedelta(days=1) #zqx: 查询结束时间为开始时间加上1天
# 查询指定日期范围内的位置记录 #zqx: 查询creation_time在指定日期范围内的OwnTrackLog记录
models = OwnTrackLog.objects.filter( #zqx: 使用filter方法筛选记录
creation_time__range=(querydate, nextdate)) #zqx: 筛选creation_time在querydate到nextdate范围内的记录
result = list() #zqx: 初始化结果列表
# 如果查询到数据,则按用户分组处理 #zqx: 如果查询到数据则按用户进行分组处理
if models and len(models): #zqx: 判断models是否存在且不为空
for tid, item in groupby( #zqx: 使用groupby按tid分组遍历models
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): #zqx: 先按tid排序然后按tid分组
d = dict() #zqx: 创建字典对象存储用户轨迹数据
d["name"] = tid #zqx: 设置字典的name字段为用户标识tid
paths = list() #zqx: 初始化路径坐标列表
# 目前使用原始GPS坐标注释掉的代码是使用高德转换坐标的部分 #zqx: 当前使用原始GPS坐标注释掉的是高德坐标转换的代码
# locations = convert_to_amap( #zqx: 调用convert_to_amap函数转换坐标已注释
# sorted(item, key=lambda x: x.creation_time)) #zqx: 按创建时间排序后转换(已注释)
# for i in locations.split(';'): #zqx: 遍历转换后的坐标字符串(已注释)
# paths.append(i.split(',')) #zqx: 将坐标分割后添加到路径列表(已注释)
# 使用GPS原始经纬度按时间排序 #zqx: 使用原始GPS坐标按时间排序
for location in sorted(item, key=lambda x: x.creation_time): #zqx: 遍历分组后的记录并按创建时间排序
paths.append([str(location.lon), str(location.lat)]) #zqx: 将经度和纬度转换为字符串并添加到路径列表
d["path"] = paths #zqx: 设置字典的path字段为路径坐标列表
result.append(d) #zqx: 将用户轨迹数据字典添加到结果列表
# 返回JSON格式的轨迹数据 #zqx: 返回JSON格式的轨迹数据响应
return JsonResponse(result, safe=False) #zqx: 使用JsonResponse返回结果safe=False允许非字典对象
@user_passes_test(is_superuser)
def get_datas(request: HttpRequest) -> JsonResponse:
"""
获取指定日期的位置数据(JSON API)
"""
# zqx: 日期处理
date_str = request.GET.get('date')
if date_str:
try:
query_date = datetime.datetime.strptime(date_str, '%Y-%m-%d').replace(
tzinfo=timezone.utc
)
except ValueError:
return JsonResponse(
{'error': 'Invalid date format. Use YYYY-MM-DD.'},
status=400
)
else:
query_date = datetime.datetime.now(timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
next_date = query_date + datetime.timedelta(days=1)
# zqx: 数据库查询优化
locations = OwnTrackLog.objects.filter(
creation_time__range=(query_date, next_date)
).order_by('tid', 'creation_time')
if not locations.exists():
return JsonResponse([], safe=False)
# zqx: 按用户分组处理数据
result = []
current_tid = None
user_paths = []
for location in locations:
if location.tid != current_tid:
# zqx: 保存前一个用户的数据
if current_tid is not None and user_paths:
result.append({
"name": current_tid,
"path": user_paths.copy()
})
# zqx 开始新用户
current_tid = location.tid
user_paths = []
user_paths.append([str(location.lon), str(location.lat)])
# zqx 添加最后一个用户的数据
if current_tid is not None and user_paths:
result.append({
"name": current_tid,
"path": user_paths
})
return JsonResponse(result, safe=False)

@ -0,0 +1,194 @@
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
PLUGIN_AUTHOR = None
# 插件配置
SUPPORTED_POSITIONS = [] # 支持的显示位置
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 设置插件路径
self.plugin_dir = self._get_plugin_directory()
self.plugin_slug = self._get_plugin_slug()
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件目录路径"""
import inspect
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件标识符(目录名)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据位置渲染插件组件
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
dict: {'html': 'HTML内容', 'priority': 优先级} None
"""
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查条件显示
if not self.should_display(position, context, **kwargs):
return None
# 调用具体的位置渲染方法
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
html = getattr(self, method_name)(context, **kwargs)
if html:
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示
子类可重写此方法实现条件显示逻辑
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: 是否显示
"""
return True
# === 各位置渲染方法 - 子类重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件模板
Args:
template_name: 模板文件名
context: 模板上下文
Returns:
HTML字符串
"""
if context is None:
context = {}
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
return render_to_string(template_path, context)
except TemplateDoesNotExist:
logger.warning(f"Plugin template not found: {template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""获取插件静态文件URL"""
from django.templatetags.static import static
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""获取插件CSS文件列表"""
return []
def get_js_files(self):
"""获取插件JavaScript文件列表"""
return []
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -0,0 +1,22 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量
POSITION_HOOKS = {
'article_top': 'article_top_widgets',
'article_bottom': 'article_bottom_widgets',
'sidebar': 'sidebar_widgets',
'header': 'header_widgets',
'footer': 'footer_widgets',
'comment_before': 'comment_before_widgets',
'comment_after': 'comment_after_widgets',
}
# 资源注入钩子
HEAD_RESOURCES_HOOK = 'head_resources'
BODY_RESOURCES_HOOK = 'body_resources'

@ -0,0 +1,44 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -0,0 +1,64 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
# 全局插件注册表
_loaded_plugins = []
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
global _loaded_plugins
_loaded_plugins = []
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 导入插件模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 获取插件实例
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""获取所有已加载的插件"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""根据名称获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""根据slug获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""获取所有插件的信息"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""获取支持指定位置的插件"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -0,0 +1,403 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
_ENV_SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Default to DEBUG=True only when no DJANGO_SECRET_KEY is provided, so local development works out of the box.
DEBUG = env_to_bool('DJANGO_DEBUG', _ENV_SECRET_KEY is None)
def get_secret_key():
if _ENV_SECRET_KEY:
return _ENV_SECRET_KEY
if DEBUG:
# Provide a deterministic key for convenience in local development.
return 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required when DEBUG=False')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret_key()
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# Allow configuring host whitelist via environment, fallback to safe defaults
_default_hosts = '127.0.0.1,localhost'
ALLOWED_HOSTS = [host.strip() for host in os.environ.get('DJANGO_ALLOWED_HOSTS', _default_hosts).split(',') if host.strip()]
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '12345678',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
'CONN_MAX_AGE': int(os.environ.get('DJANGO_DB_CONN_MAX_AGE', 60)),
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# szy同时收集项目静态目录与插件资源避免部署缺文件
STATICFILES_DIRS = [
STATICFILES,
os.path.join(BASE_DIR, 'plugins'),
]
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
}
}
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
# 压缩输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
# 压缩文件名模板 - 包含哈希值用于缓存破坏
COMPRESS_CSS_HASHING_METHOD = 'mtime'
COMPRESS_JS_HASHING_METHOD = 'mtime'
# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# CSS压缩器 - 高压缩等级
'compressor.filters.cssmin.CSSCompressorFilter',
]
# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
# JS压缩器 - 高压缩等级
'compressor.filters.jsmin.SlimItFilter',
]
# 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default'
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
# 预压缩配置
COMPRESS_PRECOMPILERS = (
# 支持SCSS/SASS
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
# 压缩等级配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件缓存配置
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# 浏览器缓存配置(通过中间件或服务器配置)
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
AVATAR_ROOT = os.path.join(MEDIA_ROOT, 'avatars')
AVATAR_URL = f'{MEDIA_URL}avatars/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer',
'image_lazy_loading',
'article_recommendation',
]

@ -0,0 +1,59 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
def items(self):
return ['blog:index', ]
def location(self, item):
return reverse(item)
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
def items(self):
return Category.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return Tag.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined

@ -0,0 +1,21 @@
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -0,0 +1,32 @@
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,79 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
# szy自定义错误页面保证异常提示保持博客风格
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handler403 = 'blog.views.permission_denied_view'
def health_check(request):
"""
健康检查接口
简单返回服务健康状态
"""
return JsonResponse({
'status': 'healthy',
'timestamp': time.time()
})
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('health/', health_check, name='health_check'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,272 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
try:
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = settings.AVATAR_ROOT
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
os.makedirs(basedir, exist_ok=True)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
avatar_path = os.path.join(basedir, save_filename)
with open(avatar_path, 'wb+') as file:
file.write(rsp.content)
return f'{settings.AVATAR_URL}{save_filename}'
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p', 'span', 'div']
# 安全的class值白名单 - 只允许代码高亮相关的class
ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""自定义class属性过滤器"""
if name == 'class':
# 只允许预定义的安全class值
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False
return value
# 安全的属性白名单
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'span': class_filter,
'div': class_filter,
'pre': class_filter,
'code': class_filter
}
# 安全的协议白名单 - 防止javascript:等危险协议
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
"""
安全的HTML清理函数
使用bleach库进行白名单过滤防止XSS攻击
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
strip=True, # 移除不允许的标签而不是转义
strip_comments=True # 移除HTML注释
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,16 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()
Loading…
Cancel
Save