diff --git a/doc/个人.docx b/doc/个人.docx
new file mode 100644
index 0000000..fe719de
Binary files /dev/null and b/doc/个人.docx differ
diff --git a/doc/第五周作业-软件数据模型设计说明书.docx b/doc/第五周作业-软件数据模型设计说明书.docx
new file mode 100644
index 0000000..86dbe16
Binary files /dev/null and b/doc/第五周作业-软件数据模型设计说明书.docx differ
diff --git a/doc/说明文档.md b/doc/说明文档.md
deleted file mode 100644
index e69de29..0000000
diff --git a/src/django-master/blog/admin.py b/src/django-master/blog/admin.py
index 46c3420..c637bfc 100644
--- a/src/django-master/blog/admin.py
+++ b/src/django-master/blog/admin.py
@@ -1,48 +1,76 @@
from django import forms
+#ymq:导入Django的forms模块,用于创建表单
from django.contrib import admin
+#ymq:导入Django的admin模块,用于后台管理配置
from django.contrib.auth import get_user_model
+#ymq:导入获取用户模型的函数,便于灵活引用用户模型
from django.urls import reverse
+#ymq:导入reverse函数,用于生成URL反向解析
from django.utils.html import format_html
+#ymq:导入format_html函数,用于安全生成HTML内容
from django.utils.translation import gettext_lazy as _
+#ymq:导入国际化翻译函数,将文本标记为可翻译
# Register your models here.
from .models import Article
+#ymq:从当前应用的models模块导入Article模型
class ArticleForm(forms.ModelForm):
+ #ymq:定义Article模型对应的表单类,继承自ModelForm
# body = forms.CharField(widget=AdminPagedownWidget())
+ #ymq:注释掉的代码,原本计划为body字段使用AdminPagedownWidget编辑器
class Meta:
+ #ymq:Meta类用于配置表单元数据
model = Article
+ #ymq:指定表单关联的模型为Article
fields = '__all__'
+ #ymq:指定表单包含模型的所有字段
def makr_article_publish(modeladmin, request, queryset):
+ #ymq:定义批量发布文章的动作函数
queryset.update(status='p')
+ #ymq:将选中的文章状态更新为'p'(发布状态)
def draft_article(modeladmin, request, queryset):
+ #ymq:定义批量设为草稿的动作函数
queryset.update(status='d')
+ #ymq:将选中的文章状态更新为'd'(草稿状态)
def close_article_commentstatus(modeladmin, request, queryset):
+ #ymq:定义批量关闭评论的动作函数
queryset.update(comment_status='c')
+ #ymq:将选中的文章评论状态更新为'c'(关闭状态)
def open_article_commentstatus(modeladmin, request, queryset):
+ #ymq:定义批量开启评论的动作函数
queryset.update(comment_status='o')
+ #ymq:将选中的文章评论状态更新为'o'(开启状态)
makr_article_publish.short_description = _('Publish selected articles')
+#ymq:设置发布动作在admin中的显示名称(支持国际化)
draft_article.short_description = _('Draft selected articles')
+#ymq:设置草稿动作在admin中的显示名称(支持国际化)
close_article_commentstatus.short_description = _('Close article comments')
+#ymq:设置关闭评论动作在admin中的显示名称(支持国际化)
open_article_commentstatus.short_description = _('Open article comments')
+#ymq:设置开启评论动作在admin中的显示名称(支持国际化)
class ArticlelAdmin(admin.ModelAdmin):
+ #ymq:定义Article模型的admin管理类,继承自ModelAdmin
list_per_page = 20
+ #ymq:设置每页显示20条记录
search_fields = ('body', 'title')
+ #ymq:设置可搜索的字段为body和title
form = ArticleForm
+ #ymq:指定使用自定义的ArticleForm表单
list_display = (
'id',
'title',
@@ -53,60 +81,93 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
+ #ymq:设置列表页显示的字段
list_display_links = ('id', 'title')
+ #ymq:设置列表页中可点击跳转编辑页的字段
list_filter = ('status', 'type', 'category')
+ #ymq:设置可用于筛选的字段
filter_horizontal = ('tags',)
+ #ymq:设置多对多字段的水平筛选器(tags字段)
exclude = ('creation_time', 'last_modify_time')
+ #ymq:设置编辑页中排除的字段(不显示)
view_on_site = True
+ #ymq:启用"在站点上查看"功能
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
+ #ymq:注册批量操作动作
def link_to_category(self, obj):
+ #ymq:自定义列表页中分类字段的显示方式(转为链接)
info = (obj.category._meta.app_label, obj.category._meta.model_name)
+ #ymq:获取分类模型的应用标签和模型名称
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
+ #ymq:生成分类的编辑页URL
return format_html(u'%s' % (link, obj.category.name))
+ #ymq:返回HTML链接,点击可跳转到分类编辑页
link_to_category.short_description = _('category')
+ #ymq:设置自定义字段在列表页的显示名称(支持国际化)
def get_form(self, request, obj=None, **kwargs):
- form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
+ #ymq:重写获取表单的方法,自定义表单字段
+ form = super(ArticlelAdmin, self).get_form(request, obj,** kwargs)
+ #ymq:调用父类方法获取表单
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
+ #ymq:限制作者字段只能选择超级用户
return form
+ #ymq:返回修改后的表单
def save_model(self, request, obj, form, change):
+ #ymq:重写保存模型的方法(可在此添加自定义保存逻辑)
super(ArticlelAdmin, self).save_model(request, obj, form, change)
+ #ymq:调用父类的保存方法完成默认保存
def get_view_on_site_url(self, obj=None):
+ #ymq:重写"在站点上查看"的URL生成方法
if obj:
+ #ymq:如果有具体对象,返回对象的完整URL
url = obj.get_full_url()
return url
else:
+ #ymq:如果无对象,返回当前站点域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
+ #ymq:定义Tag模型的admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time')
+ #ymq:编辑页排除slug、最后修改时间和创建时间字段
class CategoryAdmin(admin.ModelAdmin):
+ #ymq:定义Category模型的admin管理类
list_display = ('name', 'parent_category', 'index')
+ #ymq:列表页显示名称、父分类和排序索引字段
exclude = ('slug', 'last_mod_time', 'creation_time')
+ #ymq:编辑页排除slug、最后修改时间和创建时间字段
class LinksAdmin(admin.ModelAdmin):
+ #ymq:定义Links模型的admin管理类
exclude = ('last_mod_time', 'creation_time')
+ #ymq:编辑页排除最后修改时间和创建时间字段
class SideBarAdmin(admin.ModelAdmin):
+ #ymq:定义SideBar模型的admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence')
+ #ymq:列表页显示名称、内容、是否启用和排序序号字段
exclude = ('last_mod_time', 'creation_time')
+ #ymq:编辑页排除最后修改时间和创建时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
+ #ymq:定义BlogSettings模型的admin管理类
pass
+ #ymq:暂未设置特殊配置,使用默认admin行为
\ No newline at end of file
diff --git a/src/django-master/blog/apps.py b/src/django-master/blog/apps.py
index 7930587..d7e57a1 100644
--- a/src/django-master/blog/apps.py
+++ b/src/django-master/blog/apps.py
@@ -1,5 +1,7 @@
from django.apps import AppConfig
-
+#ymq:导入Django的AppConfig类,用于定义应用的配置信息
class BlogConfig(AppConfig):
+ #ymq:定义博客应用的配置类,继承自AppConfig
name = 'blog'
+ #ymq:指定应用的名称为'blog',Django通过该名称识别此应用
\ No newline at end of file
diff --git a/src/django-master/blog/context_processors.py b/src/django-master/blog/context_processors.py
index 73e3088..ebe7d09 100644
--- a/src/django-master/blog/context_processors.py
+++ b/src/django-master/blog/context_processors.py
@@ -1,43 +1,65 @@
import logging
+#ymq:导入logging模块,用于日志记录
from django.utils import timezone
+#ymq:导入Django的timezone模块,用于处理时间相关操作
from djangoblog.utils import cache, get_blog_setting
+#ymq:从项目工具模块导入缓存工具和获取博客设置的函数
from .models import Category, Article
+#ymq:从当前应用的models模块导入分类和文章模型
logger = logging.getLogger(__name__)
+#ymq:创建当前模块的日志记录器实例
def seo_processor(requests):
+ #ymq:定义SEO上下文处理器,用于向模板全局注入通用数据
key = 'seo_processor'
+ #ymq:缓存键名,用于标识当前处理器的缓存数据
value = cache.get(key)
+ #ymq:尝试从缓存中获取数据
+
if value:
+ #ymq:如果缓存存在,直接返回缓存数据
return value
else:
+ #ymq:如果缓存不存在,重新生成数据
logger.info('set processor cache.')
+ #ymq:记录日志,提示正在设置缓存
+
setting = get_blog_setting()
+ #ymq:获取博客的全局设置信息
+
+ #ymq:构建需要传递给模板的上下文数据字典
value = {
- 'SITE_NAME': setting.site_name,
- 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
- 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
- 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
- 'SITE_DESCRIPTION': setting.site_description,
- 'SITE_KEYWORDS': setting.site_keywords,
+ 'SITE_NAME': setting.site_name, # 网站名称
+ 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告
+ 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码
+ 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
+ 'SITE_DESCRIPTION': setting.site_description, # 网站描述
+ 'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
+ # 网站基础URL(协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
- 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
- 'nav_category_list': Category.objects.all(),
+ 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
+ 'nav_category_list': Category.objects.all(), # 导航分类列表
+ # 导航页面列表(类型为页面且状态为已发布)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
- 'OPEN_SITE_COMMENT': setting.open_site_comment,
- 'BEIAN_CODE': setting.beian_code,
- 'ANALYTICS_CODE': setting.analytics_code,
- "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
- "SHOW_GONGAN_CODE": setting.show_gongan_code,
- "CURRENT_YEAR": timezone.now().year,
- "GLOBAL_HEADER": setting.global_header,
- "GLOBAL_FOOTER": setting.global_footer,
- "COMMENT_NEED_REVIEW": setting.comment_need_review,
+ 'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
+ 'BEIAN_CODE': setting.beian_code, # 网站备案号
+ 'ANALYTICS_CODE': setting.analytics_code, # 统计分析代码
+ "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
+ "SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号
+ "CURRENT_YEAR": timezone.now().year, # 当前年份(用于页脚等位置)
+ "GLOBAL_HEADER": setting.global_header, # 全局头部代码
+ "GLOBAL_FOOTER": setting.global_footer, # 全局底部代码
+ "COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
}
+
cache.set(key, value, 60 * 60 * 10)
+ #ymq:将生成的上下文数据存入缓存,有效期10小时(60秒*60分*10小时)
+
return value
+ #ymq:返回构建好的上下文数据字典
\ No newline at end of file
diff --git a/src/django-master/blog/documents.py b/src/django-master/blog/documents.py
index 0f1db7b..38db391 100644
--- a/src/django-master/blog/documents.py
+++ b/src/django-master/blog/documents.py
@@ -1,26 +1,37 @@
import time
+#ymq:导入time模块,用于处理时间相关操作(如生成唯一ID)
import elasticsearch.client
+#ymq:导入elasticsearch客户端模块,用于操作Elasticsearch的Ingest API
from django.conf import settings
+#ymq:导入Django的settings模块,用于获取项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
+#ymq:导入elasticsearch-dsl相关类,用于定义Elasticsearch文档结构和字段类型
from elasticsearch_dsl.connections import connections
+#ymq:导入elasticsearch-dsl的连接管理工具,用于创建与Elasticsearch的连接
from blog.models import Article
+#ymq:从blog应用导入Article模型,用于同步数据到Elasticsearch
+#ymq:判断是否启用Elasticsearch(检查settings中是否配置了ELASTICSEARCH_DSL)
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
+ #ymq:如果启用Elasticsearch,创建连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
-
+ #ymq:创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
+ #ymq:创建Ingest客户端,用于管理数据处理管道
c = IngestClient(es)
try:
+ #ymq:尝试获取名为'geoip'的管道,检查是否已存在
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
+ #ymq:如果管道不存在,则创建它(用于解析IP地址的地理位置信息)
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@@ -34,72 +45,85 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
- continent_name = Keyword()
- country_iso_code = Keyword()
- country_name = Keyword()
- location = GeoPoint()
+ #ymq:定义地理位置信息的内部文档(嵌套结构)
+ continent_name = Keyword() # 大陆名称(关键字类型,不分词)
+ country_iso_code = Keyword() # 国家ISO代码(关键字类型)
+ country_name = Keyword() # 国家名称(关键字类型)
+ location = GeoPoint() # 地理位置坐标(经纬度)
class UserAgentBrowser(InnerDoc):
- Family = Keyword()
- Version = Keyword()
+ #ymq:定义用户代理中浏览器信息的内部文档
+ Family = Keyword() # 浏览器家族(如Chrome、Firefox)
+ Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser):
+ #ymq:定义用户代理中操作系统信息的内部文档(继承浏览器结构)
pass
class UserAgentDevice(InnerDoc):
- Family = Keyword()
- Brand = Keyword()
- Model = Keyword()
+ #ymq:定义用户代理中设备信息的内部文档
+ Family = Keyword() # 设备家族
+ Brand = Keyword() # 设备品牌
+ Model = Keyword() # 设备型号
class UserAgent(InnerDoc):
- browser = Object(UserAgentBrowser, required=False)
- os = Object(UserAgentOS, required=False)
- device = Object(UserAgentDevice, required=False)
- string = Text()
- is_bot = Boolean()
+ #ymq:定义用户代理完整信息的内部文档(嵌套结构)
+ browser = Object(UserAgentBrowser, required=False) # 浏览器信息
+ os = Object(UserAgentOS, required=False) # 操作系统信息
+ device = Object(UserAgentDevice, required=False) # 设备信息
+ string = Text() # 原始用户代理字符串
+ is_bot = Boolean() # 是否为爬虫
class ElapsedTimeDocument(Document):
- url = Keyword()
- time_taken = Long()
- log_datetime = Date()
- ip = Keyword()
- geoip = Object(GeoIp, required=False)
- useragent = Object(UserAgent, required=False)
+ #ymq:定义用于记录性能耗时的Elasticsearch文档
+ url = Keyword() # 访问的URL(关键字类型)
+ time_taken = Long() # 耗时(毫秒)
+ log_datetime = Date() # 日志记录时间
+ ip = Keyword() # 访问IP地址
+ geoip = Object(GeoIp, required=False) # 地理位置信息(嵌套)
+ useragent = Object(UserAgent, required=False) # 用户代理信息(嵌套)
class Index:
- name = 'performance'
+ #ymq:定义索引配置
+ name = 'performance' # 索引名称
settings = {
- "number_of_shards": 1,
- "number_of_replicas": 0
+ "number_of_shards": 1, # 分片数量
+ "number_of_replicas": 0 # 副本数量
}
class Meta:
- doc_type = 'ElapsedTime'
+ doc_type = 'ElapsedTime' # 文档类型(Elasticsearch 7.x后逐渐废弃)
class ElaspedTimeDocumentManager:
+ #ymq:性能耗时文档的管理类,用于索引的创建、删除和数据插入
@staticmethod
def build_index():
+ #ymq:创建索引(如果不存在)
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- res = client.indices.exists(index="performance")
+ res = client.indices.exists(index="performance") # 检查索引是否存在
if not res:
- ElapsedTimeDocument.init()
+ ElapsedTimeDocument.init() # 初始化索引
@staticmethod
def delete_index():
+ #ymq:删除performance索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- es.indices.delete(index='performance', ignore=[400, 404])
+ es.indices.delete(index='performance', ignore=[400, 404]) # 忽略不存在的情况
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
- ElaspedTimeDocumentManager.build_index()
+ #ymq:创建一条性能耗时记录
+ ElaspedTimeDocumentManager.build_index() # 确保索引存在
+
+ #ymq:构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@@ -116,98 +140,112 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
+ #ymq:创建文档实例,使用时间戳作为唯一ID
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
- 1000))
+ 1000)) # 毫秒级时间戳作为ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
+ #ymq:保存文档时应用geoip管道解析IP地址
doc.save(pipeline="geoip")
class ArticleDocument(Document):
+ #ymq:定义文章信息的Elasticsearch文档(用于搜索)
+ #ymq:body和title使用IK分词器(max_word分词更细,smart更简洁)
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ #ymq:嵌套作者信息
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
+ #ymq:嵌套分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
+ #ymq:嵌套标签信息(数组)
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- pub_time = Date()
- status = Text()
- comment_status = Text()
- type = Text()
- views = Integer()
- article_order = Integer()
+ pub_time = Date() # 发布时间
+ status = Text() # 状态(发布/草稿)
+ comment_status = Text() # 评论状态(开启/关闭)
+ type = Text() # 类型(文章/页面)
+ views = Integer() # 浏览量
+ article_order = Integer() # 排序序号
class Index:
- name = 'blog'
+ name = 'blog' # 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
- doc_type = 'Article'
+ doc_type = 'Article' # 文档类型
class ArticleDocumentManager():
+ #ymq:文章文档的管理类,用于索引操作和数据同步
def __init__(self):
+ #ymq:初始化时创建索引
self.create_index()
def create_index(self):
+ #ymq:初始化文章索引
ArticleDocument.init()
def delete_index(self):
+ #ymq:删除blog索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
+ #ymq:将Django模型对象转换为Elasticsearch文档对象
return [
ArticleDocument(
- meta={
- 'id': article.id},
+ meta={'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
- 'id': article.author.id},
+ 'id': article.author.id
+ },
category={
'name': article.category.name,
- 'id': article.category.id},
- tags=[
- {
- 'name': t.name,
- 'id': t.id} for t in article.tags.all()],
+ 'id': article.category.id
+ },
+ tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], # 转换多对多标签
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
- article_order=article.article_order) for article in articles]
+ article_order=article.article_order
+ ) for article in articles
+ ]
def rebuild(self, articles=None):
+ #ymq:重建索引(默认同步所有文章,可指定文章列表)
ArticleDocument.init()
- articles = articles if articles else Article.objects.all()
- docs = self.convert_to_doc(articles)
+ articles = articles if articles else Article.objects.all() # 获取文章数据
+ docs = self.convert_to_doc(articles) # 转换为文档对象
for doc in docs:
- doc.save()
+ doc.save() # 保存到Elasticsearch
def update_docs(self, docs):
+ #ymq:批量更新文档
for doc in docs:
- doc.save()
+ doc.save()
\ No newline at end of file
diff --git a/src/django-master/blog/forms.py b/src/django-master/blog/forms.py
index 1082938..e637b4f 100644
--- a/src/django-master/blog/forms.py
+++ b/src/django-master/blog/forms.py
@@ -1,19 +1,36 @@
+<<<<<<< HEAD
import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。
+=======
+import logging
+#ymq:导入logging模块,用于记录搜索相关日志
+>>>>>>> c6856732b39cce6b1aab30e6649dcdb806b75b9f
from django import forms
+#ymq:导入Django的forms模块,用于创建自定义表单
from haystack.forms import SearchForm
+#ymq:导入Haystack的SearchForm基类,扩展实现博客搜索表单
logger = logging.getLogger(__name__)
+#ymq:创建当前模块的日志记录器实例
class BlogSearchForm(SearchForm):
+ #ymq:定义博客搜索表单类,继承自Haystack的SearchForm
querydata = forms.CharField(required=True)
+ #ymq:定义搜索关键词字段,required=True表示该字段为必填项
def search(self):
+ #ymq:重写父类的search方法,自定义搜索逻辑
datas = super(BlogSearchForm, self).search()
+ #ymq:调用父类search方法,获取基础搜索结果
+
if not self.is_valid():
+ #ymq:如果表单数据验证不通过,返回无结果响应
return self.no_query_found()
if self.cleaned_data['querydata']:
+ #ymq:如果存在合法的搜索关键词,记录关键词日志
logger.info(self.cleaned_data['querydata'])
+
return datas
+ #ymq:返回最终的搜索结果集
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/build_index.py b/src/django-master/blog/management/commands/build_index.py
index 3c4acd7..69a4490 100644
--- a/src/django-master/blog/management/commands/build_index.py
+++ b/src/django-master/blog/management/commands/build_index.py
@@ -1,18 +1,31 @@
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
+#ymq:从blog.documents导入Elasticsearch相关的文档类和管理器,以及启用状态常量
# TODO 参数化
class Command(BaseCommand):
+ #ymq:定义自定义管理命令类,继承自BaseCommand
help = 'build search index'
+ #ymq:定义命令的帮助信息,使用python manage.py help build_index时显示
def handle(self, *args, **options):
+ #ymq:命令的核心处理方法,执行实际的索引构建逻辑
if ELASTICSEARCH_ENABLED:
+ #ymq:仅当Elasticsearch启用时执行以下操作
ElaspedTimeDocumentManager.build_index()
+ #ymq:调用性能耗时文档管理器构建索引(若不存在)
+
manager = ElapsedTimeDocument()
manager.init()
+ #ymq:初始化ElapsedTimeDocument对应的索引结构
+
manager = ArticleDocumentManager()
manager.delete_index()
+ #ymq:删除已存在的文章索引(重建前清理)
+
manager.rebuild()
+ #ymq:重建文章索引,将数据库中的文章数据同步到Elasticsearch
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/build_search_words.py b/src/django-master/blog/management/commands/build_search_words.py
index cfe7e0d..35d0c6e 100644
--- a/src/django-master/blog/management/commands/build_search_words.py
+++ b/src/django-master/blog/management/commands/build_search_words.py
@@ -1,13 +1,20 @@
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from blog.models import Tag, Category
+#ymq:从blog应用导入Tag(标签)和Category(分类)模型
# TODO 参数化
class Command(BaseCommand):
+ #ymq:定义自定义管理命令类,继承自BaseCommand
help = 'build search words'
+ #ymq:命令的帮助信息,说明该命令用于生成搜索词
def handle(self, *args, **options):
+ #ymq:命令的核心处理方法,执行生成搜索词的逻辑
+ # 从标签和分类中提取名称,使用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))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/clear_cache.py b/src/django-master/blog/management/commands/clear_cache.py
index 0d66172..6366680 100644
--- a/src/django-master/blog/management/commands/clear_cache.py
+++ b/src/django-master/blog/management/commands/clear_cache.py
@@ -1,11 +1,17 @@
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from djangoblog.utils import cache
+#ymq:从项目工具模块导入缓存工具
class Command(BaseCommand):
+ #ymq:定义清除缓存的自定义命令类,继承自BaseCommand
help = 'clear the whole cache'
+ #ymq:命令的帮助信息,说明该命令用于清除所有缓存
def handle(self, *args, **options):
- cache.clear()
+ #ymq:命令的核心处理方法,执行清除缓存操作
+ cache.clear() # 调用缓存工具的clear方法,清除所有缓存数据
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
+ #ymq:向标准输出写入成功信息,使用Django的SUCCESS样式(通常为绿色)
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/create_testdata.py b/src/django-master/blog/management/commands/create_testdata.py
index 675d2ba..a3dcce8 100644
--- a/src/django-master/blog/management/commands/create_testdata.py
+++ b/src/django-master/blog/management/commands/create_testdata.py
@@ -1,40 +1,62 @@
from django.contrib.auth import get_user_model
+#ymq:导入获取用户模型的函数,便于灵活引用用户模型
from django.contrib.auth.hashers import make_password
+#ymq:导入密码加密函数,用于安全存储密码
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from blog.models import Article, Tag, Category
+#ymq:从blog应用导入文章、标签、分类模型
class Command(BaseCommand):
+ #ymq:定义创建测试数据的自定义命令类,继承自BaseCommand
help = 'create test datas'
+ #ymq:命令的帮助信息,说明该命令用于创建测试数据
def handle(self, *args, **options):
+ #ymq:命令的核心处理方法,执行创建测试数据的逻辑
+ # 创建或获取测试用户(邮箱、用户名、密码加密存储)
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
+ # 创建或获取父分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
+ # 创建或获取子分类(关联父分类)
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
- category.save()
+ category.save() # 保存子分类
+
+ # 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
+
+ # 批量创建20篇测试文章
for i in range(1, 20):
+ # 创建或获取文章(关联分类、作者)
article = Article.objects.get_or_create(
category=category,
- title='nice title ' + str(i),
- body='nice content ' + str(i),
+ 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()
+ 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'))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/ping_baidu.py b/src/django-master/blog/management/commands/ping_baidu.py
index 2c7fbdd..092ed48 100644
--- a/src/django-master/blog/management/commands/ping_baidu.py
+++ b/src/django-master/blog/management/commands/ping_baidu.py
@@ -1,16 +1,24 @@
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from djangoblog.spider_notify import SpiderNotify
+#ymq:导入蜘蛛通知工具类,用于向搜索引擎提交URL
from djangoblog.utils import get_current_site
+#ymq:导入获取当前站点信息的工具函数
from blog.models import Article, Tag, Category
+#ymq:从blog应用导入文章、标签、分类模型
site = get_current_site().domain
+#ymq:获取当前站点的域名,用于构建完整URL
class Command(BaseCommand):
+ #ymq:定义百度URL提交命令类,继承自BaseCommand
help = 'notify baidu url'
+ #ymq:命令的帮助信息,说明该命令用于向百度提交URL
def add_arguments(self, parser):
+ #ymq:定义命令参数,指定提交的数据类型
parser.add_argument(
'data_type',
type=str,
@@ -20,31 +28,46 @@ class Command(BaseCommand):
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
+ #ymq:参数说明:article-所有文章,tag-所有标签,category-所有分类,all-全部
def get_full_url(self, path):
+ #ymq:构建包含域名的完整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)
+ #ymq:命令核心处理方法,执行URL收集和提交
+ type = options['data_type'] # 获取用户指定的数据类型
+ self.stdout.write('start get %s' % type) # 输出开始收集信息的提示
- urls = []
+ urls = [] # 存储待提交的URL列表
+
+ # 根据数据类型收集对应的URL
if type == 'article' or type == 'all':
+ # 收集已发布文章的URL
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
+
if type == 'tag' or type == 'all':
+ # 收集所有标签页的URL
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
+
if type == 'category' or type == 'all':
+ # 收集所有分类页的URL
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_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'))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/sync_user_avatar.py b/src/django-master/blog/management/commands/sync_user_avatar.py
index d0f4612..f51a404 100644
--- a/src/django-master/blog/management/commands/sync_user_avatar.py
+++ b/src/django-master/blog/management/commands/sync_user_avatar.py
@@ -1,47 +1,70 @@
import requests
+#ymq:导入requests库,用于发送HTTP请求测试图片URL有效性
from django.core.management.base import BaseCommand
+#ymq:导入Django的BaseCommand类,用于创建自定义管理命令
from django.templatetags.static import static
+#ymq:导入static标签,用于获取静态文件URL
from djangoblog.utils import save_user_avatar
+#ymq:导入保存用户头像的工具函数
from oauth.models import OAuthUser
+#ymq:从oauth应用导入OAuthUser模型,存储第三方用户信息
from oauth.oauthmanager import get_manager_by_type
+#ymq:导入获取对应第三方登录管理器的函数
class Command(BaseCommand):
+ #ymq:定义同步用户头像的自定义命令类,继承自BaseCommand
help = 'sync user avatar'
+ #ymq:命令的帮助信息,说明该命令用于同步用户头像
def test_picture(self, url):
+ #ymq:测试图片URL是否有效(状态码200)
try:
if requests.get(url, timeout=2).status_code == 200:
- return True
+ return True # URL有效返回True
except:
- pass
+ pass # 异常或状态码非200返回None
def handle(self, *args, **options):
- static_url = static("../")
- users = OAuthUser.objects.all()
- self.stdout.write(f'开始同步{len(users)}个用户头像')
+ #ymq:命令核心处理方法,执行用户头像同步逻辑
+ static_url = static("../") # 获取静态文件基础URL
+ users = OAuthUser.objects.all() # 获取所有第三方用户
+ self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出待同步用户数量
+
for u in users:
- self.stdout.write(f'开始同步:{u.nickname}')
- url = u.picture
+ #ymq:遍历每个用户进行头像同步
+ self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户名
+ url = u.picture # 获取用户当前头像URL
+
if url:
+ # 处理已有头像URL的情况
if url.startswith(static_url):
+ # 头像URL是本地静态文件
if self.test_picture(url):
+ # 图片有效,跳过同步
continue
else:
+ # 图片无效,重新获取
if u.metadata:
+ # 有元数据,通过第三方管理器获取头像
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
- url = save_user_avatar(url)
+ url = save_user_avatar(url) # 保存头像并获取本地URL
else:
+ # 无元数据,使用默认头像
url = static('blog/img/avatar.png')
else:
+ # 头像URL是外部链接,保存到本地
url = save_user_avatar(url)
else:
+ # 无头像URL,使用默认头像
url = static('blog/img/avatar.png')
+
if url:
- self.stdout.write(
- f'结束同步:{u.nickname}.url:{url}')
+ # 保存更新后的头像URL
+ self.stdout.write(f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
- self.stdout.write('结束同步')
+
+ self.stdout.write('结束同步') # 输出同步完成提示
\ No newline at end of file
diff --git a/src/django-master/blog/middleware.py b/src/django-master/blog/middleware.py
index 94dd70c..2c2bf83 100644
--- a/src/django-master/blog/middleware.py
+++ b/src/django-master/blog/middleware.py
@@ -1,42 +1,62 @@
import logging
import time
+#ymq:导入logging用于日志记录,time用于计算页面加载时间
from ipware import get_client_ip
+#ymq:导入get_client_ip工具,用于获取客户端IP地址
from user_agents import parse
+#ymq:导入parse函数,用于解析用户代理字符串
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
+#ymq:从博客文档模块导入Elasticsearch启用状态和性能日志管理器
logger = logging.getLogger(__name__)
+#ymq:创建当前模块的日志记录器实例
class OnlineMiddleware(object):
+ #ymq:定义在线中间件类,用于记录页面加载性能和访问信息
def __init__(self, get_response=None):
+ #ymq:初始化中间件,接收Django的响应处理器
self.get_response = get_response
super().__init__()
def __call__(self, request):
+ #ymq:中间件核心方法,处理请求并返回响应
''' page render time '''
- start_time = time.time()
- response = self.get_response(request)
- http_user_agent = request.META.get('HTTP_USER_AGENT', '')
- ip, _ = get_client_ip(request)
- user_agent = parse(http_user_agent)
+ #ymq:记录页面渲染时间的逻辑
+ start_time = time.time() # 记录请求处理开始时间
+ response = self.get_response(request) # 调用后续中间件或视图处理请求
+
+ #ymq:获取用户代理和IP地址
+ http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理字符串
+ ip, _ = get_client_ip(request) # 获取客户端IP地址
+ user_agent = parse(http_user_agent) # 解析用户代理信息(浏览器、设备等)
+
+ #ymq:非流式响应才处理(流式响应无法修改内容)
if not response.streaming:
try:
- cast_time = time.time() - start_time
+ cast_time = time.time() - start_time # 计算页面加载耗时(秒)
+
+ #ymq:如果启用了Elasticsearch,记录性能数据
if ELASTICSEARCH_ENABLED:
- time_taken = round((cast_time) * 1000, 2)
- url = request.path
+ time_taken = round((cast_time) * 1000, 2) #ymq: 转换为毫秒并保留两位小数
+ url = request.path # 获取请求的URL路径
from django.utils import timezone
+ #ymq:调用管理器创建性能日志记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
- log_datetime=timezone.now(),
- useragent=user_agent,
- ip=ip)
+ log_datetime=timezone.now(), #ymq: 记录当前时间
+ useragent=user_agent, #ymq: 已解析的用户代理信息
+ ip=ip) #ymq: 客户端IP
+
+ #ymq:替换响应内容中的标记为实际加载时间(保留前5位字符)
response.content = response.content.replace(
b'', str.encode(str(cast_time)[:5]))
+
except Exception as e:
+ #ymq:捕获并记录处理过程中的异常
logger.error("Error OnlineMiddleware: %s" % e)
- return response
+ return response #ymq: 返回处理后的响应
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0001_initial.py b/src/django-master/blog/migrations/0001_initial.py
index 3d391b6..a63b9e7 100644
--- a/src/django-master/blog/migrations/0001_initial.py
+++ b/src/django-master/blog/migrations/0001_initial.py
@@ -1,25 +1,34 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
+#ymq:该迁移文件由Django 4.1.7自动生成,生成时间为2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
+#ymq:导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
initial = True
+ #ymq:标记为初始迁移(第一次创建模型时生成)
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ #ymq:依赖于用户模型,确保用户表先创建
]
operations = [
+ #ymq:定义数据库操作列表,按顺序执行创建模型的操作
+
migrations.CreateModel(
+ #ymq:创建BlogSettings模型(网站配置)
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ #ymq:自增主键字段
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
@@ -35,13 +44,17 @@ class Migration(migrations.Migration):
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
+ #ymq:以上为网站配置的各个字段,包含网站基本信息、显示设置、备案信息等
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
+ #ymq:模型的显示名称
},
),
+
migrations.CreateModel(
+ #ymq:创建Links模型(友情链接)
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@@ -52,14 +65,18 @@ class Migration(migrations.Migration):
('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='修改时间')),
+ #ymq:友情链接字段,包含名称、URL、排序、显示位置等
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
+ #ymq:按排序号升序排列
},
),
+
migrations.CreateModel(
+ #ymq:创建SideBar模型(侧边栏)
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@@ -69,14 +86,18 @@ class Migration(migrations.Migration):
('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='修改时间')),
+ #ymq:侧边栏字段,包含标题、内容、排序等
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
+ #ymq:按排序号升序排列
},
),
+
migrations.CreateModel(
+ #ymq:创建Tag模型(标签)
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@@ -84,14 +105,18 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
+ #ymq:标签字段,包含名称、URL友好标识(slug)等
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
+ #ymq:按标签名升序排列
},
),
+
migrations.CreateModel(
+ #ymq:创建Category模型(分类)
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@@ -101,14 +126,18 @@ class Migration(migrations.Migration):
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
+ #ymq:分类字段,支持多级分类(自关联外键)、权重排序等
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
+ #ymq:按权重降序排列(权重越大越靠前)
},
),
+
migrations.CreateModel(
+ #ymq:创建Article模型(文章)
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@@ -116,6 +145,7 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
+ #ymq:使用markdown编辑器字段存储文章正文
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
@@ -124,14 +154,19 @@ class Migration(migrations.Migration):
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('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='作者')),
+ #ymq:关联用户模型(外键),级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
+ #ymq:关联分类模型(外键),级联删除
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
+ #ymq:多对多关联标签模型
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
+ #ymq:先按排序号降序,再按发布时间降序
'get_latest_by': 'id',
+ #ymq:按id获取最新记录
},
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
index adbaa36..1304b8a 100644
--- a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
+++ b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -1,23 +1,34 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
+#ymq:该迁移文件由Django 4.1.7自动生成,生成时间为2023-03-29 06:08
from django.db import migrations, models
+#ymq:导入Django迁移相关模块
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
dependencies = [
('blog', '0001_initial'),
+ #ymq:依赖于blog应用的0001_initial迁移文件,确保先执行初始迁移
]
operations = [
+ #ymq:定义数据库操作列表,添加新字段
+
migrations.AddField(
+ #ymq:向BlogSettings模型添加global_footer字段
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
+ #ymq:字段类型为文本字段,允许为空,默认值为空字符串,verbose_name为"公共尾部"
),
+
migrations.AddField(
+ #ymq:向BlogSettings模型添加global_header字段
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
+ #ymq:字段类型为文本字段,允许为空,默认值为空字符串,verbose_name为"公共头部"
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
index e9f5502..908f852 100644
--- a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
+++ b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -1,17 +1,25 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
+#ymq:该迁移文件由Django 4.2.1自动生成,生成时间为2023-05-09 07:45
from django.db import migrations, models
-
+#ymq:导入Django迁移相关模块
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
+
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
+ #ymq:依赖于blog应用的0002号迁移文件,确保先执行该迁移
]
operations = [
+ #ymq:定义数据库操作,此处为添加字段
+
migrations.AddField(
- model_name='blogsettings',
- name='comment_need_review',
+ #ymq:向BlogSettings模型添加comment_need_review字段
+ model_name='blogsettings', # 目标模型名称
+ name='comment_need_review', # 新字段名称
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
+ #ymq:字段类型为布尔值,默认值为False(不需要审核),后台显示名称为"评论是否需要审核"
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
index ceb1398..f6465d8 100644
--- a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
+++ b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -1,27 +1,39 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
+#ymq:该迁移文件由Django 4.2.1自动生成,生成时间为2023-05-09 07:51
from django.db import migrations
+#ymq:导入Django迁移相关模块
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
+
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
+ #ymq:依赖于blog应用的0003号迁移文件,确保先执行该迁移
]
operations = [
+ #ymq:定义数据库操作列表,主要是重命名字段
+
migrations.RenameField(
- model_name='blogsettings',
- old_name='analyticscode',
- new_name='analytics_code',
+ #ymq:重命名BlogSettings模型的analyticscode字段
+ model_name='blogsettings', # 目标模型名称
+ old_name='analyticscode', # 旧字段名
+ new_name='analytics_code', # 新字段名(改为下划线命名规范)
),
+
migrations.RenameField(
+ #ymq:重命名BlogSettings模型的beiancode字段
model_name='blogsettings',
old_name='beiancode',
- new_name='beian_code',
+ new_name='beian_code', # 改为下划线命名规范
),
+
migrations.RenameField(
+ #ymq:重命名BlogSettings模型的sitename字段
model_name='blogsettings',
old_name='sitename',
- new_name='site_name',
+ new_name='site_name', # 改为下划线命名规范
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
index d08e853..d06b10a 100644
--- a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
+++ b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -1,20 +1,27 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
+#ymq:该迁移文件由Django 4.2.5自动生成,生成时间为2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
+#ymq:导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
+ #ymq:依赖于用户模型和blog应用的0004号迁移文件
]
operations = [
+ #ymq:定义数据库操作列表,包含模型选项修改、字段删除、添加和修改
+
+ # 修改模型的元数据选项(主要是verbose_name的国际化调整)
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
@@ -35,6 +42,8 @@ class Migration(migrations.Migration):
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
+
+ # 删除旧的时间字段(命名方式调整)
migrations.RemoveField(
model_name='article',
name='created_time',
@@ -67,6 +76,8 @@ class Migration(migrations.Migration):
model_name='tag',
name='last_mod_time',
),
+
+ # 添加新的时间字段(统一命名为creation_time和last_modify_time)
migrations.AddField(
model_name='article',
name='creation_time',
@@ -107,6 +118,8 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+
+ # 修改Article模型的字段属性(主要是verbose_name国际化)
migrations.AlterField(
model_name='article',
name='article_order',
@@ -167,6 +180,8 @@ class Migration(migrations.Migration):
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
+
+ # 修改BlogSettings模型的字段属性(verbose_name国际化)
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
@@ -222,6 +237,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 +254,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 +286,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 +313,11 @@ class Migration(migrations.Migration):
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
+
+ # 修改Tag模型的字段属性
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
index e36feb4..207d123 100644
--- a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
+++ b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
@@ -1,17 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
+#ymq:该迁移文件由Django 4.2.7自动生成,生成时间为2024年1月26日02:41
from django.db import migrations
+#ymq:导入Django迁移相关模块
class Migration(migrations.Migration):
+ #ymq:定义迁移类,继承自migrations.Migration
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
+ #ymq:依赖于blog应用的0005号迁移文件,确保先执行该迁移
]
operations = [
+ #ymq:定义数据库操作,此处为修改模型选项
migrations.AlterModelOptions(
name='blogsettings',
+ #ymq:修改BlogSettings模型的显示名称,改为英文"Website configuration"
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/models.py b/src/django-master/blog/models.py
index 083788b..d92d098 100644
--- a/src/django-master/blog/models.py
+++ b/src/django-master/blog/models.py
@@ -1,6 +1,7 @@
import logging
import re
from abc import abstractmethod
+#ymq:导入logging用于日志记录,re用于正则表达式操作,abstractmethod用于定义抽象方法
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -8,36 +9,43 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
-from mdeditor.fields import MDTextField
-from uuslug import slugify
+from mdeditor.fields import MDTextField # 导入markdown编辑器字段
+from uuslug import slugify # 导入slug生成工具
-from djangoblog.utils import cache_decorator, cache
-from djangoblog.utils import get_current_site
+from djangoblog.utils import cache_decorator, cache # 导入缓存相关工具
+from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
class LinkShowType(models.TextChoices):
- I = ('i', _('index'))
- L = ('l', _('list'))
- P = ('p', _('post'))
- A = ('a', _('all'))
- S = ('s', _('slide'))
+ #ymq:定义链接展示位置的枚举类
+ I = ('i', _('index')) # 首页展示
+ L = ('l', _('list')) # 列表页展示
+ P = ('p', _('post')) # 文章页展示
+ A = ('a', _('all')) # 所有页面展示
+ S = ('s', _('slide')) # 幻灯片展示
class BaseModel(models.Model):
- id = models.AutoField(primary_key=True)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_modify_time = models.DateTimeField(_('modify time'), default=now)
+ #ymq:定义模型基类,封装公共字段和方法(抽象类)
+ id = models.AutoField(primary_key=True) # 自增主键
+ creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
+ last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
+ #ymq:重写保存方法,处理slug生成和特殊更新逻辑
+ # 判断是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
+ # 单独处理浏览量更新,提高性能
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
+ # 自动生成slug(用于URL友好化)
if 'slug' in self.__dict__:
+ # 根据title或name字段生成slug
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
@@ -45,79 +53,88 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
+ #ymq:生成包含域名的完整URL
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
- abstract = True
+ abstract = True # 声明为抽象模型,不生成数据库表
@abstractmethod
def get_absolute_url(self):
+ #ymq:抽象方法,子类必须实现,用于生成模型实例的URL
pass
class Article(BaseModel):
- """文章"""
+ """文章模型"""
+ # 状态选项:草稿/已发布
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
+ # 评论状态选项:开启/关闭
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
+ # 类型选项:文章/页面
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
- title = models.CharField(_('title'), max_length=200, unique=True)
- body = MDTextField(_('body'))
+
+ title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
+ body = MDTextField(_('body')) # 文章内容(使用markdown编辑器)
pub_time = models.DateTimeField(
- _('publish time'), blank=False, null=False, default=now)
+ _('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
- default='p')
+ default='p') # 发布状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
- default='o')
- type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
- views = models.PositiveIntegerField(_('views'), default=0)
+ default='o') # 评论状态
+ type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
+ views = models.PositiveIntegerField(_('views'), default=0) # 浏览量
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
- on_delete=models.CASCADE)
+ on_delete=models.CASCADE) # 关联作者(外键)
article_order = models.IntegerField(
- _('order'), blank=False, null=False, default=0)
- show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
+ _('order'), blank=False, null=False, default=0) # 排序序号
+ show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
- null=False)
- tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
+ null=False) # 关联分类(外键)
+ tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 关联标签(多对多)
def body_to_string(self):
+ #ymq:返回文章内容字符串
return self.body
def __str__(self):
+ #ymq:模型实例的字符串表示(文章标题)
return self.title
class Meta:
- ordering = ['-article_order', '-pub_time']
+ ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序号降序,再按发布时间降序
verbose_name = _('article')
verbose_name_plural = verbose_name
- get_latest_by = 'id'
+ get_latest_by = 'id' # 按id获取最新记录
def get_absolute_url(self):
+ #ymq:生成文章详情页的URL
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@@ -125,21 +142,24 @@ class Article(BaseModel):
'day': self.creation_time.day
})
- @cache_decorator(60 * 60 * 10)
+ @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
+ #ymq:获取当前文章所属分类的层级结构(含父级分类)
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
-
return names
def save(self, *args, **kwargs):
+ #ymq:重写保存方法(可扩展自定义逻辑)
super().save(*args, **kwargs)
def viewed(self):
+ #ymq:增加浏览量并保存
self.views += 1
- self.save(update_fields=['views'])
+ self.save(update_fields=['views']) # 只更新views字段,提高性能
def comment_list(self):
+ #ymq:获取文章的评论列表(带缓存)
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@@ -147,67 +167,64 @@ class Article(BaseModel):
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
- cache.set(cache_key, comments, 60 * 100)
+ cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
+ #ymq:生成文章在admin后台的编辑URL
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
- @cache_decorator(expiration=60 * 100)
+ @cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
- # 下一篇
+ #ymq:获取下一篇文章(ID更大的已发布文章)
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
- @cache_decorator(expiration=60 * 100)
+ @cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
- # 前一篇
+ #ymq:获取上一篇文章(ID更小的已发布文章)
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
- """
- Get the first image url from article.body.
- :return:
- """
- match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
+ """从文章内容中提取第一张图片的URL"""
+ match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 匹配markdown图片语法
if match:
return match.group(1)
return ""
class Category(BaseModel):
- """文章分类"""
- name = models.CharField(_('category name'), max_length=30, unique=True)
+ """文章分类模型"""
+ name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
- on_delete=models.CASCADE)
- slug = models.SlugField(default='no-slug', max_length=60, blank=True)
- index = models.IntegerField(default=0, verbose_name=_('index'))
+ on_delete=models.CASCADE) # 父分类(自关联,支持多级分类)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识
+ index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引
class Meta:
- ordering = ['-index']
+ ordering = ['-index'] # 按索引降序排列
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
+ #ymq:生成分类详情页的URL
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
+ #ymq:模型实例的字符串表示(分类名称)
return self.name
- @cache_decorator(60 * 60 * 10)
+ @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
- """
- 递归获得分类目录的父级
- :return:
- """
+ """递归获取当前分类的所有父级分类,形成层级结构"""
categorys = []
def parse(category):
@@ -218,12 +235,9 @@ class Category(BaseModel):
parse(self)
return categorys
- @cache_decorator(60 * 60 * 10)
+ @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
- """
- 获得当前分类目录所有子集
- :return:
- """
+ """获取当前分类的所有子分类(含多级子分类)"""
categorys = []
all_categorys = Category.objects.all()
@@ -241,136 +255,143 @@ class Category(BaseModel):
class Tag(BaseModel):
- """文章标签"""
- name = models.CharField(_('tag name'), max_length=30, unique=True)
- slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ """文章标签模型"""
+ name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识
def __str__(self):
+ #ymq:模型实例的字符串表示(标签名称)
return self.name
def get_absolute_url(self):
+ #ymq:生成标签详情页的URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
- @cache_decorator(60 * 60 * 10)
+ @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
+ #ymq:获取该标签关联的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
- ordering = ['name']
+ ordering = ['name'] # 按名称排序
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
- """友情链接"""
-
- name = models.CharField(_('link name'), max_length=30, unique=True)
- link = models.URLField(_('link'))
- sequence = models.IntegerField(_('order'), unique=True)
+ """友情链接模型"""
+ name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
+ link = models.URLField(_('link')) # 链接URL
+ sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(
- _('is show'), default=True, blank=False, null=False)
+ _('is show'), default=True, blank=False, null=False) # 是否显示
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
- default=LinkShowType.I)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_mod_time = models.DateTimeField(_('modify time'), default=now)
+ default=LinkShowType.I) # 展示位置
+ creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
+ last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
- ordering = ['sequence']
+ ordering = ['sequence'] # 按排序序号排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
+ #ymq:模型实例的字符串表示(链接名称)
return self.name
class SideBar(models.Model):
- """侧边栏,可以展示一些html内容"""
- name = models.CharField(_('title'), max_length=100)
- content = models.TextField(_('content'))
- sequence = models.IntegerField(_('order'), unique=True)
- is_enable = models.BooleanField(_('is enable'), default=True)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_mod_time = models.DateTimeField(_('modify time'), default=now)
+ """侧边栏模型(可展示自定义HTML内容)"""
+ name = models.CharField(_('title'), max_length=100) # 侧边栏标题
+ content = models.TextField(_('content')) # 侧边栏内容(HTML)
+ sequence = models.IntegerField(_('order'), unique=True) # 排序序号
+ is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
+ creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
+ last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
- ordering = ['sequence']
+ ordering = ['sequence'] # 按排序序号排列
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
+ #ymq:模型实例的字符串表示(侧边栏标题)
return self.name
class BlogSettings(models.Model):
- """blog的配置"""
+ """博客全局配置模型"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
- default='')
+ default='') # 网站名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
- default='')
+ default='') # 网站描述
site_seo_description = models.TextField(
- _('site seo description'), max_length=1000, null=False, blank=False, default='')
+ _('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
- default='')
- article_sub_length = models.IntegerField(_('article sub length'), default=300)
- sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
- sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
- article_comment_count = models.IntegerField(_('article comment count'), default=5)
- show_google_adsense = models.BooleanField(_('show adsense'), default=False)
+ default='') # 网站关键词
+ article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
+ sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
+ sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
+ article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量
+ show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示谷歌广告
google_adsense_codes = models.TextField(
- _('adsense code'), max_length=2000, null=True, blank=True, default='')
- open_site_comment = models.BooleanField(_('open site comment'), default=True)
- global_header = models.TextField("公共头部", null=True, blank=True, default='')
- global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
+ _('adsense code'), max_length=2000, null=True, blank=True, default='') # 谷歌广告代码
+ open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论
+ global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部代码
+ global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部代码
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
- default='')
+ default='') # 网站备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
- default='')
+ default='') # 统计分析代码
show_gongan_code = models.BooleanField(
- '是否显示公安备案号', default=False, null=False)
+ '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
- default='')
+ default='') # 公安备案号
comment_need_review = models.BooleanField(
- '评论是否需要审核', default=False, null=False)
+ '评论是否需要审核', default=False, null=False) # 评论是否需要审核
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
+ #ymq:模型实例的字符串表示(网站名称)
return self.site_name
def clean(self):
+ #ymq:数据验证,确保全局配置只能有一条记录
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
+ #ymq:保存配置后清除缓存,确保配置立即生效
super().save(*args, **kwargs)
from djangoblog.utils import cache
- cache.clear()
+ cache.clear()
\ No newline at end of file
diff --git a/src/django-master/blog/search_indexes.py b/src/django-master/blog/search_indexes.py
index 7f1dfac..f492392 100644
--- a/src/django-master/blog/search_indexes.py
+++ b/src/django-master/blog/search_indexes.py
@@ -1,13 +1,20 @@
from haystack import indexes
+#ymq:导入Haystack的indexes模块,用于定义搜索索引
from blog.models import Article
+#ymq:从blog应用导入Article模型,为其创建搜索索引
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ #ymq:定义文章搜索索引类,继承自SearchIndex和Indexable
+ #ymq: document=True表示该字段是主要搜索字段,use_template=True表示使用模板定义字段内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
+ #ymq:指定该索引对应的模型
return Article
def index_queryset(self, using=None):
- return self.get_model().objects.filter(status='p')
+ #ymq:定义需要被索引的数据集
+ #ymq: 只索引状态为'p'(已发布)的文章
+ return self.get_model().objects.filter(status='p')
\ No newline at end of file
diff --git a/src/django-master/blog/templatetags/blog_tags.py b/src/django-master/blog/templatetags/blog_tags.py
index d6cd5d5..087c485 100644
--- a/src/django-master/blog/templatetags/blog_tags.py
+++ b/src/django-master/blog/templatetags/blog_tags.py
@@ -23,15 +23,18 @@ from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
register = template.Library()
+#ymq:注册模板标签库,用于在Django模板中使用自定义标签和过滤器
@register.simple_tag(takes_context=True)
def head_meta(context):
+ #ymq:自定义简单标签,用于生成页面头部元信息(通过插件钩子处理)
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
+ #ymq:格式化时间(仅时间部分),使用settings中定义的TIME_FORMAT
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
@@ -41,6 +44,7 @@ def timeformat(data):
@register.simple_tag
def datetimeformat(data):
+ #ymq:格式化日期时间,使用settings中定义的DATE_TIME_FORMAT
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
@@ -51,11 +55,13 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
+ #ymq:将内容转换为Markdown格式并标记为安全HTML(用于文章内容)
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
+ #ymq:获取Markdown内容的目录(TOC)并标记为安全HTML
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@@ -64,6 +70,7 @@ def get_markdown_toc(content):
@register.filter()
@stringfilter
def comment_markdown(content):
+ #ymq:处理评论内容的Markdown转换,并过滤不安全HTML标签
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@@ -76,6 +83,7 @@ def truncatechars_content(content):
:param content:
:return:
"""
+ #ymq:按网站设置的长度截断文章内容(保留HTML标签)
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@@ -85,8 +93,8 @@ def truncatechars_content(content):
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
+ #ymq:截断内容为150字符并去除HTML标签(用于生成纯文本摘要)
from django.utils.html import strip_tags
-
return strip_tags(content)[:150]
@@ -97,12 +105,13 @@ def load_breadcrumb(article):
:param article:
:return:
"""
+ #ymq:生成文章面包屑导航数据,包含分类层级和网站名称
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
- names = names[::-1]
+ names = names[::-1] # 反转列表,使层级从网站到当前分类
return {
'names': names,
@@ -118,6 +127,7 @@ def load_articletags(article):
:param article:
:return:
"""
+ #ymq:获取文章关联的标签列表,包含标签URL、文章数和随机样式
tags = article.tags.all()
tags_list = []
for tag in tags:
@@ -137,6 +147,7 @@ def load_sidebar(user, linktype):
加载侧边栏
:return:
"""
+ #ymq:加载侧边栏数据(带缓存),包含文章列表、分类、标签等
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
@@ -145,6 +156,7 @@ def load_sidebar(user, linktype):
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()
@@ -157,8 +169,8 @@ def load_sidebar(user, linktype):
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
@@ -166,7 +178,6 @@ def load_sidebar(user, linktype):
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)
@@ -185,6 +196,7 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
+ # 缓存侧边栏数据3小时
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
@@ -198,6 +210,7 @@ def load_article_metas(article, user):
:param article:
:return:
"""
+ #ymq:加载文章元信息(作者、发布时间等)供模板使用
return {
'article': article,
'user': user
@@ -206,9 +219,11 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
+ #ymq:生成分页导航链接,支持首页、标签、作者、分类等不同页面类型
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})
@@ -218,6 +233,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'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()
@@ -234,6 +250,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'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(
@@ -250,6 +267,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'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()
@@ -281,6 +299,7 @@ def load_article_detail(article, isindex, user):
:param isindex:是否列表页,若是列表页只显示摘要
:return:
"""
+ #ymq:加载文章详情数据,区分列表页(显示摘要)和详情页(显示全文)
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@@ -292,35 +311,35 @@ def load_article_detail(article, isindex, user):
}
-# return only the URL of the gravatar
-# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
- """获得gravatar头像"""
+ """获得gravatar头像URL"""
+ #ymq:获取用户头像URL(优先使用第三方登录头像,否则使用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
+ # 生成Gravatar头像URL
email = email.encode('utf-8')
-
- default = static('blog/img/avatar.png')
-
+ 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)
+ cache.set(cachekey, url, 60 * 60 * 10) # 缓存头像URL 10小时
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
- """获得gravatar头像"""
+ """获得gravatar头像img标签"""
+ #ymq:生成头像img标签(调用gravatar_url获取URL)
url = gravatar_url(email, size)
return mark_safe(
'
' %
@@ -335,10 +354,12 @@ def query(qs, **kwargs):
...
{% endfor %}
"""
- return qs.filter(**kwargs)
+ #ymq:模板中过滤查询集的标签(支持动态传参过滤)
+ return qs.filter(** kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
- return str(arg1) + str(arg2)
+ #ymq:字符串拼接过滤器(将两个参数转换为字符串并拼接)
+ return str(arg1) + str(arg2)
\ No newline at end of file
diff --git a/src/django-master/blog/tests.py b/src/django-master/blog/tests.py
index ee13505..fd67d7d 100644
--- a/src/django-master/blog/tests.py
+++ b/src/django-master/blog/tests.py
@@ -1,73 +1,101 @@
import os
+#ymq:导入os模块,用于文件路径和文件操作
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
+#ymq:导入文件上传相关类,用于模拟文件上传测试
from django.core.management import call_command
+#ymq:导入call_command,用于调用Django管理命令
from django.core.paginator import Paginator
+#ymq:导入分页类,用于测试分页功能
from django.templatetags.static import static
+#ymq:导入static标签,用于获取静态文件路径
from django.test import Client, RequestFactory, TestCase
+#ymq:导入测试相关类,Client用于模拟HTTP请求,TestCase提供测试框架
from django.urls import reverse
+#ymq:导入reverse,用于反向解析URL
from django.utils import timezone
+#ymq:导入timezone,用于处理时间相关测试数据
from accounts.models import BlogUser
+#ymq:从accounts应用导入用户模型
from blog.forms import BlogSearchForm
+#ymq:从blog应用导入搜索表单
from blog.models import Article, Category, Tag, SideBar, Links
+#ymq:从blog应用导入模型类,用于测试数据创建和查询
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
+#ymq:导入自定义模板标签函数,用于测试模板标签功能
from djangoblog.utils import get_current_site, get_sha256
+#ymq:导入工具函数,用于获取站点信息和加密
from oauth.models import OAuthUser, OAuthConfig
+#ymq:从oauth应用导入模型,用于测试第三方登录相关功能
# Create your tests here.
-
class ArticleTest(TestCase):
+ #ymq:定义文章相关的测试类,继承自TestCase
def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
+ #ymq:测试前置方法,在每个测试方法执行前运行,初始化测试客户端和工厂
+ self.client = Client() # 创建测试客户端,用于模拟HTTP请求
+ self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象
def test_validate_article(self):
- site = get_current_site().domain
+ #ymq:测试文章相关功能的完整性,包括创建、查询、页面访问等
+ site = get_current_site().domain # 获取当前站点域名
+ # 创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
- user.set_password("liangliangyy")
- user.is_staff = True
- user.is_superuser = True
- user.save()
+ user.set_password("liangliangyy") # 设置用户密码
+ user.is_staff = True # 设为 staff,允许访问admin
+ user.is_superuser = True # 设为超级用户
+ user.save() # 保存用户
+
+ # 测试用户个人页面访问
response = self.client.get(user.get_absolute_url())
- self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.status_code, 200) # 断言页面正常响应
+
+ # 测试admin相关页面访问(未登录状态)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
+
+ # 创建侧边栏测试数据
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
-
+
+ # 创建分类测试数据
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
-
+
+ # 创建标签测试数据
tag = Tag()
tag.name = "nicetag"
tag.save()
-
+
+ # 创建文章测试数据
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
- article.type = 'a'
- article.status = 'p'
-
+ article.type = 'a' # 类型为文章
+ article.status = 'p' # 状态为已发布
article.save()
- self.assertEqual(0, article.tags.count())
- article.tags.add(tag)
+
+ # 测试文章标签关联
+ self.assertEqual(0, article.tags.count()) # 初始无标签
+ article.tags.add(tag) # 添加标签
article.save()
- self.assertEqual(1, article.tags.count())
-
+ self.assertEqual(1, article.tags.count()) # 断言标签已添加
+
+ # 批量创建文章(用于测试分页)
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@@ -79,56 +107,73 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
+
+ # 测试Elasticsearch搜索功能(如果启用)
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
- call_command("build_index")
- response = self.client.get('/search', {'q': 'nicetitle'})
- self.assertEqual(response.status_code, 200)
-
+ call_command("build_index") # 调用命令构建索引
+ response = self.client.get('/search', {'q': 'nicetitle'}) # 模拟搜索请求
+ self.assertEqual(response.status_code, 200) # 断言搜索页面正常响应
+
+ # 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
+
+ # 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
- SpiderNotify.notify(article.get_absolute_url())
+ SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎
+
+ # 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
-
+
+ # 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
-
+
+ # 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
+
+ # 测试文章模板标签
s = load_articletags(article)
- self.assertIsNotNone(s)
-
+ self.assertIsNotNone(s) # 断言标签返回非空
+
+ # 用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
-
+
+ # 测试归档页面访问
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
-
+
+ # 测试不同类型的分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
- self.check_pagination(p, '', '')
-
+ self.check_pagination(p, '', '') # 全部文章分页
+
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
- self.check_pagination(p, '分类标签归档', tag.slug)
-
+ self.check_pagination(p, '分类标签归档', tag.slug) # 标签文章分页
+
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
- self.check_pagination(p, '作者文章归档', 'liangliangyy')
-
+ self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者文章分页
+
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
- self.check_pagination(p, '分类目录归档', category.slug)
-
+ self.check_pagination(p, '分类目录归档', category.slug) # 分类文章分页
+
+ # 测试搜索表单
f = BlogSearchForm()
- f.search()
- # self.client.login(username='liangliangyy', password='liangliangyy')
- from djangoblog.spider_notify import SpiderNotify
+ f.search() # 调用搜索方法
+
+ # 测试百度蜘蛛通知
SpiderNotify.baidu_notify([article.get_full_url()])
-
+
+ # 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
- u = gravatar_url('liangliangyy@gmail.com')
- u = gravatar('liangliangyy@gmail.com')
-
+ u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL
+ u = gravatar('liangliangyy@gmail.com') # 生成头像HTML
+
+ # 测试友情链接页面
link = Links(
sequence=1,
name="lylinux",
@@ -136,57 +181,75 @@ class ArticleTest(TestCase):
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
-
+
+ # 测试RSS订阅和站点地图
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
-
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
-
+
+ # 测试admin操作(删除文章、访问日志)
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
+ #ymq:测试分页功能的辅助方法
for page in range(1, p.num_pages + 1):
+ # 调用分页模板标签获取分页信息
s = load_pagination_info(p.page(page), type, value)
- self.assertIsNotNone(s)
+ self.assertIsNotNone(s) # 断言分页信息非空
+ # 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
+ # 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
+ #ymq:测试图片上传功能
import requests
+ # 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
- imagepath = os.path.join(settings.BASE_DIR, 'python.png')
+ imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 保存路径
with open(imagepath, 'wb') as file:
- file.write(rsp.content)
+ file.write(rsp.content) # 保存图片
+
+ # 测试未授权上传
rsp = self.client.post('/upload')
- self.assertEqual(rsp.status_code, 403)
+ self.assertEqual(rsp.status_code, 403) # 断言被拒绝
+
+ # 生成上传签名(模拟授权)
sign = get_sha256(get_sha256(settings.SECRET_KEY))
+ # 模拟带签名的上传请求
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
- self.assertEqual(rsp.status_code, 200)
- os.remove(imagepath)
+ self.assertEqual(rsp.status_code, 200) # 断言上传成功
+
+ os.remove(imagepath) # 清理测试文件
+
+ # 测试用户头像保存和邮件发送工具函数
from djangoblog.utils import save_user_avatar, send_email
- send_email(['qq@qq.com'], 'testTitle', 'testContent')
+ send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件
save_user_avatar(
- 'https://www.python.org/static/img/python-logo.png')
+ 'https://www.python.org/static/img/python-logo.png') # 测试保存头像
def test_errorpage(self):
- rsp = self.client.get('/eee')
- self.assertEqual(rsp.status_code, 404)
+ #ymq:测试错误页面(404)
+ rsp = self.client.get('/eee') # 访问不存在的URL
+ self.assertEqual(rsp.status_code, 404) # 断言返回404
def test_commands(self):
+ #ymq:测试Django管理命令
+ # 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@@ -194,13 +257,15 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
-
+
+ # 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
-
+
+ # 创建OAuth用户关联
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@@ -211,7 +276,8 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
-
+
+ # 创建另一个OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@@ -221,12 +287,15 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
-
+
+ # 测试Elasticsearch索引构建命令(如果启用)
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
- call_command("ping_baidu", "all")
- call_command("create_testdata")
- call_command("clear_cache")
- call_command("sync_user_avatar")
- call_command("build_search_words")
+
+ # 测试其他管理命令
+ call_command("ping_baidu", "all") # 百度ping通知
+ call_command("create_testdata") # 创建测试数据
+ call_command("clear_cache") # 清理缓存
+ call_command("sync_user_avatar") # 同步用户头像
+ call_command("build_search_words") # 构建搜索词
\ No newline at end of file
diff --git a/src/django-master/blog/urls.py b/src/django-master/blog/urls.py
index adf2703..966e0d4 100644
--- a/src/django-master/blog/urls.py
+++ b/src/django-master/blog/urls.py
@@ -1,62 +1,92 @@
from django.urls import path
+#ymq:导入Django的path函数,用于定义URL路由
from django.views.decorators.cache import cache_page
+#ymq:导入缓存装饰器,用于对视图进行缓存
from . import views
+#ymq:从当前应用导入views模块,引用视图函数/类
app_name = "blog"
+#ymq:定义应用命名空间,避免URL名称冲突
+
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
+ #ymq:首页URL,映射到IndexView视图类,名称为'index'
+
path(
r'page//',
views.IndexView.as_view(),
name='index_page'),
+ #ymq:分页首页URL,接收整数类型的page参数,名称为'index_page'
+
path(
r'article////.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
+ #ymq:文章详情页URL,接收年、月、日、文章ID参数,名称为'detailbyid'
+
path(
r'category/.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
+ #ymq:分类详情页URL,接收slug类型的分类名称参数,名称为'category_detail'
+
path(
r'category//.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
+ #ymq:分类分页详情页URL,接收分类名称和页码参数,名称为'category_detail_page'
+
path(
r'author/.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
+ #ymq:作者详情页URL,接收作者名称参数,名称为'author_detail'
+
path(
r'author//.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
+ #ymq:作者分页详情页URL,接收作者名称和页码参数,名称为'author_detail_page'
+
path(
r'tag/.html',
views.TagDetailView.as_view(),
name='tag_detail'),
+ #ymq:标签详情页URL,接收slug类型的标签名称参数,名称为'tag_detail'
+
path(
r'tag//.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
+ #ymq:标签分页详情页URL,接收标签名称和页码参数,名称为'tag_detail_page'
+
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
+ #ymq:归档页面URL,使用cache_page装饰器缓存1小时(60*60秒),名称为'archives'
+
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
+ #ymq:友情链接页面URL,映射到LinkListView视图类,名称为'links'
+
path(
r'upload',
views.fileupload,
name='upload'),
+ #ymq:文件上传URL,映射到fileupload视图函数,名称为'upload'
+
path(
r'clean',
views.clean_cache_view,
name='clean'),
-]
+ #ymq:清理缓存URL,映射到clean_cache_view视图函数,名称为'clean'
+]
\ No newline at end of file
diff --git a/src/django-master/blog/views.py b/src/django-master/blog/views.py
index d5dc7ec..01f69af 100644
--- a/src/django-master/blog/views.py
+++ b/src/django-master/blog/views.py
@@ -1,6 +1,7 @@
import logging
import os
import uuid
+#ymq:导入日志、文件操作、UUID生成相关模块
from django.conf import settings
from django.core.paginator import Paginator
@@ -14,17 +15,24 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
+#ymq:导入Django核心组件、视图类、HTTP响应类等
from blog.models import Article, Category, LinkShowType, Links, Tag
+#ymq:从blog应用导入模型类
from comments.forms import CommentForm
+#ymq:从comments应用导入评论表单
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+#ymq:导入插件钩子相关模块,用于扩展文章功能
from djangoblog.utils import cache, get_blog_setting, get_sha256
+#ymq:导入工具函数,用于缓存、获取博客设置和加密
logger = logging.getLogger(__name__)
+#ymq:创建当前模块的日志记录器实例
class ArticleListView(ListView):
+ #ymq:文章列表基础视图类,继承自Django的ListView
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@@ -33,15 +41,17 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
- paginate_by = settings.PAGINATE_BY
- page_kwarg = 'page'
- link_type = LinkShowType.L
+ paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取
+ page_kwarg = 'page' # 页码参数名
+ link_type = LinkShowType.L # 链接展示类型
def get_view_cache_key(self):
+ #ymq:获取视图缓存键(未实际使用,预留方法)
return self.request.get['pages']
@property
def page_number(self):
+ #ymq:获取当前页码(从URL参数或默认值)
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@@ -51,13 +61,13 @@ class ArticleListView(ListView):
"""
子类重写.获得queryset的缓存key
"""
- raise NotImplementedError()
+ raise NotImplementedError() # 强制子类实现该方法
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
- raise NotImplementedError()
+ raise NotImplementedError() # 强制子类实现该方法
def get_queryset_from_cache(self, cache_key):
'''
@@ -70,8 +80,8 @@ class ArticleListView(ListView):
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
- article_list = self.get_queryset_data()
- cache.set(cache_key, article_list)
+ article_list = self.get_queryset_data() # 调用子类实现的方法获取数据
+ cache.set(cache_key, article_list) # 存入缓存
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
@@ -80,46 +90,53 @@ class ArticleListView(ListView):
重写默认,从缓存获取数据
:return:
'''
- key = self.get_queryset_cache_key()
- value = self.get_queryset_from_cache(key)
+ key = self.get_queryset_cache_key() # 获取缓存键
+ value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value
def get_context_data(self, **kwargs):
+ #ymq:扩展上下文数据,添加链接类型
kwargs['linktype'] = self.link_type
- return super(ArticleListView, self).get_context_data(**kwargs)
+ return super(ArticleListView, self).get_context_data(** kwargs)
class IndexView(ArticleListView):
'''
- 首页
+ 首页视图
'''
- # 友情链接类型
+ # 友情链接类型:首页展示
link_type = LinkShowType.I
def get_queryset_data(self):
+ #ymq:获取首页文章列表(已发布的文章)
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
+ #ymq:生成首页缓存键(包含页码)
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
- 文章详情页面
+ 文章详情页面视图
'''
- template_name = 'blog/article_detail.html'
- model = Article
- pk_url_kwarg = 'article_id'
- context_object_name = "article"
+ template_name = 'blog/article_detail.html' # 详情页模板
+ model = Article # 关联模型
+ pk_url_kwarg = 'article_id' # URL中主键参数名
+ context_object_name = "article" # 模板中上下文变量名
def get_context_data(self, **kwargs):
- comment_form = CommentForm()
+ #ymq:扩展文章详情页的上下文数据
+ comment_form = CommentForm() # 初始化评论表单
+ # 获取文章评论列表
article_comments = self.object.comment_list()
- parent_comments = article_comments.filter(parent_comment=None)
- blog_setting = get_blog_setting()
+ parent_comments = article_comments.filter(parent_comment=None) # 过滤顶级评论
+ blog_setting = get_blog_setting() # 获取博客设置
+
+ # 评论分页处理
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
@@ -135,26 +152,32 @@ class ArticleDetailView(DetailView):
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
+ # 生成评论分页链接
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
+
+ # 向上下文添加数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
+ # 上一篇/下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
+ # 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
- # Action Hook, 通知插件"文章详情已获取"
+
+ # 触发插件钩子:文章详情已获取
hooks.run_action('after_article_body_get', article=article, request=self.request)
- # # Filter Hook, 允许插件修改文章正文
+ # 应用插件过滤器:修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
@@ -163,23 +186,27 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):
'''
- 分类目录列表
+ 分类目录列表视图
'''
- page_type = "分类目录归档"
+ page_type = "分类目录归档" # 页面类型标识
def get_queryset_data(self):
- slug = self.kwargs['category_name']
- category = get_object_or_404(Category, slug=slug)
+ #ymq:获取指定分类下的文章列表
+ slug = self.kwargs['category_name'] # 从URL获取分类别名
+ category = get_object_or_404(Category, slug=slug) # 获取分类对象
categoryname = category.name
self.categoryname = categoryname
+ # 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
+ # 查询属于当前分类及子分类的已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
+ #ymq:生成分类列表缓存键
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@@ -189,59 +216,65 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
-
+ #ymq:扩展分类页上下文数据
categoryname = self.categoryname
try:
- categoryname = categoryname.split('/')[-1]
+ categoryname = categoryname.split('/')[-1] # 处理多级分类名称
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
- return super(CategoryDetailView, self).get_context_data(**kwargs)
+ return super(CategoryDetailView, self).get_context_data(** kwargs)
class AuthorDetailView(ArticleListView):
'''
- 作者详情页
+ 作者详情页视图
'''
- page_type = '作者文章归档'
+ page_type = '作者文章归档' # 页面类型标识
def get_queryset_cache_key(self):
+ #ymq:生成作者文章列表缓存键
from uuslug import slugify
- author_name = slugify(self.kwargs['author_name'])
+ author_name = slugify(self.kwargs['author_name']) # 作者名转slug
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
+ #ymq:获取指定作者的文章列表
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
- author__username=author_name, type='a', status='p')
+ author__username=author_name, type='a', status='p') # 过滤已发布的文章
return article_list
def get_context_data(self, **kwargs):
+ #ymq:扩展作者页上下文数据
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
- return super(AuthorDetailView, self).get_context_data(**kwargs)
+ return super(AuthorDetailView, self).get_context_data(** kwargs)
class TagDetailView(ArticleListView):
'''
- 标签列表页面
+ 标签列表页面视图
'''
- page_type = '分类标签归档'
+ page_type = '分类标签归档' # 页面类型标识
def get_queryset_data(self):
- slug = self.kwargs['tag_name']
- tag = get_object_or_404(Tag, slug=slug)
+ #ymq:获取指定标签的文章列表
+ slug = self.kwargs['tag_name'] # 从URL获取标签别名
+ tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
tag_name = tag.name
self.name = tag_name
+ # 查询包含当前标签的已发布文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
+ #ymq:生成标签文章列表缓存键
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@@ -251,101 +284,118 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
- # tag_name = self.kwargs['tag_name']
+ #ymq:扩展标签页上下文数据
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
- return super(TagDetailView, self).get_context_data(**kwargs)
+ return super(TagDetailView, self).get_context_data(** kwargs)
class ArchivesView(ArticleListView):
'''
- 文章归档页面
+ 文章归档页面视图
'''
- page_type = '文章归档'
- paginate_by = None
- page_kwarg = None
- template_name = 'blog/article_archives.html'
+ page_type = '文章归档' # 页面类型标识
+ paginate_by = None # 不分页
+ page_kwarg = None # 无页码参数
+ template_name = 'blog/article_archives.html' # 归档页模板
def get_queryset_data(self):
+ #ymq:获取所有已发布文章(用于归档)
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
+ #ymq:生成归档页缓存键
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
- model = Links
- template_name = 'blog/links_list.html'
+ #ymq:友情链接列表视图
+ model = Links # 关联模型
+ template_name = 'blog/links_list.html' # 链接列表模板
def get_queryset(self):
+ #ymq:只获取启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
+ #ymq:Elasticsearch搜索视图,继承自Haystack的SearchView
def get_context(self):
- paginator, page = self.build_page()
+ #ymq:构建搜索结果页面的上下文数据
+ paginator, page = self.build_page() # 处理分页
context = {
- "query": self.query,
- "form": self.form,
- "page": page,
- "paginator": paginator,
- "suggestion": None,
+ "query": self.query, # 搜索关键词
+ "form": self.form, # 搜索表单
+ "page": page, # 当前页数据
+ "paginator": paginator, # 分页器
+ "suggestion": None, # 搜索建议(默认无)
}
+ # 如果启用拼写建议,添加建议内容
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
- context.update(self.extra_context())
+ context.update(self.extra_context()) # 添加额外上下文
return context
-@csrf_exempt
+@csrf_exempt # 禁用CSRF保护(用于外部调用)
def fileupload(request):
"""
- 该方法需自己写调用端来上传图片,该方法仅提供图床功能
+ 图片/文件上传接口,需验证签名
:param request:
:return:
"""
if request.method == 'POST':
- sign = request.GET.get('sign', None)
+ sign = request.GET.get('sign', None) # 获取签名参数
if not sign:
- return HttpResponseForbidden()
+ return HttpResponseForbidden() # 无签名则拒绝
+ # 验证签名(双重SHA256加密对比)
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
- return HttpResponseForbidden()
- response = []
+ return HttpResponseForbidden() # 签名错误则拒绝
+
+ response = [] # 存储上传后的文件URL
for filename in request.FILES:
- timestr = timezone.now().strftime('%Y/%m/%d')
- imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
+ timestr = timezone.now().strftime('%Y/%m/%d') # 按日期组织文件
+ imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename))
+ # 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
+ # 确定存储目录(图片/文件分开存储)
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
- os.makedirs(base_dir)
+ os.makedirs(base_dir) # 目录不存在则创建
+ # 生成唯一文件名(UUID+原扩展名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
+ # 安全校验:防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
+ # 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
+ # 图片压缩处理
if isimage:
from PIL import Image
image = Image.open(savepath)
- image.save(savepath, quality=20, optimize=True)
+ image.save(savepath, quality=20, optimize=True) # 压缩质量为20
+ # 生成文件访问URL
url = static(savepath)
response.append(url)
- return HttpResponse(response)
+ return HttpResponse(response) # 返回所有上传文件的URL
else:
- return HttpResponse("only for post")
+ return HttpResponse("only for post") # 只允许POST方法
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
+ #ymq:404错误处理视图
if exception:
- logger.error(exception)
+ logger.error(exception) # 记录错误日志
url = request.get_full_path()
return render(request,
template_name,
@@ -355,6 +405,7 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
+ #ymq:500错误处理视图
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@@ -366,8 +417,9 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
+ #ymq:403错误处理视图
if exception:
- logger.error(exception)
+ logger.error(exception) # 记录错误日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
@@ -375,5 +427,6 @@ def permission_denied_view(
def clean_cache_view(request):
- cache.clear()
- return HttpResponse('ok')
+ #ymq:清理缓存的视图
+ cache.clear() # 清除所有缓存
+ return HttpResponse('ok') # 返回成功响应
\ No newline at end of file