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