From 01f9792c1c9cbaf792d0998c5f61f0c23069d913 Mon Sep 17 00:00:00 2001 From: bu661 Date: Sat, 8 Nov 2025 22:37:29 +0800 Subject: [PATCH] =?UTF-8?q?blog=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DjangoBlog/blog/admin.py | 56 +++++- src/DjangoBlog/blog/apps.py | 3 + src/DjangoBlog/blog/context_processors.py | 37 +++- src/DjangoBlog/blog/documents.py | 75 ++++++++ src/DjangoBlog/blog/forms.py | 13 ++ .../blog/management/commands/build_index.py | 17 +- .../management/commands/build_search_words.py | 7 + .../blog/management/commands/clear_cache.py | 7 + .../management/commands/create_testdata.py | 25 +++ .../blog/management/commands/ping_baidu.py | 21 +++ .../management/commands/sync_user_avatar.py | 28 +++ src/DjangoBlog/blog/middleware.py | 27 ++- .../blog/migrations/0001_initial.py | 81 ++++++++- ...002_blogsettings_global_footer_and_more.py | 14 +- .../0003_blogsettings_comment_need_review.py | 10 +- ...de_blogsettings_analytics_code_and_more.py | 18 +- ...options_alter_category_options_and_more.py | 69 ++++++- .../0006_alter_blogsettings_options.py | 9 +- src/DjangoBlog/blog/models.py | 117 ++++++++++++ src/DjangoBlog/blog/search_indexes.py | 8 + .../blog/static/account/css/account.css | 10 +- .../blog/static/account/js/account.js | 31 ++++ .../assets/js/ie-emulation-modes-warning.js | 26 ++- .../assets/js/ie10-viewport-bug-workaround.js | 12 +- src/DjangoBlog/blog/static/blog/css/ie.css | 66 ++++++- .../blog/static/blog/css/nprogress.css | 21 ++- .../blog/static/blog/css/oauth_style.css | 77 +++++++- src/DjangoBlog/blog/static/blog/js/blog.js | 49 ++++- .../blog/static/blog/js/mathjax-loader.js | 109 +++++++---- .../blog/static/blog/js/navigation.js | 30 ++- .../blog/static/blog/js/nprogress.js | 172 +++++++++++++----- src/DjangoBlog/blog/tests.py | 67 ++++++- src/DjangoBlog/blog/urls.py | 17 ++ src/DjangoBlog/blog/views.py | 99 ++++++++-- 34 files changed, 1282 insertions(+), 146 deletions(-) diff --git a/src/DjangoBlog/blog/admin.py b/src/DjangoBlog/blog/admin.py index 69d7f8e..6999ffa 100644 --- a/src/DjangoBlog/blog/admin.py +++ b/src/DjangoBlog/blog/admin.py @@ -1,3 +1,4 @@ +# bjy: 从Django中导入所需的模块和类 from django import forms from django.contrib import admin from django.contrib.auth import get_user_model @@ -5,110 +6,161 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -# Register your models here. +# bjy: 注册你的模型到这里。 from .models import Article, Category, Tag, Links, SideBar, BlogSettings +# bjy: 为Article模型创建一个自定义的ModelForm class ArticleForm(forms.ModelForm): + # bjy: 示例:如果使用Pagedown编辑器,可以取消下面这行的注释 # body = forms.CharField(widget=AdminPagedownWidget()) class Meta: + # bjy: 指定这个表单对应的模型是Article model = Article + # bjy: 表示在表单中包含模型的所有字段 fields = '__all__' +# bjy: 定义一个admin动作,用于将选中的文章发布 def makr_article_publish(modeladmin, request, queryset): + # bjy: 批量更新查询集中所有文章的状态为'p'(已发布) queryset.update(status='p') +# bjy: 定义一个admin动作,用于将选中的文章设为草稿 def draft_article(modeladmin, request, queryset): + # bjy: 批量更新查询集中所有文章的状态为'd'(草稿) queryset.update(status='d') +# bjy: 定义一个admin动作,用于关闭选中文章的评论功能 def close_article_commentstatus(modeladmin, request, queryset): + # bjy: 批量更新查询集中所有文章的评论状态为'c'(关闭) queryset.update(comment_status='c') +# bjy: 定义一个admin动作,用于开启选中文章的评论功能 def open_article_commentstatus(modeladmin, request, queryset): + # bjy: 批量更新查询集中所有文章的评论状态为'o'(开启) queryset.update(comment_status='o') +# bjy: 为admin动作设置在后台显示的描述文本 makr_article_publish.short_description = _('Publish selected articles') draft_article.short_description = _('Draft selected articles') close_article_commentstatus.short_description = _('Close article comments') open_article_commentstatus.short_description = _('Open article comments') +# bjy: 为Article模型自定义Admin管理界面 class ArticlelAdmin(admin.ModelAdmin): + # bjy: 设置每页显示20条记录 list_per_page = 20 + # bjy: 启用搜索功能,搜索范围包括文章内容(body)和标题(title) search_fields = ('body', 'title') + # bjy: 指定使用的自定义表单 form = ArticleForm + # bjy: 在列表视图中显示的字段 list_display = ( 'id', 'title', 'author', - 'link_to_category', + 'link_to_category', # bjy: 自定义方法,显示指向分类的链接 'creation_time', 'views', 'status', 'type', 'article_order') + # bjy: 设置列表视图中可点击进入编辑页面的链接字段 list_display_links = ('id', 'title') + # bjy: 启用右侧筛选栏,可按状态、类型、分类进行筛选 list_filter = ('status', 'type', 'category') + # bjy: 启用日期层次导航,按创建时间进行分层 date_hierarchy = 'creation_time' + # bjy: 为多对多字段(tags)提供一个水平筛选的界面 filter_horizontal = ('tags',) + # bjy: 在编辑页面中排除的字段,这些字段将自动处理 exclude = ('creation_time', 'last_modify_time') + # bjy: 在列表页面显示“在站点上查看”的按钮 view_on_site = True + # bjy: 将自定义的admin动作添加到动作下拉列表中 actions = [ makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] + # bjy: 对于外键字段(author, category),显示为一个输入框用于输入ID,而不是下拉列表 raw_id_fields = ('author', 'category',) + # bjy: 自定义方法,用于在列表页面显示一个指向文章分类的链接 def link_to_category(self, obj): + # bjy: 获取分类模型的app_label和model_name,用于构建admin URL info = (obj.category._meta.app_label, obj.category._meta.model_name) + # bjy: 生成指向该分类编辑页面的URL link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + # bjy: 使用format_html安全地生成HTML链接 return format_html(u'%s' % (link, obj.category.name)) + # bjy: 设置该方法在列表页面列标题的显示文本 link_to_category.short_description = _('category') + # bjy: 重写get_form方法,用于动态修改表单 def get_form(self, request, obj=None, **kwargs): + # bjy: 获取父类的表单 form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + # bjy: 修改author字段的查询集,只显示超级用户 form.base_fields['author'].queryset = get_user_model( ).objects.filter(is_superuser=True) return form + # bjy: 重写save_model方法,在保存模型时执行额外操作 def save_model(self, request, obj, form, change): + # bjy: 调用父类的save_model方法执行默认保存操作 super(ArticlelAdmin, self).save_model(request, obj, form, change) + # bjy: 重写get_view_on_site_url方法,自定义“在站点上查看”的URL def get_view_on_site_url(self, obj=None): if obj: + # bjy: 如果对象存在,则调用模型的get_full_url方法获取URL url = obj.get_full_url() return url else: + # bjy: 如果对象不存在(例如在添加新对象时),则返回网站首页URL from djangoblog.utils import get_current_site site = get_current_site().domain return site +# bjy: 为Tag模型自定义Admin管理界面 class TagAdmin(admin.ModelAdmin): + # bjy: 在编辑页面中排除的字段 exclude = ('slug', 'last_mod_time', 'creation_time') +# bjy: 为Category模型自定义Admin管理界面 class CategoryAdmin(admin.ModelAdmin): + # bjy: 在列表视图中显示的字段 list_display = ('name', 'parent_category', 'index') + # bjy: 在编辑页面中排除的字段 exclude = ('slug', 'last_mod_time', 'creation_time') +# bjy: 为Links模型自定义Admin管理界面 class LinksAdmin(admin.ModelAdmin): + # bjy: 在编辑页面中排除的字段 exclude = ('last_mod_time', 'creation_time') +# bjy: 为SideBar模型自定义Admin管理界面 class SideBarAdmin(admin.ModelAdmin): + # bjy: 在列表视图中显示的字段 list_display = ('name', 'content', 'is_enable', 'sequence') + # bjy: 在编辑页面中排除的字段 exclude = ('last_mod_time', 'creation_time') +# bjy: 为BlogSettings模型自定义Admin管理界面 class BlogSettingsAdmin(admin.ModelAdmin): + # bjy: 使用默认配置,无需自定义 pass diff --git a/src/DjangoBlog/blog/apps.py b/src/DjangoBlog/blog/apps.py index 7930587..0198010 100644 --- a/src/DjangoBlog/blog/apps.py +++ b/src/DjangoBlog/blog/apps.py @@ -1,5 +1,8 @@ +# bjy: 从Django中导入AppConfig基类,用于配置应用程序 from django.apps import AppConfig +# bjy: 定义一个名为BlogConfig的配置类,它继承自AppConfig class BlogConfig(AppConfig): + # bjy: 指定这个配置类对应的应用程序名称(通常是Python包的路径) name = 'blog' diff --git a/src/DjangoBlog/blog/context_processors.py b/src/DjangoBlog/blog/context_processors.py index 73e3088..6f1d347 100644 --- a/src/DjangoBlog/blog/context_processors.py +++ b/src/DjangoBlog/blog/context_processors.py @@ -1,43 +1,76 @@ +# bjy: 导入日志模块 import logging +# bjy: 从Django中导入时区工具 from django.utils import timezone +# bjy: 从项目工具模块中导入缓存和获取博客设置的函数 from djangoblog.utils import cache, get_blog_setting +# bjy: 从当前应用的models中导入Category和Article模型 from .models import Category, Article +# bjy: 获取一个名为__name__的logger实例,用于记录日志 logger = logging.getLogger(__name__) +# bjy: 定义一个上下文处理器,用于在所有模板中注入全局变量 def seo_processor(requests): + # bjy: 定义一个缓存键名 key = 'seo_processor' + # bjy: 尝试从缓存中获取数据 value = cache.get(key) + # bjy: 如果缓存中存在数据,则直接返回 if value: return value else: + # bjy: 如果缓存中没有数据,则记录一条日志 logger.info('set processor cache.') + # bjy: 获取博客的设置对象 setting = get_blog_setting() + # bjy: 构建一个包含所有SEO和全局设置的字典 value = { + # bjy: 网站名称 'SITE_NAME': setting.site_name, + # bjy: 是否显示Google AdSense广告 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, + # bjy: Google AdSense的广告代码 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, + # bjy: 网站的SEO描述 'SITE_SEO_DESCRIPTION': setting.site_seo_description, + # bjy: 网站的普通描述 'SITE_DESCRIPTION': setting.site_description, + # bjy: 网站的关键词 'SITE_KEYWORDS': setting.site_keywords, + # bjy: 网站的完整基础URL(协议+域名) 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', + # bjy: 文章列表页的摘要长度 'ARTICLE_SUB_LENGTH': setting.article_sub_length, + # bjy: 用于导航栏的所有分类列表 'nav_category_list': Category.objects.all(), + # bjy: 用于导航栏的所有已发布的“页面”类型的文章 'nav_pages': Article.objects.filter( - type='p', - status='p'), + type='p', # bjy: 类型为'p'(page) + status='p'), # bjy: 状态为'p'(published) + # bjy: 是否开启全站评论功能 'OPEN_SITE_COMMENT': setting.open_site_comment, + # bjy: 网站的ICP备案号 'BEIAN_CODE': setting.beian_code, + # bjy: 网站统计代码(如Google Analytics) 'ANALYTICS_CODE': setting.analytics_code, + # bjy: 公安备案号 "BEIAN_CODE_GONGAN": setting.gongan_beiancode, + # bjy: 是否显示公安备案号 "SHOW_GONGAN_CODE": setting.show_gongan_code, + # bjy: 当前年份,用于页脚版权信息 "CURRENT_YEAR": timezone.now().year, + # bjy: 全局页头HTML代码 "GLOBAL_HEADER": setting.global_header, + # bjy: 全局页脚HTML代码 "GLOBAL_FOOTER": setting.global_footer, + # bjy: 评论是否需要审核 "COMMENT_NEED_REVIEW": setting.comment_need_review, } + # bjy: 将构建好的字典存入缓存,缓存时间为10小时(60*60*10秒) cache.set(key, value, 60 * 60 * 10) + # bjy: 返回这个字典,它将被注入到所有模板的上下文中 return value diff --git a/src/DjangoBlog/blog/documents.py b/src/DjangoBlog/blog/documents.py index 0f1db7b..f662223 100644 --- a/src/DjangoBlog/blog/documents.py +++ b/src/DjangoBlog/blog/documents.py @@ -1,26 +1,40 @@ +# bjy: 导入时间模块 import time +# bjy: 导入Elasticsearch的客户端模块和异常类 import elasticsearch.client +import elasticsearch.exceptions +# bjy: 导入Django的设置 from django.conf import settings +# bjy: 从elasticsearch_dsl中导入文档、内部文档、字段类型和连接管理器 from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean from elasticsearch_dsl.connections import connections +# bjy: 从blog应用中导入Article模型 from blog.models import Article +# bjy: 检查Django设置中是否配置了ELASTICSEARCH_DSL,以决定是否启用Elasticsearch功能 ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') +# bjy: 如果启用了Elasticsearch if ELASTICSEARCH_ENABLED: + # bjy: 根据Django设置创建到Elasticsearch的连接 connections.create_connection( hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + # bjy: 导入并实例化Elasticsearch客户端 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + # bjy: 导入并实例化Ingest客户端,用于管理管道 from elasticsearch.client import IngestClient c = IngestClient(es) + # bjy: 尝试获取名为'geoip'的管道 try: c.get_pipeline('geoip') + # bjy: 如果管道不存在,则创建它 except elasticsearch.exceptions.NotFoundError: + # bjy: 创建一个geoip管道,用于根据IP地址添加地理位置信息 c.put_pipeline('geoip', body='''{ "description" : "Add geoip info", "processors" : [ @@ -33,58 +47,90 @@ if ELASTICSEARCH_ENABLED: }''') +# bjy: 定义一个内部文档(InnerDoc)结构,用于存储IP地理位置信息 class GeoIp(InnerDoc): + # bjy: 大洲名称 continent_name = Keyword() + # bjy: 国家ISO代码 country_iso_code = Keyword() + # bjy: 国家名称 country_name = Keyword() + # bjy: 地理坐标(经纬度) location = GeoPoint() +# bjy: 定义内部文档,用于存储用户代理(User-Agent)中的浏览器信息 class UserAgentBrowser(InnerDoc): + # bjy: 浏览器家族(如Chrome, Firefox) Family = Keyword() + # bjy: 浏览器版本 Version = Keyword() +# bjy: 定义内部文档,用于存储用户代理中的操作系统信息 class UserAgentOS(UserAgentBrowser): + # bjy: 继承自UserAgentBrowser,结构相同 pass +# bjy: 定义内部文档,用于存储用户代理中的设备信息 class UserAgentDevice(InnerDoc): + # bjy: 设备家族(如iPhone, Android) Family = Keyword() + # bjy: 设备品牌(如Apple, Samsung) Brand = Keyword() + # bjy: 设备型号(如iPhone 12) Model = Keyword() +# bjy: 定义内部文档,用于存储完整的用户代理信息 class UserAgent(InnerDoc): + # bjy: 嵌套浏览器信息 browser = Object(UserAgentBrowser, required=False) + # bjy: 嵌套操作系统信息 os = Object(UserAgentOS, required=False) + # bjy: 嵌套设备信息 device = Object(UserAgentDevice, required=False) + # bjy: 原始User-Agent字符串 string = Text() + # bjy: 是否为爬虫或机器人 is_bot = Boolean() +# bjy: 定义一个Elasticsearch文档,用于存储页面性能数据(如响应时间) class ElapsedTimeDocument(Document): + # bjy: 请求的URL url = Keyword() + # bjy: 请求耗时(毫秒) time_taken = Long() + # bjy: 日志记录时间 log_datetime = Date() + # bjy: 客户端IP地址 ip = Keyword() + # bjy: 嵌套的IP地理位置信息 geoip = Object(GeoIp, required=False) + # bjy: 嵌套的用户代理信息 useragent = Object(UserAgent, required=False) class Index: + # bjy: 指定索引名称为'performance' name = 'performance' + # bjy: 设置索引的分片和副本数 settings = { "number_of_shards": 1, "number_of_replicas": 0 } class Meta: + # bjy: 指定文档类型 doc_type = 'ElapsedTime' +# bjy: 定义一个管理类,用于操作ElapsedTimeDocument索引 class ElaspedTimeDocumentManager: @staticmethod def build_index(): + # bjy: 如果索引不存在,则创建它 from elasticsearch import Elasticsearch client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) res = client.indices.exists(index="performance") @@ -93,13 +139,16 @@ class ElaspedTimeDocumentManager: @staticmethod def delete_index(): + # bjy: 删除'performance'索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es.indices.delete(index='performance', ignore=[400, 404]) @staticmethod def create(url, time_taken, log_datetime, useragent, ip): + # bjy: 确保索引存在 ElaspedTimeDocumentManager.build_index() + # bjy: 构建UserAgent内部文档对象 ua = UserAgent() ua.browser = UserAgentBrowser() ua.browser.Family = useragent.browser.family @@ -116,8 +165,10 @@ class ElaspedTimeDocumentManager: ua.string = useragent.ua_string ua.is_bot = useragent.is_bot + # bjy: 创建ElapsedTimeDocument文档实例 doc = ElapsedTimeDocument( meta={ + # bjy: 使用当前时间的毫秒数作为文档ID 'id': int( round( time.time() * @@ -127,57 +178,78 @@ class ElaspedTimeDocumentManager: time_taken=time_taken, log_datetime=log_datetime, useragent=ua, ip=ip) + # bjy: 保存文档,并使用'geoip'管道处理IP地址 doc.save(pipeline="geoip") +# bjy: 定义一个Elasticsearch文档,用于存储博客文章数据,以支持全文搜索 class ArticleDocument(Document): + # bjy: 文章内容,使用ik分词器进行索引和搜索 body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + # bjy: 文章标题,使用ik分词器 title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + # bjy: 作者信息,为一个对象类型 author = Object(properties={ 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) + # bjy: 分类信息,为一个对象类型 category = Object(properties={ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) + # bjy: 标签信息,为一个对象类型 tags = Object(properties={ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) + # bjy: 发布时间 pub_time = Date() + # bjy: 文章状态 status = Text() + # bjy: 评论状态 comment_status = Text() + # bjy: 文章类型 type = Text() + # bjy: 浏览量 views = Integer() + # bjy: 文章排序权重 article_order = Integer() class Index: + # bjy: 指定索引名称为'blog' name = 'blog' + # bjy: 设置索引的分片和副本数 settings = { "number_of_shards": 1, "number_of_replicas": 0 } class Meta: + # bjy: 指定文档类型 doc_type = 'Article' +# bjy: 定义一个管理类,用于操作ArticleDocument索引 class ArticleDocumentManager(): def __init__(self): + # bjy: 初始化时创建索引 self.create_index() def create_index(self): + # bjy: 创建'blog'索引 ArticleDocument.init() def delete_index(self): + # bjy: 删除'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): + # bjy: 将Django的Article查询集转换为ArticleDocument对象列表 return [ ArticleDocument( meta={ @@ -202,12 +274,15 @@ class ArticleDocumentManager(): article_order=article.article_order) for article in articles] def rebuild(self, articles=None): + # bjy: 重建索引。如果未提供articles,则使用所有文章 ArticleDocument.init() articles = articles if articles else Article.objects.all() docs = self.convert_to_doc(articles) + # bjy: 遍历并保存每个文档 for doc in docs: doc.save() def update_docs(self, docs): + # bjy: 更新一组文档 for doc in docs: doc.save() diff --git a/src/DjangoBlog/blog/forms.py b/src/DjangoBlog/blog/forms.py index 715be76..283fcf9 100644 --- a/src/DjangoBlog/blog/forms.py +++ b/src/DjangoBlog/blog/forms.py @@ -1,19 +1,32 @@ +# bjy: 导入日志模块 import logging +# bjy: 从Django中导入表单模块 from django import forms +# bjy: 从haystack(一个Django搜索框架)中导入基础搜索表单 from haystack.forms import SearchForm +# bjy: 获取一个名为__name__的logger实例,用于记录日志 logger = logging.getLogger(__name__) +# bjy: 定义一个自定义的博客搜索表单,继承自Haystack的SearchForm class BlogSearchForm(SearchForm): + # bjy: 定义一个名为querydata的字符字段,用于接收用户输入的搜索关键词,并设置为必填 querydata = forms.CharField(required=True) + # bjy: 重写search方法,用于执行搜索逻辑 def search(self): + # bjy: 调用父类的search方法,执行默认的搜索并返回结果集 datas = super(BlogSearchForm, self).search() + # bjy: 检查表单数据是否有效 if not self.is_valid(): + # bjy: 如果表单无效,则调用no_query_found方法(通常返回一个空的结果集) return self.no_query_found() + # bjy: 如果用户在querydata字段中输入了内容 if self.cleaned_data['querydata']: + # bjy: 将用户输入的搜索关键词记录到日志中 logger.info(self.cleaned_data['querydata']) + # bjy: 返回搜索结果 return datas diff --git a/src/DjangoBlog/blog/management/commands/build_index.py b/src/DjangoBlog/blog/management/commands/build_index.py index 3c4acd7..96a88f8 100644 --- a/src/DjangoBlog/blog/management/commands/build_index.py +++ b/src/DjangoBlog/blog/management/commands/build_index.py @@ -1,18 +1,23 @@ -from django.core.management.base import BaseCommand - -from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ - ELASTICSEARCH_ENABLED - - # TODO 参数化 +# bjy: 定义一个继承自BaseCommand的命令类,用于执行构建搜索索引的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索索引" help = 'build search index' + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 检查Elasticsearch功能是否已启用,确保在启用状态下才执行索引操作 if ELASTICSEARCH_ENABLED: + # bjy: 调用ElapsedTimeDocumentManager的类方法,构建用于记录耗时的文档索引 ElaspedTimeDocumentManager.build_index() + + # bjy: 创建ElapsedTimeDocument的实例,并调用其init方法进行初始化(可能是数据同步或设置) manager = ElapsedTimeDocument() manager.init() + + # bjy: 创建ArticleDocumentManager的实例,用于管理文章的搜索索引 manager = ArticleDocumentManager() + # bjy: 删除现有的文章索引,为重建做准备,防止旧数据冲突 manager.delete_index() + # bjy: 重新构建文章索引,将数据库中的最新文章数据同步到Elasticsearch manager.rebuild() diff --git a/src/DjangoBlog/blog/management/commands/build_search_words.py b/src/DjangoBlog/blog/management/commands/build_search_words.py index cfe7e0d..359a75a 100644 --- a/src/DjangoBlog/blog/management/commands/build_search_words.py +++ b/src/DjangoBlog/blog/management/commands/build_search_words.py @@ -1,13 +1,20 @@ +# bjy: 从Django核心管理模块导入BaseCommand基类,用于创建自定义管理命令 from django.core.management.base import BaseCommand +# bjy: 从当前应用的models模块导入Tag和Category模型,用于获取数据 from blog.models import Tag, Category # TODO 参数化 +# bjy: 定义一个继承自BaseCommand的命令类,用于执行构建搜索词的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索词" help = 'build search words' + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 使用集合推导式获取所有Tag和Category的name字段,并自动去重 datas = set([t.name for t in Tag.objects.all()] + [t.name for t in Category.objects.all()]) + # bjy: 将去重后的搜索词集合中的每个元素用换行符连接,并打印到标准输出 print('\n'.join(datas)) diff --git a/src/DjangoBlog/blog/management/commands/clear_cache.py b/src/DjangoBlog/blog/management/commands/clear_cache.py index 0d66172..20bf8e5 100644 --- a/src/DjangoBlog/blog/management/commands/clear_cache.py +++ b/src/DjangoBlog/blog/management/commands/clear_cache.py @@ -1,11 +1,18 @@ +# bjy: 从Django核心管理模块导入BaseCommand基类,用于创建自定义管理命令 from django.core.management.base import BaseCommand +# bjy: 从项目工具模块导入cache实例,用于操作缓存 from djangoblog.utils import cache +# bjy: 定义一个继承自BaseCommand的命令类,用于执行清空缓存的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"清空所有缓存" help = 'clear the whole cache' + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 调用cache实例的clear方法,清空所有缓存 cache.clear() + # bjy: 使用成功样式向标准输出写入操作成功的信息,并附带换行符 self.stdout.write(self.style.SUCCESS('Cleared cache\n')) diff --git a/src/DjangoBlog/blog/management/commands/create_testdata.py b/src/DjangoBlog/blog/management/commands/create_testdata.py index 675d2ba..89d4204 100644 --- a/src/DjangoBlog/blog/management/commands/create_testdata.py +++ b/src/DjangoBlog/blog/management/commands/create_testdata.py @@ -1,40 +1,65 @@ +# bjy: 从Django的auth模块导入get_user_model函数,用于动态获取当前项目激活的用户模型 from django.contrib.auth import get_user_model +# bjy: 从Django的auth模块导入make_password函数,用于创建加密后的密码哈希 from django.contrib.auth.hashers import make_password +# bjy: 从Django核心管理模块导入BaseCommand基类,用于创建自定义管理命令 from django.core.management.base import BaseCommand +# bjy: 从当前应用的models模块导入Article, Tag, Category模型,用于创建测试数据 from blog.models import Article, Tag, Category +# bjy: 定义一个继承自BaseCommand的命令类,用于执行创建测试数据的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"创建测试数据" help = 'create test datas' + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 获取或创建一个测试用户,如果不存在则创建,密码已加密 user = get_user_model().objects.get_or_create( email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + # bjy: 获取或创建一个父级分类,parent_category为None表示它是顶级分类 pcategory = Category.objects.get_or_create( name='我是父类目', parent_category=None)[0] + # bjy: 获取或创建一个子分类,并设置其父分类为上面创建的父级分类 category = Category.objects.get_or_create( name='子类目', parent_category=pcategory)[0] + # bjy: 显式保存子分类实例,确保数据已写入数据库(虽然get_or_create通常会保存) category.save() + + # bjy: 创建一个基础标签,所有文章都将共用此标签 basetag = Tag() basetag.name = "标签" basetag.save() + + # bjy: 循环19次,创建19篇测试文章和对应的标签 for i in range(1, 20): + # bjy: 获取或创建一篇文章,关联到上面创建的分类、用户,并设置标题和内容 article = Article.objects.get_or_create( category=category, title='nice title ' + str(i), body='nice content ' + str(i), author=user)[0] + + # bjy: 为每篇文章创建一个专属标签 tag = Tag() tag.name = "标签" + str(i) tag.save() + + # bjy: 将专属标签和基础标签都添加到当前文章的标签集合中 article.tags.add(tag) article.tags.add(basetag) + + # bjy: 保存文章,使标签关联生效 article.save() + # bjy: 导入项目的cache工具,用于清理缓存 from djangoblog.utils import cache + # bjy: 清空所有缓存,以确保新创建的数据能被正确加载 cache.clear() + # bjy: 使用成功样式向标准输出写入操作完成的信息 self.stdout.write(self.style.SUCCESS('created test datas \n')) diff --git a/src/DjangoBlog/blog/management/commands/ping_baidu.py b/src/DjangoBlog/blog/management/commands/ping_baidu.py index 2c7fbdd..dc01909 100644 --- a/src/DjangoBlog/blog/management/commands/ping_baidu.py +++ b/src/DjangoBlog/blog/management/commands/ping_baidu.py @@ -1,16 +1,25 @@ +# bjy: 从Django核心管理模块导入BaseCommand基类,用于创建自定义管理命令 from django.core.management.base import BaseCommand +# bjy: 从项目工具模块导入SpiderNotify类,用于通知搜索引擎抓取新内容 from djangoblog.spider_notify import SpiderNotify +# bjy: 从项目工具模块导入get_current_site函数,用于获取当前站点域名等信息 from djangoblog.utils import get_current_site +# bjy: 从当前应用的models模块导入Article, Tag, Category模型,用于获取待通知的URL from blog.models import Article, Tag, Category +# bjy: 获取当前站点的域名,用于拼接完整URL site = get_current_site().domain +# bjy: 定义一个继承自BaseCommand的命令类,用于执行通知百度抓取URL的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"通知百度URL" help = 'notify baidu url' + # bjy: 为命令添加参数,允许用户指定通知的数据类型 def add_arguments(self, parser): + # bjy: 添加一个名为data_type的位置参数,类型为字符串,且只能从给定的选项中选择 parser.add_argument( 'data_type', type=str, @@ -21,30 +30,42 @@ class Command(BaseCommand): 'category'], help='article : all article,tag : all tag,category: all category,all: All of these') + # bjy: 定义一个辅助方法,用于根据路径拼接完整的URL def get_full_url(self, path): + # bjy: 使用https协议和当前站点域名拼接完整URL url = "https://{site}{path}".format(site=site, path=path) return url + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 获取用户指定的data_type参数,决定通知哪些类型的URL type = options['data_type'] + # bjy: 输出开始获取指定类型URL的信息 self.stdout.write('start get %s' % type) + # bjy: 初始化一个空列表,用于收集所有待通知的URL urls = [] + # bjy: 如果类型为article或all,则收集所有已发布文章的完整URL if type == 'article' or type == 'all': for article in Article.objects.filter(status='p'): urls.append(article.get_full_url()) + # bjy: 如果类型为tag或all,则收集所有标签的完整URL if type == 'tag' or type == 'all': for tag in Tag.objects.all(): url = tag.get_absolute_url() urls.append(self.get_full_url(url)) + # bjy: 如果类型为category或all,则收集所有分类的完整URL if type == 'category' or type == 'all': for category in Category.objects.all(): url = category.get_absolute_url() urls.append(self.get_full_url(url)) + # bjy: 输出开始通知URL的数量信息,使用成功样式 self.stdout.write( self.style.SUCCESS( 'start notify %d urls' % len(urls))) + # bjy: 调用SpiderNotify的百度通知方法,将收集到的URL发送给百度 SpiderNotify.baidu_notify(urls) + # bjy: 输出通知完成的信息,使用成功样式 self.stdout.write(self.style.SUCCESS('finish notify')) diff --git a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py index d0f4612..30ee868 100644 --- a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py +++ b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py @@ -1,47 +1,75 @@ +# bjy: 导入requests库,用于发起HTTP请求,检测头像URL是否可访问 import requests +# bjy: 从Django核心管理模块导入BaseCommand基类,用于创建自定义管理命令 from django.core.management.base import BaseCommand +# bjy: 从Django模板标签模块导入static函数,用于生成静态文件的URL from django.templatetags.static import static +# bjy: 从项目工具模块导入save_user_avatar函数,用于保存用户头像到本地 from djangoblog.utils import save_user_avatar +# bjy: 从oauth应用导入OAuthUser模型,用于获取所有OAuth用户数据 from oauth.models import OAuthUser +# bjy: 从oauth应用导入get_manager_by_type函数,用于根据OAuth类型获取对应的管理器 from oauth.oauthmanager import get_manager_by_type +# bjy: 定义一个继承自BaseCommand的命令类,用于执行同步用户头像的任务 class Command(BaseCommand): + # bjy: 设置命令的帮助信息,描述该命令的功能是"同步用户头像" help = 'sync user avatar' + # bjy: 定义一个辅助方法,用于测试给定的URL是否可访问(返回200状态码) def test_picture(self, url): try: + # bjy: 尝试GET请求,设置2秒超时,如果状态码为200则返回True if requests.get(url, timeout=2).status_code == 200: return True except: + # bjy: 任何异常都视为不可访问,静默忽略 pass + # bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行 def handle(self, *args, **options): + # bjy: 获取项目静态文件的基础URL,用于判断头像是否为本地静态文件 static_url = static("../") + # bjy: 获取所有OAuth用户 users = OAuthUser.objects.all() + # bjy: 输出开始同步用户头像的总数信息 self.stdout.write(f'开始同步{len(users)}个用户头像') + # bjy: 遍历每个用户,进行头像同步 for u in users: + # bjy: 输出当前正在同步的用户昵称 self.stdout.write(f'开始同步:{u.nickname}') + # bjy: 获取用户当前的头像URL url = u.picture + # bjy: 如果头像URL不为空,则执行同步逻辑 if url: + # bjy: 如果当前头像URL是本地静态文件路径 if url.startswith(static_url): + # bjy: 测试该静态文件是否可访问,若可访问则跳过此用户 if self.test_picture(url): continue else: + # bjy: 如果不可访问,且用户有metadata信息,则尝试通过OAuth管理器重新获取头像URL并保存 if u.metadata: manage = get_manager_by_type(u.type) url = manage.get_picture(u.metadata) url = save_user_avatar(url) else: + # bjy: 如果没有metadata,则使用默认头像 url = static('blog/img/avatar.png') else: + # bjy: 如果头像URL不是本地静态文件,则直接保存到本地 url = save_user_avatar(url) else: + # bjy: 如果头像URL为空,则使用默认头像 url = static('blog/img/avatar.png') + # bjy: 如果最终得到的URL不为空,则更新用户头像并保存 if url: + # bjy: 输出同步完成后的用户昵称和头像URL self.stdout.write( f'结束同步:{u.nickname}.url:{url}') u.picture = url u.save() + # bjy: 输出同步全部结束的信息 self.stdout.write('结束同步') diff --git a/src/DjangoBlog/blog/middleware.py b/src/DjangoBlog/blog/middleware.py index 94dd70c..039dc8a 100644 --- a/src/DjangoBlog/blog/middleware.py +++ b/src/DjangoBlog/blog/middleware.py @@ -1,42 +1,67 @@ +# bjy: 导入日志模块 import logging +# bjy: 导入时间模块,用于计算页面渲染时间 import time +# bjy: 从ipware库导入get_client_ip函数,用于获取客户端真实IP from ipware import get_client_ip +# bjy: 从user_agents库导入parse函数,用于解析User-Agent字符串 from user_agents import parse +# bjy: 从blog应用的documents模块中导入Elasticsearch是否启用的标志和性能文档管理器 from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager +# bjy: 获取一个名为__name__的logger实例,用于记录日志 logger = logging.getLogger(__name__) +# bjy: 定义一个中间件类,用于记录页面性能和在线用户信息 class OnlineMiddleware(object): + # bjy: Django 1.10+ 兼容的初始化方法 def __init__(self, get_response=None): + # bjy: 保存get_response可调用对象,它是Django请求-响应链中的下一个处理器 self.get_response = get_response + # bjy: 调用父类的初始化方法 super().__init__() + # bjy: 中间件的核心调用方法,每个请求都会经过这里 def __call__(self, request): - ''' page render time ''' + # bjy: 记录页面渲染开始时间 start_time = time.time() + # bjy: 调用下一个中间件或视图,获取响应对象 response = self.get_response(request) + # bjy: 从请求头中获取User-Agent字符串 http_user_agent = request.META.get('HTTP_USER_AGENT', '') + # bjy: 使用ipware库获取客户端的IP地址 ip, _ = get_client_ip(request) + # bjy: 解析User-Agent字符串,得到结构化的用户代理信息 user_agent = parse(http_user_agent) + # bjy: 检查响应是否为流式响应(如文件下载),如果不是,则进行处理 if not response.streaming: try: + # bjy: 计算页面渲染耗时(秒) cast_time = time.time() - start_time + # bjy: 如果启用了Elasticsearch if ELASTICSEARCH_ENABLED: + # bjy: 将耗时转换为毫秒并四舍五入 time_taken = round((cast_time) * 1000, 2) + # bjy: 获取请求的URL路径 url = request.path + # bjy: 导入Django的时区工具 from django.utils import timezone + # bjy: 调用文档管理器,将性能数据保存到Elasticsearch ElaspedTimeDocumentManager.create( url=url, time_taken=time_taken, log_datetime=timezone.now(), useragent=user_agent, ip=ip) + # bjy: 将页面渲染耗时替换到响应内容的特定占位符中 response.content = response.content.replace( b'', str.encode(str(cast_time)[:5])) + # bjy: 捕获并记录处理过程中可能发生的任何异常 except Exception as e: logger.error("Error OnlineMiddleware: %s" % e) + # bjy: 返回最终的响应对象 return response diff --git a/src/DjangoBlog/blog/migrations/0001_initial.py b/src/DjangoBlog/blog/migrations/0001_initial.py index 3d391b6..a9febfe 100644 --- a/src/DjangoBlog/blog/migrations/0001_initial.py +++ b/src/DjangoBlog/blog/migrations/0001_initial.py @@ -1,133 +1,212 @@ -# Generated by Django 4.1.7 on 2023-03-02 07:14 +# bjy: 此文件由Django 4.1.7于2023-03-02 07:14自动生成,用于数据库结构迁移 +# bjy: 从Django配置模块导入settings,用于获取AUTH_USER_MODEL等配置 from django.conf import settings +# bjy: 从Django数据库模块导入migrations和models,用于定义迁移操作和模型字段 from django.db import migrations, models +# bjy: 导入django.db.models.deletion,用于定义模型删除时的行为(如级联删除) import django.db.models.deletion +# bjy: 导入django.utils.timezone,用于为模型字段提供默认的时区感知时间 import django.utils.timezone +# bjy: 导入mdeditor.fields,用于使用Markdown编辑器字段类型 import mdeditor.fields +# bjy: 定义一个迁移类,用于创建博客应用所需的数据表 class Migration(migrations.Migration): + # bjy: 标记这是该应用的初始迁移 initial = True + # bjy: 定义此迁移的依赖关系,依赖于用户模型的迁移,确保用户表先被创建 dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作1:创建BlogSettings(网站配置)模型对应的数据库表 migrations.CreateModel( name='BlogSettings', fields=[ + # bjy: 主键ID,大整数自增 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # bjy: 网站名称,字符串类型,最大长度200 ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), + # bjy: 网站描述,文本类型,最大长度1000 ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), + # bjy: 网站SEO描述,文本类型,最大长度1000 ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), + # bjy: 网站关键字,文本类型,最大长度1000 ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), + # bjy: 文章摘要长度,整数类型,默认300 ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), + # bjy: 侧边栏文章数目,整数类型,默认10 ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), + # bjy: 侧边栏评论数目,整数类型,默认5 ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), + # bjy: 文章页面默认显示评论数目,整数类型,默认5 ('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')), + # bjy: 是否显示谷歌广告,布尔类型,默认False ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')), + # bjy: 广告内容,文本类型,可为空,最大长度2000 ('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')), + # bjy: 是否打开网站评论功能,布尔类型,默认True ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')), + # bjy: 备案号,字符串类型,可为空,最大长度2000 ('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')), + # bjy: 网站统计代码,文本类型,最大长度1000 ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), + # bjy: 是否显示公安备案号,布尔类型,默认False ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), + # bjy: 公安备案号,文本类型,可为空,最大长度2000 ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), ], options={ + # bjy: 设置模型在后台管理中的单复数名称 'verbose_name': '网站配置', 'verbose_name_plural': '网站配置', }, ), + # bjy: 操作2:创建Links(友情链接)模型对应的数据库表 migrations.CreateModel( name='Links', fields=[ + # bjy: 主键ID,大整数自增 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # bjy: 链接名称,字符串类型,最大长度30,唯一 ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), + # bjy: 链接地址,URLField类型 ('link', models.URLField(verbose_name='链接地址')), + # bjy: 排序,整数类型,唯一 ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + # bjy: 是否显示,布尔类型,默认True ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + # bjy: 显示类型,字符类型,提供选择项,默认'i'(首页) ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), + # bjy: 创建时间,日期时间类型,默认为当前时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # bjy: 修改时间,日期时间类型,默认为当前时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ], options={ + # bjy: 设置模型在后台管理中的单复数名称和默认排序方式 'verbose_name': '友情链接', 'verbose_name_plural': '友情链接', 'ordering': ['sequence'], }, ), + # bjy: 操作3:创建SideBar(侧边栏)模型对应的数据库表 migrations.CreateModel( name='SideBar', fields=[ + # bjy: 主键ID,大整数自增 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + # bjy: 标题,字符串类型,最大长度100 ('name', models.CharField(max_length=100, verbose_name='标题')), + # bjy: 内容,文本类型 ('content', models.TextField(verbose_name='内容')), + # bjy: 排序,整数类型,唯一 ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + # bjy: 是否启用,布尔类型,默认True ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), + # bjy: 创建时间,日期时间类型,默认为当前时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # bjy: 修改时间,日期时间类型,默认为当前时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ], options={ + # bjy: 设置模型在后台管理中的单复数名称和默认排序方式 'verbose_name': '侧边栏', 'verbose_name_plural': '侧边栏', 'ordering': ['sequence'], }, ), + # bjy: 操作4:创建Tag(标签)模型对应的数据库表 migrations.CreateModel( name='Tag', fields=[ + # bjy: 主键ID,自增整数 ('id', models.AutoField(primary_key=True, serialize=False)), + # bjy: 创建时间,日期时间类型,默认为当前时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # bjy: 修改时间,日期时间类型,默认为当前时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + # bjy: 标签名,字符串类型,最大长度30,唯一 ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), + # bjy: 别名,SlugField类型,用于生成友好URL,可为空,默认'no-slug' ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), ], options={ + # bjy: 设置模型在后台管理中的单复数名称和默认排序方式 'verbose_name': '标签', 'verbose_name_plural': '标签', 'ordering': ['name'], }, ), + # bjy: 操作5:创建Category(分类)模型对应的数据库表 migrations.CreateModel( name='Category', fields=[ + # bjy: 主键ID,自增整数 ('id', models.AutoField(primary_key=True, serialize=False)), + # bjy: 创建时间,日期时间类型,默认为当前时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # bjy: 修改时间,日期时间类型,默认为当前时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + # bjy: 分类名,字符串类型,最大长度30,唯一 ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), + # bjy: 别名,SlugField类型,用于生成友好URL,可为空,默认'no-slug' ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + # bjy: 权重排序,整数类型,默认0,越大越靠前 ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), + # bjy: 父级分类,外键关联到自身,可为空,级联删除 ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), ], options={ + # bjy: 设置模型在后台管理中的单复数名称和默认排序方式(按权重降序) 'verbose_name': '分类', 'verbose_name_plural': '分类', 'ordering': ['-index'], }, ), + # bjy: 操作6:创建Article(文章)模型对应的数据库表 migrations.CreateModel( name='Article', fields=[ + # bjy: 主键ID,自增整数 ('id', models.AutoField(primary_key=True, serialize=False)), + # bjy: 创建时间,日期时间类型,默认为当前时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + # bjy: 修改时间,日期时间类型,默认为当前时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + # bjy: 标题,字符串类型,最大长度200,唯一 ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), + # bjy: 正文,Markdown编辑器字段类型 ('body', mdeditor.fields.MDTextField(verbose_name='正文')), + # bjy: 发布时间,日期时间类型,默认为当前时间 ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), + # bjy: 文章状态,字符类型,提供选择项(草稿/发表),默认'p'(发表) ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), + # bjy: 评论状态,字符类型,提供选择项(打开/关闭),默认'o'(打开) ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), + # bjy: 类型,字符类型,提供选择项(文章/页面),默认'a'(文章) ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')), + # bjy: 浏览量,正整数类型,默认0 ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), + # bjy: 排序,整数类型,默认0,数字越大越靠前 ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), + # bjy: 是否显示toc目录,布尔类型,默认False ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), + # bjy: 作者,外键关联到用户模型,级联删除 ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + # bjy: 分类,外键关联到Category模型,级联删除 ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), + # bjy: 标签集合,多对多关系,关联到Tag模型,可为空 ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), ], options={ + # bjy: 设置模型在后台管理中的单复数名称、默认排序方式和获取最新记录的依据字段 'verbose_name': '文章', 'verbose_name_plural': '文章', 'ordering': ['-article_order', '-pub_time'], diff --git a/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py index adbaa36..0e92fe9 100644 --- a/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py +++ b/src/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py @@ -1,23 +1,35 @@ -# Generated by Django 4.1.7 on 2023-03-29 06:08 +# bjy: 此文件由Django 4.1.7于2023-03-29 06:08自动生成,用于数据库结构迁移 +# bjy: 从Django数据库模块导入migrations和models,用于定义迁移操作和模型字段 from django.db import migrations, models +# bjy: 定义一个迁移类,用于对blog应用进行数据库结构变更 class Migration(migrations.Migration): + # bjy: 定义此迁移的依赖关系,它依赖于blog应用的0001_initial迁移,确保基础表已存在 dependencies = [ ('blog', '0001_initial'), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作1:为BlogSettings模型添加一个名为'global_footer'的字段 migrations.AddField( + # bjy: 指定要操作的模型名称 model_name='blogsettings', + # bjy: 指定新字段的名称 name='global_footer', + # bjy: 定义新字段的类型和属性:文本类型,可为空,默认空字符串,并设置verbose_name field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), ), + # bjy: 操作2:为BlogSettings模型添加一个名为'global_header'的字段 migrations.AddField( + # bjy: 指定要操作的模型名称 model_name='blogsettings', + # bjy: 指定新字段的名称 name='global_header', + # bjy: 定义新字段的类型和属性:文本类型,可为空,默认空字符串,并设置verbose_name field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), ), ] diff --git a/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py b/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py index e9f5502..b408d83 100644 --- a/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py +++ b/src/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py @@ -1,17 +1,25 @@ -# Generated by Django 4.2.1 on 2023-05-09 07:45 +# bjy: 此文件由Django 4.2.1于2023-05-09 07:45自动生成,用于数据库结构迁移 +# bjy: 从Django数据库模块导入migrations和models,用于定义迁移操作和模型字段 from django.db import migrations, models +# bjy: 定义一个迁移类,用于对blog应用进行数据库结构变更 class Migration(migrations.Migration): + # bjy: 定义此迁移的依赖关系,它依赖于blog应用的0002_blogsettings_global_footer_and_more迁移 dependencies = [ ('blog', '0002_blogsettings_global_footer_and_more'), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作:为BlogSettings模型添加一个新字段 migrations.AddField( + # bjy: 指定要操作的模型名称为'blogsettings' model_name='blogsettings', + # bjy: 指定新字段的名称为'comment_need_review' name='comment_need_review', + # bjy: 定义新字段的类型和属性:布尔类型,默认值为False,并设置verbose_name field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), ), ] diff --git a/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py index ceb1398..198d53e 100644 --- a/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py +++ b/src/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py @@ -1,27 +1,43 @@ -# Generated by Django 4.2.1 on 2023-05-09 07:51 +# bjy: 此文件由Django 4.2.1于2023-05-09 07:51自动生成,用于数据库结构迁移 +# bjy: 从Django数据库模块导入migrations,用于定义迁移操作 from django.db import migrations +# bjy: 定义一个迁移类,用于对blog应用进行数据库结构变更 class Migration(migrations.Migration): + # bjy: 定义此迁移的依赖关系,它依赖于blog应用的0003_blogsettings_comment_need_review迁移 dependencies = [ ('blog', '0003_blogsettings_comment_need_review'), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作1:重命名BlogSettings模型中的一个字段 migrations.RenameField( + # bjy: 指定要操作的模型名称为'blogsettings' model_name='blogsettings', + # bjy: 指定字段的原始名称为'analyticscode' old_name='analyticscode', + # bjy: 指定字段的新名称为'analytics_code' new_name='analytics_code', ), + # bjy: 操作2:重命名BlogSettings模型中的另一个字段 migrations.RenameField( + # bjy: 指定要操作的模型名称为'blogsettings' model_name='blogsettings', + # bjy: 指定字段的原始名称为'beiancode' old_name='beiancode', + # bjy: 指定字段的新名称为'beian_code' new_name='beian_code', ), + # bjy: 操作3:重命名BlogSettings模型中的第三个字段 migrations.RenameField( + # bjy: 指定要操作的模型名称为'blogsettings' model_name='blogsettings', + # bjy: 指定字段的原始名称为'sitename' old_name='sitename', + # bjy: 指定字段的新名称为'site_name' new_name='site_name', ), ] diff --git a/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py index d08e853..54590bf 100644 --- a/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py +++ b/src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py @@ -1,297 +1,364 @@ -# Generated by Django 4.2.5 on 2023-09-06 13:13 +# bjy: 此文件由Django 4.2.5于2023-09-06 13:13自动生成,用于数据库结构迁移 +# bjy: 从Django配置模块导入settings,用于获取AUTH_USER_MODEL等配置 from django.conf import settings +# bjy: 从Django数据库模块导入migrations和models,用于定义迁移操作和模型字段 from django.db import migrations, models +# bjy: 导入django.db.models.deletion,用于定义模型删除时的行为(如级联删除) import django.db.models.deletion +# bjy: 导入django.utils.timezone,用于为模型字段提供默认的时区感知时间 import django.utils.timezone +# bjy: 导入mdeditor.fields,用于使用Markdown编辑器字段类型 import mdeditor.fields +# bjy: 定义一个迁移类,用于对blog应用进行数据库结构变更 class Migration(migrations.Migration): + # bjy: 定义此迁移的依赖关系,它依赖于用户模型和blog应用的0004迁移 dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作1:修改Article模型的Meta选项,更新verbose_name等 migrations.AlterModelOptions( name='article', options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, ), + # bjy: 操作2:修改Category模型的Meta选项,更新verbose_name等 migrations.AlterModelOptions( name='category', options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, ), + # bjy: 操作3:修改Links模型的Meta选项,更新verbose_name等 migrations.AlterModelOptions( name='links', options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, ), + # bjy: 操作4:修改SideBar模型的Meta选项,更新verbose_name等 migrations.AlterModelOptions( name='sidebar', options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, ), + # bjy: 操作5:修改Tag模型的Meta选项,更新verbose_name等 migrations.AlterModelOptions( name='tag', options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, ), + # bjy: 操作6:删除Article模型的created_time字段 migrations.RemoveField( model_name='article', name='created_time', ), + # bjy: 操作7:删除Article模型的last_mod_time字段 migrations.RemoveField( model_name='article', name='last_mod_time', ), + # bjy: 操作8:删除Category模型的created_time字段 migrations.RemoveField( model_name='category', name='created_time', ), + # bjy: 操作9:删除Category模型的last_mod_time字段 migrations.RemoveField( model_name='category', name='last_mod_time', ), + # bjy: 操作10:删除Links模型的created_time字段 migrations.RemoveField( model_name='links', name='created_time', ), + # bjy: 操作11:删除SideBar模型的created_time字段 migrations.RemoveField( model_name='sidebar', name='created_time', ), + # bjy: 操作12:删除Tag模型的created_time字段 migrations.RemoveField( model_name='tag', name='created_time', ), + # bjy: 操作13:删除Tag模型的last_mod_time字段 migrations.RemoveField( model_name='tag', name='last_mod_time', ), + # bjy: 操作14:为Article模型添加creation_time字段 migrations.AddField( model_name='article', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # bjy: 操作15:为Article模型添加last_modify_time字段 migrations.AddField( model_name='article', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # bjy: 操作16:为Category模型添加creation_time字段 migrations.AddField( model_name='category', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # bjy: 操作17:为Category模型添加last_modify_time字段 migrations.AddField( model_name='category', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # bjy: 操作18:为Links模型添加creation_time字段 migrations.AddField( model_name='links', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # bjy: 操作19:为SideBar模型添加creation_time字段 migrations.AddField( model_name='sidebar', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # bjy: 操作20:为Tag模型添加creation_time字段 migrations.AddField( model_name='tag', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), + # bjy: 操作21:为Tag模型添加last_modify_time字段 migrations.AddField( model_name='tag', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # bjy: 操作22:修改Article模型的article_order字段,更新verbose_name migrations.AlterField( model_name='article', name='article_order', field=models.IntegerField(default=0, verbose_name='order'), ), + # bjy: 操作23:修改Article模型的author字段,更新verbose_name migrations.AlterField( model_name='article', name='author', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), ), + # bjy: 操作24:修改Article模型的body字段,更新verbose_name migrations.AlterField( model_name='article', name='body', field=mdeditor.fields.MDTextField(verbose_name='body'), ), + # bjy: 操作25:修改Article模型的category字段,更新verbose_name migrations.AlterField( model_name='article', name='category', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), ), + # bjy: 操作26:修改Article模型的comment_status字段,更新choices和verbose_name migrations.AlterField( model_name='article', name='comment_status', field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'), ), + # bjy: 操作27:修改Article模型的pub_time字段,更新verbose_name migrations.AlterField( model_name='article', name='pub_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), ), + # bjy: 操作28:修改Article模型的show_toc字段,更新verbose_name migrations.AlterField( model_name='article', name='show_toc', field=models.BooleanField(default=False, verbose_name='show toc'), ), + # bjy: 操作29:修改Article模型的status字段,更新choices和verbose_name migrations.AlterField( model_name='article', name='status', field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), ), + # bjy: 操作30:修改Article模型的tags字段,更新verbose_name migrations.AlterField( model_name='article', name='tags', field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), ), + # bjy: 操作31:修改Article模型的title字段,更新verbose_name migrations.AlterField( model_name='article', name='title', field=models.CharField(max_length=200, unique=True, verbose_name='title'), ), + # bjy: 操作32:修改Article模型的type字段,更新choices和verbose_name migrations.AlterField( model_name='article', name='type', field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), ), + # bjy: 操作33:修改Article模型的views字段,更新verbose_name migrations.AlterField( model_name='article', name='views', field=models.PositiveIntegerField(default=0, verbose_name='views'), ), + # bjy: 操作34:修改BlogSettings模型的article_comment_count字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='article_comment_count', field=models.IntegerField(default=5, verbose_name='article comment count'), ), + # bjy: 操作35:修改BlogSettings模型的article_sub_length字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='article_sub_length', field=models.IntegerField(default=300, verbose_name='article sub length'), ), + # bjy: 操作36:修改BlogSettings模型的google_adsense_codes字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='google_adsense_codes', field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), ), + # bjy: 操作37:修改BlogSettings模型的open_site_comment字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='open_site_comment', field=models.BooleanField(default=True, verbose_name='open site comment'), ), + # bjy: 操作38:修改BlogSettings模型的show_google_adsense字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='show_google_adsense', field=models.BooleanField(default=False, verbose_name='show adsense'), ), + # bjy: 操作39:修改BlogSettings模型的sidebar_article_count字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='sidebar_article_count', field=models.IntegerField(default=10, verbose_name='sidebar article count'), ), + # bjy: 操作40:修改BlogSettings模型的sidebar_comment_count字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='sidebar_comment_count', field=models.IntegerField(default=5, verbose_name='sidebar comment count'), ), + # bjy: 操作41:修改BlogSettings模型的site_description字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='site_description', field=models.TextField(default='', max_length=1000, verbose_name='site description'), ), + # bjy: 操作42:修改BlogSettings模型的site_keywords字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='site_keywords', field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), ), + # bjy: 操作43:修改BlogSettings模型的site_name字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='site_name', field=models.CharField(default='', max_length=200, verbose_name='site name'), ), + # bjy: 操作44:修改BlogSettings模型的site_seo_description字段,更新verbose_name migrations.AlterField( model_name='blogsettings', name='site_seo_description', field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), ), + # bjy: 操作45:修改Category模型的index字段,更新verbose_name migrations.AlterField( model_name='category', name='index', field=models.IntegerField(default=0, verbose_name='index'), ), + # bjy: 操作46:修改Category模型的name字段,更新verbose_name migrations.AlterField( model_name='category', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='category name'), ), + # bjy: 操作47:修改Category模型的parent_category字段,更新verbose_name migrations.AlterField( model_name='category', name='parent_category', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), ), + # bjy: 操作48:修改Links模型的is_enable字段,更新verbose_name migrations.AlterField( model_name='links', name='is_enable', field=models.BooleanField(default=True, verbose_name='is show'), ), + # bjy: 操作49:修改Links模型的last_mod_time字段,更新verbose_name migrations.AlterField( model_name='links', name='last_mod_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # bjy: 操作50:修改Links模型的link字段,更新verbose_name migrations.AlterField( model_name='links', name='link', field=models.URLField(verbose_name='link'), ), + # bjy: 操作51:修改Links模型的name字段,更新verbose_name migrations.AlterField( model_name='links', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='link name'), ), + # bjy: 操作52:修改Links模型的sequence字段,更新verbose_name migrations.AlterField( model_name='links', name='sequence', field=models.IntegerField(unique=True, verbose_name='order'), ), + # bjy: 操作53:修改Links模型的show_type字段,更新choices和verbose_name migrations.AlterField( model_name='links', name='show_type', field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), ), + # bjy: 操作54:修改SideBar模型的content字段,更新verbose_name migrations.AlterField( model_name='sidebar', name='content', field=models.TextField(verbose_name='content'), ), + # bjy: 操作55:修改SideBar模型的is_enable字段,更新verbose_name migrations.AlterField( model_name='sidebar', name='is_enable', field=models.BooleanField(default=True, verbose_name='is enable'), ), + # bjy: 操作56:修改SideBar模型的last_mod_time字段,更新verbose_name migrations.AlterField( model_name='sidebar', name='last_mod_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + # bjy: 操作57:修改SideBar模型的name字段,更新verbose_name migrations.AlterField( model_name='sidebar', name='name', field=models.CharField(max_length=100, verbose_name='title'), ), + # bjy: 操作58:修改SideBar模型的sequence字段,更新verbose_name migrations.AlterField( model_name='sidebar', name='sequence', field=models.IntegerField(unique=True, verbose_name='order'), ), + # bjy: 操作59:修改Tag模型的name字段,更新verbose_name migrations.AlterField( model_name='tag', name='name', diff --git a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py index e36feb4..560a1a4 100644 --- a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py +++ b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py @@ -1,17 +1,24 @@ -# Generated by Django 4.2.7 on 2024-01-26 02:41 +# bjy: 此文件由Django 4.2.7于2024-01-26 02:41自动生成,用于数据库结构迁移 +# bjy: 从Django数据库模块导入migrations,用于定义迁移操作 from django.db import migrations +# bjy: 定义一个迁移类,用于对blog应用进行数据库结构变更 class Migration(migrations.Migration): + # bjy: 定义此迁移的依赖关系,它依赖于blog应用的0005迁移 dependencies = [ ('blog', '0005_alter_article_options_alter_category_options_and_more'), ] + # bjy: 定义此迁移要执行的一系列操作 operations = [ + # bjy: 操作:修改BlogSettings模型的Meta选项,更新verbose_name为英文 migrations.AlterModelOptions( + # bjy: 指定要操作的模型名称为'blogsettings' name='blogsettings', + # bjy: 更新模型的verbose_name和verbose_name_plural为'Website configuration' options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, ), ] diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py index 083788b..71c3b4f 100644 --- a/src/DjangoBlog/blog/models.py +++ b/src/DjangoBlog/blog/models.py @@ -1,49 +1,74 @@ +# bjy: 导入日志模块 import logging +# bjy: 导入正则表达式模块 import re +# bjy: 导入抽象基类模块,用于定义抽象方法 from abc import abstractmethod +# bjy: 从Django中导入设置、异常、模型、URL反向解析、时区和国际化工具 from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +# bjy: 从Django MDEditor中导入Markdown文本字段 from mdeditor.fields import MDTextField +# bjy: 从uuslug中导入slugify函数,用于生成URL友好的slug from uuslug import slugify +# bjy: 从项目工具模块中导入缓存装饰器和缓存对象 from djangoblog.utils import cache_decorator, cache +# bjy: 从项目工具模块中导入获取当前站点的函数 from djangoblog.utils import get_current_site +# bjy: 获取一个名为__name__的logger实例,用于记录日志 logger = logging.getLogger(__name__) +# bjy: 定义一个文本选择类,用于链接显示类型 class LinkShowType(models.TextChoices): + # bjy: 首页 I = ('i', _('index')) + # bjy: 列表页 L = ('l', _('list')) + # bjy: 文章页 P = ('p', _('post')) + # bjy: 所有页面 A = ('a', _('all')) + # bjy: 幻灯片 S = ('s', _('slide')) +# bjy: 定义一个基础模型类,作为其他模型的父类 class BaseModel(models.Model): + # bjy: 自增主键 id = models.AutoField(primary_key=True) + # bjy: 创建时间,默认为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # bjy: 最后修改时间,默认为当前时间 last_modify_time = models.DateTimeField(_('modify time'), default=now) + # bjy: 重写save方法,以实现自定义逻辑 def save(self, *args, **kwargs): + # bjy: 检查是否是更新文章浏览量的操作 is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: + # bjy: 如果是,则直接更新数据库中的views字段,避免触发其他save逻辑 Article.objects.filter(pk=self.pk).update(views=self.views) else: + # bjy: 如果模型有slug字段,则根据title或name自动生成slug if 'slug' in self.__dict__: slug = getattr( self, 'title') if 'title' in self.__dict__ else getattr( self, 'name') setattr(self, 'slug', slugify(slug)) + # bjy: 调用父类的save方法 super().save(*args, **kwargs) + # bjy: 获取模型的完整URL(包括域名) def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, @@ -51,72 +76,96 @@ class BaseModel(models.Model): return url class Meta: + # bjy: 设置为抽象模型,不会在数据库中创建表 abstract = True + # bjy: 定义一个抽象方法,要求子类必须实现 @abstractmethod def get_absolute_url(self): pass +# bjy: 定义文章模型 class Article(BaseModel): """文章""" + # bjy: 文章状态选择 STATUS_CHOICES = ( ('d', _('Draft')), ('p', _('Published')), ) + # bjy: 评论状态选择 COMMENT_STATUS = ( ('o', _('Open')), ('c', _('Close')), ) + # bjy: 文章类型选择 TYPE = ( ('a', _('Article')), ('p', _('Page')), ) + # bjy: 文章标题,唯一 title = models.CharField(_('title'), max_length=200, unique=True) + # bjy: 文章正文,使用Markdown编辑器 body = MDTextField(_('body')) + # bjy: 发布时间 pub_time = models.DateTimeField( _('publish time'), blank=False, null=False, default=now) + # bjy: 文章状态 status = models.CharField( _('status'), max_length=1, choices=STATUS_CHOICES, default='p') + # bjy: 评论状态 comment_status = models.CharField( _('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') + # bjy: 文章类型 type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') + # bjy: 浏览量 views = models.PositiveIntegerField(_('views'), default=0) + # bjy: 作者,外键关联到用户模型 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, on_delete=models.CASCADE) + # bjy: 文章排序权重 article_order = models.IntegerField( _('order'), blank=False, null=False, default=0) + # bjy: 是否显示目录 show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + # bjy: 分类,外键关联到Category模型 category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, null=False) + # bjy: 标签,多对多关联到Tag模型 tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + # bjy: 将body字段转换为字符串 def body_to_string(self): return self.body + # bjy: 定义文章的字符串表示 def __str__(self): return self.title class Meta: + # bjy: 默认排序方式 ordering = ['-article_order', '-pub_time'] + # bjy: 模型的单数和复数名称 verbose_name = _('article') verbose_name_plural = verbose_name + # bjy: 指定按哪个字段获取最新对象 get_latest_by = 'id' + # bjy: 获取文章的绝对URL def get_absolute_url(self): return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, @@ -125,6 +174,7 @@ class Article(BaseModel): 'day': self.creation_time.day }) + # bjy: 获取文章的分类树路径,带缓存 @cache_decorator(60 * 60 * 10) def get_category_tree(self): tree = self.category.get_category_tree() @@ -132,13 +182,16 @@ class Article(BaseModel): return names + # bjy: 重写save方法 def save(self, *args, **kwargs): super().save(*args, **kwargs) + # bjy: 增加浏览量 def viewed(self): self.views += 1 self.save(update_fields=['views']) + # bjy: 获取文章的评论列表,带缓存 def comment_list(self): cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) @@ -151,21 +204,25 @@ class Article(BaseModel): logger.info('set article comments:{id}'.format(id=self.id)) return comments + # bjy: 获取文章在Admin后台的编辑URL def get_admin_url(self): info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + # bjy: 获取下一篇文章,带缓存 @cache_decorator(expiration=60 * 100) def next_article(self): # 下一篇 return Article.objects.filter( id__gt=self.id, status='p').order_by('id').first() + # bjy: 获取上一篇文章,带缓存 @cache_decorator(expiration=60 * 100) def prev_article(self): # 前一篇 return Article.objects.filter(id__lt=self.id, status='p').first() + # bjy: 从文章正文中提取第一张图片的URL def get_first_image_url(self): """ Get the first image url from article.body. @@ -177,31 +234,40 @@ class Article(BaseModel): return "" +# bjy: 定义分类模型 class Category(BaseModel): """文章分类""" + # bjy: 分类名称,唯一 name = models.CharField(_('category name'), max_length=30, unique=True) + # bjy: 父分类,自关联外键 parent_category = models.ForeignKey( 'self', verbose_name=_('parent category'), blank=True, null=True, on_delete=models.CASCADE) + # bjy: URL友好的别名 slug = models.SlugField(default='no-slug', max_length=60, blank=True) + # bjy: 排序索引 index = models.IntegerField(default=0, verbose_name=_('index')) class Meta: + # bjy: 按索引降序排列 ordering = ['-index'] verbose_name = _('category') verbose_name_plural = verbose_name + # bjy: 获取分类的绝对URL def get_absolute_url(self): return reverse( 'blog:category_detail', kwargs={ 'category_name': self.slug}) + # bjy: 定义分类的字符串表示 def __str__(self): return self.name + # bjy: 递归获取分类的所有父级分类,带缓存 @cache_decorator(60 * 60 * 10) def get_category_tree(self): """ @@ -218,6 +284,7 @@ class Category(BaseModel): parse(self) return categorys + # bjy: 获取当前分类的所有子分类,带缓存 @cache_decorator(60 * 60 * 10) def get_sub_categorys(self): """ @@ -240,136 +307,186 @@ class Category(BaseModel): return categorys +# bjy: 定义标签模型 class Tag(BaseModel): """文章标签""" + # bjy: 标签名称,唯一 name = models.CharField(_('tag name'), max_length=30, unique=True) + # bjy: URL友好的别名 slug = models.SlugField(default='no-slug', max_length=60, blank=True) + # bjy: 定义标签的字符串表示 def __str__(self): return self.name + # bjy: 获取标签的绝对URL def get_absolute_url(self): return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) + # bjy: 获取使用该标签的文章数量,带缓存 @cache_decorator(60 * 60 * 10) def get_article_count(self): return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: + # bjy: 按名称排序 ordering = ['name'] verbose_name = _('tag') verbose_name_plural = verbose_name +# bjy: 定义友情链接模型 class Links(models.Model): """友情链接""" + # bjy: 链接名称,唯一 name = models.CharField(_('link name'), max_length=30, unique=True) + # bjy: 链接URL link = models.URLField(_('link')) + # bjy: 排序权重,唯一 sequence = models.IntegerField(_('order'), unique=True) + # bjy: 是否启用 is_enable = models.BooleanField( _('is show'), default=True, blank=False, null=False) + # bjy: 显示类型 show_type = models.CharField( _('show type'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I) + # bjy: 创建时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # bjy: 最后修改时间 last_mod_time = models.DateTimeField(_('modify time'), default=now) class Meta: + # bjy: 按排序权重升序排列 ordering = ['sequence'] verbose_name = _('link') verbose_name_plural = verbose_name + # bjy: 定义链接的字符串表示 def __str__(self): return self.name +# bjy: 定义侧边栏模型 class SideBar(models.Model): """侧边栏,可以展示一些html内容""" + # bjy: 侧边栏标题 name = models.CharField(_('title'), max_length=100) + # bjy: 侧边栏内容(HTML) content = models.TextField(_('content')) + # bjy: 排序权重,唯一 sequence = models.IntegerField(_('order'), unique=True) + # bjy: 是否启用 is_enable = models.BooleanField(_('is enable'), default=True) + # bjy: 创建时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # bjy: 最后修改时间 last_mod_time = models.DateTimeField(_('modify time'), default=now) class Meta: + # bjy: 按排序权重升序排列 ordering = ['sequence'] verbose_name = _('sidebar') verbose_name_plural = verbose_name + # bjy: 定义侧边栏的字符串表示 def __str__(self): return self.name +# bjy: 定义博客设置模型 class BlogSettings(models.Model): """blog的配置""" + # bjy: 网站名称 site_name = models.CharField( _('site name'), max_length=200, null=False, blank=False, default='') + # bjy: 网站描述 site_description = models.TextField( _('site description'), max_length=1000, null=False, blank=False, default='') + # bjy: SEO描述 site_seo_description = models.TextField( _('site seo description'), max_length=1000, null=False, blank=False, default='') + # bjy: 网站关键词 site_keywords = models.TextField( _('site keywords'), max_length=1000, null=False, blank=False, default='') + # bjy: 文章摘要长度 article_sub_length = models.IntegerField(_('article sub length'), default=300) + # bjy: 侧边栏文章数量 sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) + # bjy: 侧边栏评论数量 sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) + # bjy: 文章页评论数量 article_comment_count = models.IntegerField(_('article comment count'), default=5) + # bjy: 是否显示Google AdSense show_google_adsense = models.BooleanField(_('show adsense'), default=False) + # bjy: Google AdSense代码 google_adsense_codes = models.TextField( _('adsense code'), max_length=2000, null=True, blank=True, default='') + # bjy: 是否开启全站评论 open_site_comment = models.BooleanField(_('open site comment'), default=True) + # bjy: 公共头部HTML代码 global_header = models.TextField("公共头部", null=True, blank=True, default='') + # bjy: 公共尾部HTML代码 global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + # bjy: ICP备案号 beian_code = models.CharField( '备案号', max_length=2000, null=True, blank=True, default='') + # bjy: 网站统计代码 analytics_code = models.TextField( "网站统计代码", max_length=1000, null=False, blank=False, default='') + # bjy: 是否显示公安备案号 show_gongan_code = models.BooleanField( '是否显示公安备案号', default=False, null=False) + # bjy: 公安备案号 gongan_beiancode = models.TextField( '公安备案号', max_length=2000, null=True, blank=True, default='') + # bjy: 评论是否需要审核 comment_need_review = models.BooleanField( '评论是否需要审核', default=False, null=False) class Meta: + # bjy: 模型的单数和复数名称 verbose_name = _('Website configuration') verbose_name_plural = verbose_name + # bjy: 定义设置的字符串表示 def __str__(self): return self.site_name + # bjy: 重写clean方法,用于模型验证 def clean(self): + # bjy: 确保数据库中只能有一条配置记录 if BlogSettings.objects.exclude(id=self.id).count(): raise ValidationError(_('There can only be one configuration')) + # bjy: 重写save方法,保存后清除缓存 def save(self, *args, **kwargs): super().save(*args, **kwargs) from djangoblog.utils import cache diff --git a/src/DjangoBlog/blog/search_indexes.py b/src/DjangoBlog/blog/search_indexes.py index 7f1dfac..055ddf7 100644 --- a/src/DjangoBlog/blog/search_indexes.py +++ b/src/DjangoBlog/blog/search_indexes.py @@ -1,13 +1,21 @@ +# bjy: 从haystack框架中导入indexes模块,用于创建搜索索引 from haystack import indexes +# bjy: 从blog应用中导入Article模型 from blog.models import Article +# bjy: 为Article模型定义一个搜索索引类 class ArticleIndex(indexes.SearchIndex, indexes.Indexable): + # bjy: 定义一个主文本字段,`document=True`表示这是搜索的主要字段 + # bjy: `use_template=True`表示该字段的内容将由一个模板来生成 text = indexes.CharField(document=True, use_template=True) + # bjy: `get_model`方法必须实现,用于返回此索引对应的模型类 def get_model(self): return Article + # bjy: `index_queryset`方法定义了哪些模型实例应该被建立索引 def index_queryset(self, using=None): + # bjy: 这里只返回状态为'p'(已发布)的文章 return self.get_model().objects.filter(status='p') diff --git a/src/DjangoBlog/blog/static/account/css/account.css b/src/DjangoBlog/blog/static/account/css/account.css index 7d4cec7..7daa472 100644 --- a/src/DjangoBlog/blog/static/account/css/account.css +++ b/src/DjangoBlog/blog/static/account/css/account.css @@ -1,9 +1,17 @@ +/* bjy: 定义一个名为.button的CSS类,用于设置按钮的通用样式 */ .button { + /* bjy: 移除按钮的默认边框 */ border: none; + /* bjy: 设置按钮的内边距,上下4像素,左右80像素 */ padding: 4px 80px; + /* bjy: 设置按钮内部文本的水平居中对齐 */ text-align: center; + /* bjy: 移除文本装饰(如下划线),通常用于链接样式的按钮 */ text-decoration: none; + /* bjy: 将按钮设置为行内块级元素,使其可以设置宽高并与其他元素在同一行显示 */ display: inline-block; + /* bjy: 设置按钮内部文本的字体大小为16像素 */ font-size: 16px; + /* bjy: 设置按钮的外边距,上下4像素,左右2像素,用于控制按钮之间的间距 */ margin: 4px 2px; -} \ No newline at end of file +} diff --git a/src/DjangoBlog/blog/static/account/js/account.js b/src/DjangoBlog/blog/static/account/js/account.js index f1a8771..25e977a 100644 --- a/src/DjangoBlog/blog/static/account/js/account.js +++ b/src/DjangoBlog/blog/static/account/js/account.js @@ -1,45 +1,76 @@ +// bjy: 声明一个全局变量wait,用于倒计时,初始值为60秒 let wait = 60; +// bjy: 定义一个名为time的函数,用于处理按钮的倒计时效果 +// bjy: 参数o代表触发倒计时的按钮元素 function time(o) { + // bjy: 如果倒计时结束(wait为0) if (wait == 0) { + // bjy: 移除按钮的disabled属性,使其重新可点击 o.removeAttribute("disabled"); + // bjy: 将按钮的显示文本恢复为“获取验证码” o.value = "获取验证码"; + // bjy: 重置倒计时变量为60,以便下次使用 wait = 60 + // bjy: 结束函数执行 return false } else { + // bjy: 如果倒计时未结束,禁用按钮,防止重复点击 o.setAttribute("disabled", true); + // bjy: 更新按钮的显示文本,显示剩余的倒计时秒数 o.value = "重新发送(" + wait + ")"; + // bjy: 倒计时秒数减一 wait--; + // bjy: 设置一个1秒(1000毫秒)后执行的定时器 setTimeout(function () { + // bjy: 定时器回调函数中递归调用time函数,实现每秒更新一次倒计时 time(o) }, 1000) } } +// bjy: 为ID为"btn"的元素绑定点击事件处理函数 document.getElementById("btn").onclick = function () { + // bjy: 使用jQuery选择器获取邮箱输入框元素 let id_email = $("#id_email") + // bjy: 使用jQuery选择器获取CSRF令牌的值,用于Django的POST请求安全验证 let token = $("*[name='csrfmiddlewaretoken']").val() + // bjy: 将this(即被点击的按钮)的引用保存到ts变量中,以便在AJAX回调中使用 let ts = this + // bjy: 使用jQuery选择器获取用于显示错误信息的元素 let myErr = $("#myErr") + // bjy: 使用jQuery发起一个AJAX请求 $.ajax( { + // bjy: 请求的URL地址 url: "/forget_password_code/", + // bjy: 请求的类型为POST type: "POST", + // bjy: 发送到服务器的数据,包含邮箱和CSRF令牌 data: { "email": id_email.val(), "csrfmiddlewaretoken": token }, + // bjy: 定义请求成功时的回调函数,result是服务器返回的数据 success: function (result) { + // bjy: 如果服务器返回的结果不是"ok"(表示发送失败或有错误) if (result != "ok") { + // bjy: 移除页面上可能存在的旧错误提示 myErr.remove() + // bjy: 在邮箱输入框后面动态添加一个错误提示列表,显示服务器返回的错误信息 id_email.after("") + // bjy: 结束函数执行 return } + // bjy: 如果发送成功,移除页面上可能存在的旧错误提示 myErr.remove() + // bjy: 调用time函数,开始按钮的倒计时效果 time(ts) }, + // bjy: 定义请求失败时的回调函数,e是错误对象 error: function (e) { + // bjy: 弹出一个警告框,提示用户发送失败 alert("发送失败,请重试") } } diff --git a/src/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js b/src/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js index 3f97ba5..cc9df91 100644 --- a/src/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js +++ b/src/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js @@ -8,44 +8,64 @@ * details, see https://creativecommons.org/licenses/by/3.0/. */ // Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes. +// bjy: 使用一个立即执行函数表达式(IIFE)来创建一个独立的作用域,避免污染全局命名空间 (function () { + // bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作 'use strict'; + // bjy: 定义一个函数,用于从用户代理字符串中获取IE的模拟版本号 function emulatedIEMajorVersion() { + // bjy: 使用正则表达式匹配用户代理字符串中的 "MSIE x.x" 部分 var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent) + // bjy: 如果匹配不到,说明不是IE或版本号无法识别,返回null if (groups === null) { return null } + // bjy: 将匹配到的版本号字符串(如 "10.0")转换为整数 var ieVersionNum = parseInt(groups[1], 10) + // bjy: 取整数部分作为主版本号 var ieMajorVersion = Math.floor(ieVersionNum) + // bjy: 返回模拟的IE主版本号 return ieMajorVersion } + // bjy: 定义一个函数,用于检测当前浏览器实际运行的IE版本,即使它处于旧版IE的模拟模式下 function actualNonEmulatedIEMajorVersion() { - // Detects the actual version of IE in use, even if it's in an older-IE emulation mode. - // IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx - // @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx + // bjy: 此函数通过IE特有的JScript条件编译来检测真实版本 + // bjy: IE JavaScript条件编译文档: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx + // bjy: @cc_on 文档: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx + // bjy: 创建一个新的Function,其内容是IE的条件编译语句,用于获取JScript版本 var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line + // bjy: 如果jscriptVersion未定义,说明是IE11或更高版本,且不在模拟模式下 if (jscriptVersion === undefined) { return 11 // IE11+ not in emulation mode } + // bjy: 如果JScript版本小于9,则判断为IE8或更低版本 if (jscriptVersion < 9) { return 8 // IE8 (or lower; haven't tested on IE<8) } + // bjy: 否则,返回JScript版本,这对应于IE9或IE10(在任何模式下),或IE11在非IE11模式下 return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode } + // bjy: 获取当前浏览器的用户代理字符串 var ua = window.navigator.userAgent + // bjy: 检查用户代理中是否包含'Opera'或'Presto'(Opera的旧版渲染引擎) if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) { return // Opera, which might pretend to be IE } + // bjy: 调用函数,获取模拟的IE版本号 var emulated = emulatedIEMajorVersion() + // bjy: 如果模拟版本为null,说明不是IE浏览器,直接返回 if (emulated === null) { return // Not IE } + // bjy: 调用函数,获取实际的IE版本号 var nonEmulated = actualNonEmulatedIEMajorVersion() + // bjy: 比较模拟版本和实际版本,如果不相同,说明IE正处于模拟模式下 if (emulated !== nonEmulated) { + // bjy: 弹出一个警告框,提示用户当前正处于IE模拟模式,并警告其行为可能与真实旧版IE不同 window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!') } })(); diff --git a/src/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js b/src/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js index 479a6eb..0da33b3 100644 --- a/src/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js +++ b/src/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js @@ -7,17 +7,27 @@ // See the Getting Started docs for more information: // http://getbootstrap.com/getting-started/#support-ie10-width +// bjy: 使用一个立即执行函数表达式(IIFE)来创建一个独立的作用域,避免污染全局命名空间 (function () { + // bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作 'use strict'; + // bjy: 检查当前浏览器的用户代理字符串(User Agent)是否匹配IEMobile/10.0 + // bjy: 这是为了专门识别运行在Windows Phone 8上的IE10移动版浏览器 if (navigator.userAgent.match(/IEMobile\/10\.0/)) { + // bjy: 如果匹配,则创建一个新的