You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
djangoBlogStudy/src/djangoblog/utils.py

400 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python
# encoding: utf-8
"""
djangoblog 工具函数模块utils.py
提供博客系统所需的通用工具函数包括缓存、安全、Markdown解析、邮件发送、文件处理等功能。
"""
# 导入标准库和第三方库
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach # 用于清理 HTML 标签,防止 XSS 攻击
import markdown # 用于将 Markdown 文本转换为 HTML
import requests # 用于发起 HTTP 请求
from django.conf import settings
from django.contrib.sites.models import Site # Django 的站点框架
from django.core.cache import cache # 使用 Django 缓存后端
from django.templatetags.static import static # 用于生成静态文件 URL
# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""
获取当前数据库中最新(主键最大)的文章和评论的 ID。
返回:
tuple: (最新文章的主键, 最新评论的主键)
"""
from ..blog.models import Article
from ..comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
#wwc
def get_sha256(str):
"""
计算输入字符串的 SHA256 哈希值。
参数:
str (str): 要哈希的字符串
返回:
str: SHA256 哈希值的十六进制字符串表示
"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
#wwc
def cache_decorator(expiration=3 * 60):
"""
缓存装饰器,用于为函数添加缓存功能,避免重复计算或数据库查询。
参数:
expiration (int): 缓存过期时间(秒),默认 3 分钟
返回:
decorator: 可用于装饰函数的装饰器
工作原理:
1. 尝试从视图对象获取缓存键(通过 get_cache_key 方法)
2. 如果没有,则基于函数名和参数生成唯一哈希作为键
3. 查询缓存,命中则返回缓存值
4. 未命中则执行原函数,将结果存入缓存并返回
5. 特殊处理返回值为 None 的情况,避免缓存穿透
"""
#wwc
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:
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
#wwc
def expire_view_cache(path, servername, serverport, key_prefix=None):
"""
手动清除特定视图的缓存(用于内容更新后刷新缓存)。
参数:
path (str): URL 路径(如 '/article/1/'
servername (str): 服务器域名
serverport (str): 服务器端口
key_prefix (str, optional): 缓存键前缀
返回:
bool: 缓存是否成功删除
实现:
1. 构造一个模拟的 HttpRequest 对象
2. 使用 Django 的 get_cache_key 工具生成与视图缓存对应的键
3. 如果键存在且缓存中有值,则删除该缓存
"""
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
#wwc
@cache_decorator()
def get_current_site():
"""
获取当前 Django 站点对象,并使用缓存优化性能。
返回:
Site: 当前站点实例(包含域名、名称等信息)
"""
site = Site.objects.get_current()
return site
#wwc
class CommonMarkdown:
"""
提供统一的 Markdown 解析功能,支持代码高亮、目录生成等。
"""
#wwc
@staticmethod
def _convert_markdown(value):
"""
内部方法:将 Markdown 字符串转换为 HTML并提取目录TOC
参数:
value (str): Markdown 格式的文本
返回:
tuple: (HTML 内容字符串, 目录 HTML 字符串)
"""
md = markdown.Markdown(
extensions=[
'extra', # 标准扩展(表格、脚注等)
'codehilite', # 代码高亮
'toc', # 自动生成目录
'tables', # 表格支持
]
)
body = md.convert(value)
toc = md.toc
return body, toc
#wwc
@staticmethod
def get_markdown_with_toc(value):
"""
解析 Markdown 文本,同时返回 HTML 内容和目录。
参数:
value (str): Markdown 文本
返回:
tuple: (HTML 内容, TOC 目录)
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
#wwc
@staticmethod
def get_markdown(value):
"""
仅解析 Markdown 文本为 HTML 内容,不返回目录。
参数:
value (str): Markdown 文本
返回:
str: 转换后的 HTML 字符串
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
#wwc
def send_email(emailto, title, content):
"""
发送邮件的快捷方法,通过 Django 信号机制解耦。
参数:
emailto (str): 收件人邮箱
title (str): 邮件标题
content (str): 邮件正文
实现:
触发自定义的 send_email_signal 信号,由信号处理器完成实际的邮件发送逻辑。
"""
from ..djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
#wwc
def generate_code() -> str:
"""
生成一个 6 位的随机数字验证码。
返回:
str: 6 位数字组成的字符串(如 '123456'
"""
return ''.join(random.sample(string.digits, 6))
#wwc
def parse_dict_to_url(dict):
"""
将字典转换为 URL 查询参数字符串(键值对用 & 连接)。
参数:
dict (dict): 要转换的字典
返回:
str: URL 编码后的查询字符串(如 'key1=value1&key2=value2'
注意:
使用 urllib.parse.quote 对键和值进行 URL 编码safe='/' 表示斜杠不编码。
"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
#wwc
def get_blog_setting():
"""
获取博客系统设置,优先从缓存读取,未命中则从数据库获取并缓存。
如果数据库中没有设置记录,则创建一个默认设置并保存。
返回:
BlogSettings: 博客设置模型实例
"""
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
#wwc
def save_user_avatar(url):
"""
从指定 URL 下载用户头像并保存到本地静态文件目录。
参数:
url (str): 头像图片的远程 URL
返回:
str: 保存后的本地静态文件 URL'/static/avatar/abc123.jpg'
下载失败时返回默认头像路径。
流程:
1. 创建本地头像存储目录
2. 下载图片内容
3. 根据原始 URL 判断文件类型,生成唯一文件名
4. 保存文件
5. 返回静态 URL
"""
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
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:
file.write(rsp.content)
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
#wwc
def delete_sidebar_cache():
"""
清除侧边栏所有缓存。
侧边栏内容(如最新文章、热门评论)通常会被缓存以提高性能。
当内容更新时,需调用此函数清除相关缓存。
实现:
遍历 LinkShowType 的所有值,删除以 'sidebar' 为前缀的缓存键。
"""
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)
#wwc
def delete_view_cache(prefix, keys):
"""
删除基于模板片段缓存(@cache的缓存。
参数:
prefix (str): 缓存片段的名称(与模板中 cache 标签的第一个参数对应)
keys (list): 缓存的变量列表(用于生成唯一键)
实现:
使用 make_template_fragment_key 生成正确的缓存键,然后删除。
"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
#wwc
def get_resource_url():
"""
获取静态资源的基础 URL。
如果设置了 STATIC_URL则直接返回。
否则,构建一个完整的 URLhttp://domain/static/)。
返回:
str: 静态资源基础 URL
"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
# 定义允许在用户内容中使用的 HTML 标签和属性
# 用于 sanitize_html 函数,防止 XSS 攻击
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']}
#wwc
def sanitize_html(html):
"""
清理 HTML 内容移除不安全的标签和属性防止跨站脚本XSS攻击。
参数:
html (str): 待清理的 HTML 字符串
返回:
str: 清理后的 HTML 字符串,仅包含 ALLOWED_TAGS 和 ALLOWED_ATTRIBUTES 中定义的内容
"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)