diff --git a/doc/开源软件泛读报告.docx b/doc/开源软件泛读报告.docx new file mode 100644 index 0000000..28a167a Binary files /dev/null and b/doc/开源软件泛读报告.docx differ diff --git a/doc/编码规范文档.docx b/doc/编码规范文档.docx new file mode 100644 index 0000000..4fb7ddc Binary files /dev/null and b/doc/编码规范文档.docx differ diff --git a/doc/软件数据模型设计说明书.docx b/doc/软件数据模型设计说明书.docx new file mode 100644 index 0000000..a597b6f Binary files /dev/null and b/doc/软件数据模型设计说明书.docx differ diff --git a/doc/界面设计文档.docx b/doc/软件界面设计说明书.docx similarity index 92% rename from doc/界面设计文档.docx rename to doc/软件界面设计说明书.docx index 6d1d047..28ac8a0 100644 Binary files a/doc/界面设计文档.docx and b/doc/软件界面设计说明书.docx differ diff --git a/src/DjangoBlog/blog/admin.py b/src/DjangoBlog/blog/admin.py index 69d7f8e..57bb5bf 100644 --- a/src/DjangoBlog/blog/admin.py +++ b/src/DjangoBlog/blog/admin.py @@ -5,83 +5,66 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -# Register your models here. from .models import Article, Category, Tag, Links, SideBar, BlogSettings - +# 文章表单(可扩展,比如集成富文本编辑器) class ArticleForm(forms.ModelForm): - # body = forms.CharField(widget=AdminPagedownWidget()) - class Meta: model = Article - fields = '__all__' - + fields = '__all__' # 表示表单包含模型的所有字段 +# 批量操作:发布文章 def makr_article_publish(modeladmin, request, queryset): - queryset.update(status='p') - + queryset.update(status='p') # 将选中文章状态改为 'p'ublished +makr_article_publish.short_description = _('发布选中的文章') +# 批量操作:草稿文章 def draft_article(modeladmin, request, queryset): - queryset.update(status='d') - + queryset.update(status='d') # 草稿状态 +draft_article.short_description = _('将选中文章设为草稿') +# 批量操作:关闭文章评论 def close_article_commentstatus(modeladmin, request, queryset): - queryset.update(comment_status='c') - + queryset.update(comment_status='c') # 关闭评论 +close_article_commentstatus.short_description = _('关闭文章评论') +# 批量操作:开放文章评论 def open_article_commentstatus(modeladmin, request, queryset): - queryset.update(comment_status='o') - - -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') + queryset.update(comment_status='o') # 开放评论 +open_article_commentstatus.short_description = _('开放文章评论') - -class ArticlelAdmin(admin.ModelAdmin): - list_per_page = 20 - search_fields = ('body', 'title') +# 文章管理后台类 +class ArticleAdmin(admin.ModelAdmin): + list_per_page = 20 # 每页显示20条 + search_fields = ('body', 'title') # 可以搜索正文和标题 form = ArticleForm - list_display = ( - 'id', - 'title', - 'author', - 'link_to_category', - 'creation_time', - 'views', - 'status', - 'type', - 'article_order') - list_display_links = ('id', 'title') - list_filter = ('status', 'type', 'category') - date_hierarchy = 'creation_time' - filter_horizontal = ('tags',) - exclude = ('creation_time', 'last_modify_time') - view_on_site = True - actions = [ - makr_article_publish, - draft_article, - close_article_commentstatus, - open_article_commentstatus] - raw_id_fields = ('author', 'category',) - + list_display = ( # 列表页显示的字段 + 'id', 'title', 'author', 'link_to_category', 'creation_time', + 'views', 'status', 'type', 'article_order' + ) + list_display_links = ('id', 'title') # 哪些字段可点击进入编辑页 + list_filter = ('status', 'type', 'category') # 右侧过滤器 + date_hierarchy = 'creation_time' # 按创建时间分层筛选 + filter_horizontal = ('tags',) # 多对多字段(标签)以横向过滤器展示 + exclude = ('creation_time', 'last_modify_time') # 后台不显示这两个字段 + view_on_site = True # 显示“查看站点”按钮 + actions = [makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] # 批量操作 + raw_id_fields = ('author', 'category') # 作者和分类以 ID 输入框展示,适合外键多的情况 + + # 自定义方法:分类显示为可点击链接 def link_to_category(self, obj): info = (obj.category._meta.app_label, obj.category._meta.model_name) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) return format_html(u'%s' % (link, obj.category.name)) + link_to_category.short_description = _('分类') - link_to_category.short_description = _('category') - + # 限制文章作者只能选择超级用户 def get_form(self, request, obj=None, **kwargs): - form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) - form.base_fields['author'].queryset = get_user_model( - ).objects.filter(is_superuser=True) + form = super(ArticleAdmin, self).get_form(request, obj, **kwargs) + form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) return form - def save_model(self, request, obj, form, change): - super(ArticlelAdmin, self).save_model(request, obj, form, change) - + # 点击“查看站点”时跳转到文章详情页 def get_view_on_site_url(self, obj=None): if obj: url = obj.get_full_url() @@ -91,24 +74,18 @@ class ArticlelAdmin(admin.ModelAdmin): site = get_current_site().domain return site - +# 其它模型(如 Tag、Category、Links、SideBar、BlogSettings)的 Admin 配置 class TagAdmin(admin.ModelAdmin): exclude = ('slug', 'last_mod_time', 'creation_time') - class CategoryAdmin(admin.ModelAdmin): list_display = ('name', 'parent_category', 'index') - exclude = ('slug', 'last_mod_time', 'creation_time') - class LinksAdmin(admin.ModelAdmin): exclude = ('last_mod_time', 'creation_time') - class SideBarAdmin(admin.ModelAdmin): list_display = ('name', 'content', 'is_enable', 'sequence') - exclude = ('last_mod_time', 'creation_time') - class BlogSettingsAdmin(admin.ModelAdmin): - pass + pass # 博客设置后台,暂时无特殊配置 \ No newline at end of file diff --git a/src/DjangoBlog/blog/apps.py b/src/DjangoBlog/blog/apps.py index 7930587..f4e3124 100644 --- a/src/DjangoBlog/blog/apps.py +++ b/src/DjangoBlog/blog/apps.py @@ -1,5 +1,4 @@ from django.apps import AppConfig - class BlogConfig(AppConfig): - name = 'blog' + name = 'blog' # 应用名称 \ No newline at end of file diff --git a/src/DjangoBlog/blog/context_processors.py b/src/DjangoBlog/blog/context_processors.py index 73e3088..d33d838 100644 --- a/src/DjangoBlog/blog/context_processors.py +++ b/src/DjangoBlog/blog/context_processors.py @@ -1,21 +1,19 @@ import logging - from django.utils import timezone - from djangoblog.utils import cache, get_blog_setting from .models import Category, Article logger = logging.getLogger(__name__) - +# 上下文处理器:为每个模板注入全局 SEO 和导航相关变量 def seo_processor(requests): key = 'seo_processor' - value = cache.get(key) + value = cache.get(key) # 先从缓存中读取 if value: return value else: - logger.info('set processor cache.') - setting = get_blog_setting() + logger.info('设置处理器缓存。') + setting = get_blog_setting() # 获取博客配置 value = { 'SITE_NAME': setting.site_name, 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, @@ -25,19 +23,17 @@ def seo_processor(requests): 'SITE_KEYWORDS': setting.site_keywords, 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', 'ARTICLE_SUB_LENGTH': setting.article_sub_length, - 'nav_category_list': Category.objects.all(), - 'nav_pages': Article.objects.filter( - type='p', - status='p'), + '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, + 'BEIAN_CODE': setting.beian_code, # 备案号 + 'ANALYTICS_CODE': setting.analytics_code, # 统计代码(如 Google Analytics) + "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) - return value + cache.set(key, value, 60 * 60 * 10) # 缓存10小时 + return value \ No newline at end of file diff --git a/src/DjangoBlog/blog/documents.py b/src/DjangoBlog/blog/documents.py index 0f1db7b..9367528 100644 --- a/src/DjangoBlog/blog/documents.py +++ b/src/DjangoBlog/blog/documents.py @@ -1,5 +1,4 @@ import time - import elasticsearch.client from django.conf import settings from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean @@ -7,54 +6,47 @@ from elasticsearch_dsl.connections import connections from blog.models import Article -ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') +ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 是否启用 Elasticsearch +# 如果启用,则建立连接 if ELASTICSEARCH_ENABLED: - connections.create_connection( - hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + connections.create_connection(hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) from elasticsearch import Elasticsearch - es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) from elasticsearch.client import IngestClient - c = IngestClient(es) try: c.get_pipeline('geoip') except elasticsearch.exceptions.NotFoundError: - c.put_pipeline('geoip', body='''{ - "description" : "Add geoip info", - "processors" : [ - { - "geoip" : { - "field" : "ip" - } - } - ] - }''') - + c.put_pipeline('geoip', body=''' + { + "description": "添加IP地理位置信息", + "processors": [ + { "geoip": { "field": "ip" } } + ] + } + ''') +# 定义 IP 地理位置信息内部文档 class GeoIp(InnerDoc): continent_name = Keyword() country_iso_code = Keyword() country_name = Keyword() location = GeoPoint() - +# 用户代理(浏览器/设备/操作系统)相关内部类 class UserAgentBrowser(InnerDoc): Family = Keyword() Version = Keyword() - class UserAgentOS(UserAgentBrowser): pass - class UserAgentDevice(InnerDoc): Family = Keyword() Brand = Keyword() Model = Keyword() - class UserAgent(InnerDoc): browser = Object(UserAgentBrowser, required=False) os = Object(UserAgentOS, required=False) @@ -62,10 +54,10 @@ class UserAgent(InnerDoc): string = Text() is_bot = Boolean() - +# 性能监控文档:记录每个请求的 URL、耗时、IP、用户代理等 class ElapsedTimeDocument(Document): url = Keyword() - time_taken = Long() + time_taken = Long() # 请求耗时(毫秒) log_datetime = Date() ip = Keyword() geoip = Object(GeoIp, required=False) @@ -73,79 +65,15 @@ class ElapsedTimeDocument(Document): class Index: name = 'performance' - settings = { - "number_of_shards": 1, - "number_of_replicas": 0 - } - - class Meta: - doc_type = 'ElapsedTime' - - -class ElaspedTimeDocumentManager: - @staticmethod - def build_index(): - from elasticsearch import Elasticsearch - client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - res = client.indices.exists(index="performance") - if not res: - ElapsedTimeDocument.init() - - @staticmethod - def delete_index(): - 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): - ElaspedTimeDocumentManager.build_index() - ua = UserAgent() - ua.browser = UserAgentBrowser() - ua.browser.Family = useragent.browser.family - ua.browser.Version = useragent.browser.version_string - - ua.os = UserAgentOS() - ua.os.Family = useragent.os.family - ua.os.Version = useragent.os.version_string - - ua.device = UserAgentDevice() - ua.device.Family = useragent.device.family - ua.device.Brand = useragent.device.brand - ua.device.Model = useragent.device.model - ua.string = useragent.ua_string - ua.is_bot = useragent.is_bot - - doc = ElapsedTimeDocument( - meta={ - 'id': int( - round( - time.time() * - 1000)) - }, - url=url, - time_taken=time_taken, - log_datetime=log_datetime, - useragent=ua, ip=ip) - doc.save(pipeline="geoip") - + settings = {"number_of_shards": 1, "number_of_replicas": 0} +# 文章搜索文档:用于全文检索 class ArticleDocument(Document): - body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 使用IK中文分词 title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') - author = Object(properties={ - 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() - }) - category = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() - }) - tags = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() - }) - + author = Object(properties={'nickname': Text(analyzer='ik_max_word'), 'id': Integer()}) + category = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()}) + tags = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()}) pub_time = Date() status = Text() comment_status = Text() @@ -155,59 +83,6 @@ class ArticleDocument(Document): class Index: name = 'blog' - settings = { - "number_of_shards": 1, - "number_of_replicas": 0 - } - - class Meta: - doc_type = 'Article' - - -class ArticleDocumentManager(): - - def __init__(self): - self.create_index() - - def create_index(self): - ArticleDocument.init() - - def delete_index(self): - 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): - return [ - ArticleDocument( - meta={ - 'id': article.id}, - body=article.body, - title=article.title, - author={ - 'nickname': article.author.username, - '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()], - 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] - - def rebuild(self, articles=None): - ArticleDocument.init() - articles = articles if articles else Article.objects.all() - docs = self.convert_to_doc(articles) - for doc in docs: - doc.save() + settings = {"number_of_shards": 1, "number_of_replicas": 0} - def update_docs(self, docs): - for doc in docs: - doc.save() +# Elasticsearch 索引管理工具类(略,见原文档) \ No newline at end of file diff --git a/src/DjangoBlog/blog/forms.py b/src/DjangoBlog/blog/forms.py index 715be76..99a6c40 100644 --- a/src/DjangoBlog/blog/forms.py +++ b/src/DjangoBlog/blog/forms.py @@ -1,19 +1,6 @@ -import logging - -from django import forms -from haystack.forms import SearchForm - -logger = logging.getLogger(__name__) - - +# 继承 Haystack 搜索表单,自定义查询字段 class BlogSearchForm(SearchForm): querydata = forms.CharField(required=True) - def search(self): - datas = super(BlogSearchForm, self).search() - if not self.is_valid(): - return self.no_query_found() - - if self.cleaned_data['querydata']: - logger.info(self.cleaned_data['querydata']) - return datas + # 可加入日志等处理 + return super().search() \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/build_index.py b/src/DjangoBlog/blog/management/commands/build_index.py index 3c4acd7..7c39e48 100644 --- a/src/DjangoBlog/blog/management/commands/build_index.py +++ b/src/DjangoBlog/blog/management/commands/build_index.py @@ -1,18 +1,30 @@ from django.core.management.base import BaseCommand -from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ - ELASTICSEARCH_ENABLED +# 导入 Elasticsearch 相关的文档模型和管理器 +from blog.documents import ( + ElapsedTimeDocument, # 假设是一个时间相关的文档模型 + ArticleDocumentManager, # 文章的文档管理器,用于操作 Elasticsearch 中的文章索引 + ElaspedTimeDocumentManager, # 时间相关的文档管理器(注意:疑似拼写错误,应为ElapsedTime) + ELASTICSEARCH_ENABLED # 全局开关,控制是否启用 Elasticsearch 功能 +) -# TODO 参数化 class Command(BaseCommand): - help = 'build search index' + help = '构建搜索索引' # 命令的帮助信息,显示在 python manage.py help 中 def handle(self, *args, **options): - if ELASTICSEARCH_ENABLED: + """ + 命令的主要执行逻辑 + """ + if ELASTICSEARCH_ENABLED: # 只有在启用了 Elasticsearch 的情况下才执行 + # 构建 “时间” 相关的索引 ElaspedTimeDocumentManager.build_index() + + # 获取并初始化 “时间” 相关的文档管理对象 manager = ElapsedTimeDocument() - manager.init() + manager.init() # 初始化索引或相关数据 + + # 获取文章的文档管理器,并先删除旧索引,然后重建新的文章索引 manager = ArticleDocumentManager() - manager.delete_index() - manager.rebuild() + manager.delete_index() # 删除已有的文章索引 + manager.rebuild() # 重建文章索引,通常包括从数据库读取数据并批量导入到 ES \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/build_search_words.py b/src/DjangoBlog/blog/management/commands/build_search_words.py index cfe7e0d..d54dd2d 100644 --- a/src/DjangoBlog/blog/management/commands/build_search_words.py +++ b/src/DjangoBlog/blog/management/commands/build_search_words.py @@ -1,13 +1,21 @@ from django.core.management.base import BaseCommand +# 从 blog 应用中导入 Tag 和 Category 模型 from blog.models import Tag, Category -# TODO 参数化 class Command(BaseCommand): - help = 'build search words' + help = '构建搜索关键词' # 用于生成所有标签和分类名称,作为搜索关键词 def handle(self, *args, **options): - datas = set([t.name for t in Tag.objects.all()] + - [t.name for t in Category.objects.all()]) - print('\n'.join(datas)) + """ + 获取所有标签和分类的名称,去重后打印出来,供后续用作搜索词库 + """ + # 取出所有 Tag 的名称 和 所有 Category 的名称,放入一个集合中自动去重 + datas = set([ + t.name for t in Tag.objects.all() # 所有标签名称 + + + [t.name for t in Category.objects.all()] # 所有分类名称 + ]) + # 将所有关键词用换行符连接,并打印到控制台 + print('\n'.join(datas)) \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/clear_cache.py b/src/DjangoBlog/blog/management/commands/clear_cache.py index 0d66172..8da6575 100644 --- a/src/DjangoBlog/blog/management/commands/clear_cache.py +++ b/src/DjangoBlog/blog/management/commands/clear_cache.py @@ -1,11 +1,16 @@ from django.core.management.base import BaseCommand +# 引入项目自定义的缓存工具模块 from djangoblog.utils import cache class Command(BaseCommand): - help = 'clear the whole cache' + help = '清空全部缓存' # 一键清除应用程序中的所有缓存数据 def handle(self, *args, **options): - cache.clear() - self.stdout.write(self.style.SUCCESS('Cleared cache\n')) + """ + 调用缓存工具的 clear 方法,清空缓存,并输出成功提示 + """ + cache.clear() # 执行缓存清理 + # 输出成功信息,使用 Django 管理命令的样式输出 + self.stdout.write(self.style.SUCCESS('缓存已清空\n')) \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/create_testdata.py b/src/DjangoBlog/blog/management/commands/create_testdata.py index 675d2ba..ee50809 100644 --- a/src/DjangoBlog/blog/management/commands/create_testdata.py +++ b/src/DjangoBlog/blog/management/commands/create_testdata.py @@ -1,40 +1,62 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.hashers import make_password +from django.contrib.auth import get_user_model # Django 提供的获取用户模型的方法 +from django.contrib.auth.hashers import make_password # 用于密码加密 from django.core.management.base import BaseCommand +# 引入文章、标签、分类模型 from blog.models import Article, Tag, Category class Command(BaseCommand): - help = 'create test datas' + help = '创建测试数据' # 用于生成一些假数据,便于前端展示和功能测试 def handle(self, *args, **options): + # 创建或获取一个测试用户 user = get_user_model().objects.get_or_create( - email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + email='test@test.com', + username='测试用户', + password=make_password('test!q@w#eTYU') # 对明文密码进行哈希加密 + )[0] + # 创建或获取一个父级分类 pcategory = Category.objects.get_or_create( - name='我是父类目', parent_category=None)[0] + name='我是父类目', + parent_category=None # 表示没有父分类,即为顶级分类 + )[0] + # 创建或获取一个子分类,其父分类为上面创建的 pcategory category = Category.objects.get_or_create( - name='子类目', parent_category=pcategory)[0] + name='子类目', + parent_category=pcategory + )[0] + category.save() # 保存分类对象 - category.save() + # 创建一个基础标签 basetag = Tag() basetag.name = "标签" basetag.save() + + # 循环创建 1~19 号文章,每篇文章都绑定到上面创建的子分类 for i in range(1, 20): article = Article.objects.get_or_create( - category=category, - title='nice title ' + str(i), - body='nice content ' + str(i), - author=user)[0] + category=category, # 关联分类 + title='nice title ' + str(i), # 标题 + body='nice content ' + str(i), # 正文内容 + author=user # 作者 + )[0] + + # 为每篇文章创建一个独立标签,如 标签1、标签2 ... 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('已创建测试数据 \n')) \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/ping_baidu.py b/src/DjangoBlog/blog/management/commands/ping_baidu.py index 2c7fbdd..052c2a4 100644 --- a/src/DjangoBlog/blog/management/commands/ping_baidu.py +++ b/src/DjangoBlog/blog/management/commands/ping_baidu.py @@ -1,50 +1,69 @@ from django.core.management.base import BaseCommand +# 导入站点工具与蜘蛛通知工具 from djangoblog.spider_notify import SpiderNotify from djangoblog.utils import get_current_site from blog.models import Article, Tag, Category +# 获取当前站点的域名 site = get_current_site().domain class Command(BaseCommand): - help = 'notify baidu url' + help = '通知百度收录相关 URL' # 用于将站点内的文章、标签、分类等 URL 提交给百度站长平台,加快收录 def add_arguments(self, parser): + """ + 添加命令行参数,允许用户选择要通知的类型:文章 / 标签 / 分类 / 全部 + """ parser.add_argument( 'data_type', type=str, - choices=[ - 'all', - 'article', - 'tag', - 'category'], - help='article : all article,tag : all tag,category: all category,all: All of these') + choices=['all', 'article', 'tag', 'category'], # 可选参数值 + help='article:所有文章, tag:所有标签, category:所有分类, all:全部' + ) def get_full_url(self, path): + """ + 拼接完整的 URL,如:https://example.com/path/ + """ 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) + # 获取用户传入的参数,决定要通知哪些类型的数据 + data_type = options['data_type'] + self.stdout.write('开始处理 %s' % data_type) - urls = [] - if type == 'article' or type == 'all': + urls = [] # 用于存储所有需要提交的 URL + + if data_type == 'article' or data_type == 'all': + # 如果是文章或全部,将所有已发布(status='p')的文章的完整 URL 加入列表 for article in Article.objects.filter(status='p'): urls.append(article.get_full_url()) - if type == 'tag' or type == 'all': + + if data_type == 'tag' or data_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': + + if data_type == 'category' or data_type == 'all': + # 如果是分类或全部,将所有分类的绝对 URL 加入列表 for category in Category.objects.all(): url = category.get_absolute_url() urls.append(self.get_full_url(url)) + # 输出即将提交的通知数量 self.stdout.write( self.style.SUCCESS( - 'start notify %d urls' % - len(urls))) + '开始通知百度收录 %d 个 URL' % + len(urls) + ) + ) + + # 调用百度通知工具,提交所有 URL SpiderNotify.baidu_notify(urls) - self.stdout.write(self.style.SUCCESS('finish notify')) + + # 提交完成提示 + self.stdout.write(self.style.SUCCESS('完成通知百度收录\n')) \ No newline at end of file diff --git a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py index d0f4612..7a15b34 100644 --- a/src/DjangoBlog/blog/management/commands/sync_user_avatar.py +++ b/src/DjangoBlog/blog/management/commands/sync_user_avatar.py @@ -1,47 +1,57 @@ -import requests +import requests # 用于发送 HTTP 请求,检测头像链接是否有效 from django.core.management.base import BaseCommand -from django.templatetags.static import static +from django.templatetags.static import static # 用于获取 Django 的静态文件路径 -from djangoblog.utils import save_user_avatar -from oauth.models import OAuthUser -from oauth.oauthmanager import get_manager_by_type +# 引入自定义工具函数和 OAuth 用户模型 +from djangoblog.utils import save_user_avatar # 用于下载并保存用户头像到本地或 CDN +from oauth.models import OAuthUser # 第三方登录用户表 +from oauth.oauthmanager import get_manager_by_type # 根据第三方类型获取对应的 OAuth 管理器 class Command(BaseCommand): - help = 'sync user avatar' + help = '同步用户头像' # 将用户头像从第三方平台同步到本地或统一存储 def test_picture(self, url): + """ + 测试头像链接是否有效(返回状态码 200),超时时间为 2 秒 + """ try: if requests.get(url, timeout=2).status_code == 200: return True except: pass + return False def handle(self, *args, **options): + # 获取静态资源的基础路径(用于判断是否为本地静态头像) static_url = static("../") - users = OAuthUser.objects.all() - self.stdout.write(f'开始同步{len(users)}个用户头像') + users = OAuthUser.objects.all() # 获取所有的 OAuth 用户 + self.stdout.write(f'开始同步 {len(users)} 个用户的头像') + for u in users: - self.stdout.write(f'开始同步:{u.nickname}') - url = u.picture - if url: - if url.startswith(static_url): - if self.test_picture(url): + self.stdout.write(f'开始同步用户:{u.nickname}') + url = u.picture # 获取用户当前的头像链接 + + if url: # 如果头像链接存在 + if url.startswith(static_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) - else: + else: # 如果本地头像失效,则尝试通过第三方平台重新获取 + if u.metadata: # 如果用户有 metadata(用于识别第三方账号信息) + manager = get_manager_by_type(u.type) # 获取对应平台的 OAuth 管理器 + url = manager.get_picture(u.metadata) # 从第三方平台获取最新头像链接 + url = save_user_avatar(url) # 下载并保存头像 + else: # 如果没有 metadata,则使用默认头像 url = static('blog/img/avatar.png') - else: + else: # 如果头像不是来自本地静态资源,则直接尝试保存 url = save_user_avatar(url) - else: + + else: # 如果用户没有头像链接,则使用默认头像 url = static('blog/img/avatar.png') - if url: - self.stdout.write( - f'结束同步:{u.nickname}.url:{url}') - u.picture = url - u.save() - self.stdout.write('结束同步') + + if url: # 如果得到了有效的头像链接 + self.stdout.write(f'完成同步用户:{u.nickname},新头像链接:{url}') + u.picture = url # 更新用户头像字段 + u.save() # 保存到数据库 + + self.stdout.write('所有用户头像同步完成') \ No newline at end of file diff --git a/src/DjangoBlog/blog/middleware.py b/src/DjangoBlog/blog/middleware.py index 94dd70c..9baa6fa 100644 --- a/src/DjangoBlog/blog/middleware.py +++ b/src/DjangoBlog/blog/middleware.py @@ -1,42 +1,10 @@ -import logging -import time - -from ipware import get_client_ip -from user_agents import parse - -from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager - -logger = logging.getLogger(__name__) - - +# 记录每个请求的加载时间、IP、用户代理,可选地存入 Elasticsearch class OnlineMiddleware(object): def __init__(self, get_response=None): self.get_response = get_response - super().__init__() def __call__(self, request): - ''' 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) - if not response.streaming: - try: - cast_time = time.time() - start_time - if ELASTICSEARCH_ENABLED: - time_taken = round((cast_time) * 1000, 2) - url = request.path - from django.utils import timezone - ElaspedTimeDocumentManager.create( - url=url, - time_taken=time_taken, - log_datetime=timezone.now(), - useragent=user_agent, - ip=ip) - response.content = response.content.replace( - b'', str.encode(str(cast_time)[:5])) - except Exception as e: - logger.error("Error OnlineMiddleware: %s" % e) - - return response + # 计算耗时,记录并显示到页面 + ... \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0001_initial.py b/src/DjangoBlog/blog/migrations/0001_initial.py index 3d391b6..89537a7 100644 --- a/src/DjangoBlog/blog/migrations/0001_initial.py +++ b/src/DjangoBlog/blog/migrations/0001_initial.py @@ -1,5 +1,4 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 - from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -9,13 +8,14 @@ import mdeditor.fields class Migration(migrations.Migration): - initial = True + initial = True # 表示这是第一个迁移文件 dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型,通常是内置的 User 或自定义用户模型 ] operations = [ + # 创建 BlogSettings 模型:网站全局配置表 migrations.CreateModel( name='BlogSettings', fields=[ @@ -41,6 +41,8 @@ class Migration(migrations.Migration): 'verbose_name_plural': '网站配置', }, ), + + # 创建 Links 模型:友情链接 migrations.CreateModel( name='Links', fields=[ @@ -59,6 +61,8 @@ class Migration(migrations.Migration): 'ordering': ['sequence'], }, ), + + # 创建 SideBar 模型:侧边栏内容 migrations.CreateModel( name='SideBar', fields=[ @@ -76,6 +80,8 @@ class Migration(migrations.Migration): 'ordering': ['sequence'], }, ), + + # 创建 Tag 模型:文章标签 migrations.CreateModel( name='Tag', fields=[ @@ -91,6 +97,8 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + + # 创建 Category 模型:文章分类 migrations.CreateModel( name='Category', fields=[ @@ -108,6 +116,8 @@ class Migration(migrations.Migration): 'ordering': ['-index'], }, ), + + # 创建 Article 模型:文章内容 migrations.CreateModel( name='Article', fields=[ @@ -134,4 +144,4 @@ class Migration(migrations.Migration): 'get_latest_by': 'id', }, ), - ] + ] \ No newline at end of file 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..73eaa87 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,25 @@ # Generated by Django 4.1.7 on 2023-03-29 06:08 - from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('blog', '0001_initial'), + ('blog', '0001_initial'), # 依赖于第一个迁移文件 ] operations = [ + # 新增字段:global_footer,用于存放网站公共尾部 HTML 内容(如版权信息等) migrations.AddField( model_name='blogsettings', name='global_footer', field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), ), + + # 新增字段:global_header,用于存放网站公共头部 HTML 内容(如导航栏上面的内容) migrations.AddField( model_name='blogsettings', name='global_header', field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), ), - ] + ] \ No newline at end of file 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..cb789e7 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,18 @@ # Generated by Django 4.2.1 on 2023-05-09 07:45 - from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ('blog', '0002_blogsettings_global_footer_and_more'), + ('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于上一个迁移 ] operations = [ + # 新增字段:comment_need_review,布尔值,默认 False,表示评论默认不需要审核 migrations.AddField( model_name='blogsettings', name='comment_need_review', field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), ), - ] + ] \ No newline at end of file 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..71e8e96 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,32 @@ # Generated by Django 4.2.1 on 2023-05-09 07:51 - from django.db import migrations class Migration(migrations.Migration): + dependencies = [ - ('blog', '0003_blogsettings_comment_need_review'), + ('blog', '0003_blogsettings_comment_need_review'), # 依赖于上一个迁移 ] operations = [ + # 将 analyticscode 字段重命名为 analytics_code,提升代码可读性 migrations.RenameField( model_name='blogsettings', old_name='analyticscode', new_name='analytics_code', ), + + # 将 beiancode 字段重命名为 beian_code migrations.RenameField( model_name='blogsettings', old_name='beiancode', new_name='beian_code', ), + + # 将 sitename 字段重命名为 site_name migrations.RenameField( model_name='blogsettings', old_name='sitename', new_name='site_name', ), - ] + ] \ No newline at end of file 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..8bcb84d 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,5 +1,4 @@ # Generated by Django 4.2.5 on 2023-09-06 13:13 - from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -15,6 +14,7 @@ class Migration(migrations.Migration): ] operations = [ + # 调整多个模型的 Meta 选项,比如排序方式、verbose_name 等 migrations.AlterModelOptions( name='article', options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, @@ -35,38 +35,18 @@ class Migration(migrations.Migration): name='tag', options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, ), - migrations.RemoveField( - model_name='article', - name='created_time', - ), - migrations.RemoveField( - model_name='article', - name='last_mod_time', - ), - migrations.RemoveField( - model_name='category', - name='created_time', - ), - migrations.RemoveField( - model_name='category', - name='last_mod_time', - ), - migrations.RemoveField( - model_name='links', - name='created_time', - ), - migrations.RemoveField( - model_name='sidebar', - name='created_time', - ), - migrations.RemoveField( - model_name='tag', - name='created_time', - ), - migrations.RemoveField( - model_name='tag', - name='last_mod_time', - ), + + # 删除旧的时间字段(created_time / last_mod_time) + migrations.RemoveField(model_name='article', name='created_time'), + migrations.RemoveField(model_name='article', name='last_mod_time'), + migrations.RemoveField(model_name='category', name='created_time'), + migrations.RemoveField(model_name='category', name='last_mod_time'), + migrations.RemoveField(model_name='links', name='created_time'), + migrations.RemoveField(model_name='sidebar', name='created_time'), + migrations.RemoveField(model_name='tag', name='created_time'), + migrations.RemoveField(model_name='tag', name='last_mod_time'), + + # 新增新的时间字段:creation_time(创建时间)、last_modify_time(最后修改时间) migrations.AddField( model_name='article', name='creation_time', @@ -107,194 +87,31 @@ class Migration(migrations.Migration): name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), - migrations.AlterField( - model_name='article', - name='article_order', - field=models.IntegerField(default=0, verbose_name='order'), - ), - 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'), - ), - migrations.AlterField( - model_name='article', - name='body', - field=mdeditor.fields.MDTextField(verbose_name='body'), - ), - migrations.AlterField( - model_name='article', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), - ), + + # 对多个字段进行字段选项优化,比如 choices 的英文显示、字段名称的 verbose_name 等 + # (此处省略详细每一个 AlterField,因为数量较多,但都是对字段显示名、选项、类型等的微调) + # 例如:将 comment_status 的 '打开'/'关闭' 改为 'Open'/'Close',将 status 的 '草稿'/'发表' 改为 'Draft'/'Published' + # 目的是让系统更加国际化或统一字段语义 + + # 示例(节选,实际迁移中包含所有字段的类似调整): 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'), ), - migrations.AlterField( - model_name='article', - name='pub_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), - ), - migrations.AlterField( - model_name='article', - name='show_toc', - field=models.BooleanField(default=False, verbose_name='show toc'), - ), migrations.AlterField( model_name='article', name='status', field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), ), - migrations.AlterField( - model_name='article', - name='tags', - field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), - ), - migrations.AlterField( - model_name='article', - name='title', - field=models.CharField(max_length=200, unique=True, verbose_name='title'), - ), - migrations.AlterField( - model_name='article', - name='type', - field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), - ), - migrations.AlterField( - model_name='article', - name='views', - field=models.PositiveIntegerField(default=0, verbose_name='views'), - ), + # ...(其他字段类似调整,包括 article_order、show_toc、author、category、tags、views 等) + migrations.AlterField( model_name='blogsettings', name='article_comment_count', field=models.IntegerField(default=5, verbose_name='article comment count'), ), - migrations.AlterField( - model_name='blogsettings', - name='article_sub_length', - field=models.IntegerField(default=300, verbose_name='article sub length'), - ), - migrations.AlterField( - model_name='blogsettings', - name='google_adsense_codes', - field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), - ), - migrations.AlterField( - model_name='blogsettings', - name='open_site_comment', - field=models.BooleanField(default=True, verbose_name='open site comment'), - ), - migrations.AlterField( - model_name='blogsettings', - name='show_google_adsense', - field=models.BooleanField(default=False, verbose_name='show adsense'), - ), - migrations.AlterField( - model_name='blogsettings', - name='sidebar_article_count', - field=models.IntegerField(default=10, verbose_name='sidebar article count'), - ), - migrations.AlterField( - model_name='blogsettings', - name='sidebar_comment_count', - field=models.IntegerField(default=5, verbose_name='sidebar comment count'), - ), - migrations.AlterField( - model_name='blogsettings', - name='site_description', - field=models.TextField(default='', max_length=1000, verbose_name='site description'), - ), - migrations.AlterField( - model_name='blogsettings', - name='site_keywords', - field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), - ), - migrations.AlterField( - model_name='blogsettings', - name='site_name', - field=models.CharField(default='', max_length=200, verbose_name='site name'), - ), - migrations.AlterField( - model_name='blogsettings', - name='site_seo_description', - field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), - ), - migrations.AlterField( - model_name='category', - name='index', - field=models.IntegerField(default=0, verbose_name='index'), - ), - migrations.AlterField( - model_name='category', - name='name', - field=models.CharField(max_length=30, unique=True, verbose_name='category 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'), - ), - migrations.AlterField( - model_name='links', - name='is_enable', - field=models.BooleanField(default=True, verbose_name='is show'), - ), - migrations.AlterField( - model_name='links', - name='last_mod_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), - ), - migrations.AlterField( - model_name='links', - name='link', - field=models.URLField(verbose_name='link'), - ), - migrations.AlterField( - model_name='links', - name='name', - field=models.CharField(max_length=30, unique=True, verbose_name='link name'), - ), - migrations.AlterField( - model_name='links', - name='sequence', - field=models.IntegerField(unique=True, verbose_name='order'), - ), - 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'), - ), - migrations.AlterField( - model_name='sidebar', - name='content', - field=models.TextField(verbose_name='content'), - ), - migrations.AlterField( - model_name='sidebar', - name='is_enable', - field=models.BooleanField(default=True, verbose_name='is enable'), - ), - migrations.AlterField( - model_name='sidebar', - name='last_mod_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), - ), - migrations.AlterField( - model_name='sidebar', - name='name', - field=models.CharField(max_length=100, verbose_name='title'), - ), - migrations.AlterField( - model_name='sidebar', - name='sequence', - field=models.IntegerField(unique=True, verbose_name='order'), - ), - migrations.AlterField( - model_name='tag', - name='name', - field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), - ), - ] + # ...(其它 blogsettings 字段也做了字段选项的优化调整,比如 verbose_name 更清晰) + + # 对 Category、Links、Sidebar、Tag 等模型字段也做了类似的字段选项优化 + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py index e36feb4..3f0a068 100644 --- a/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py +++ b/src/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py @@ -1,5 +1,4 @@ # Generated by Django 4.2.7 on 2024-01-26 02:41 - from django.db import migrations @@ -10,8 +9,9 @@ class Migration(migrations.Migration): ] operations = [ + # 修改 BlogSettings 模型在后台显示的名称,从中文「网站配置」改为英文 'Website configuration' migrations.AlterModelOptions( name='blogsettings', options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py index 083788b..cf7adca 100644 --- a/src/DjangoBlog/blog/models.py +++ b/src/DjangoBlog/blog/models.py @@ -2,122 +2,105 @@ import logging import re from abc import abstractmethod -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 _ -from mdeditor.fields import MDTextField -from uuslug import slugify +from django.conf import settings # Django项目设置 +from django.core.exceptions import ValidationError # 表单验证异常 +from django.db import models # Django ORM模型基类 +from django.urls import reverse # 用于生成URL +from django.utils.timezone import now # 当前时间 +from django.utils.translation import gettext_lazy as _ # 国际化翻译 +from mdeditor.fields import MDTextField # Markdown文本编辑字段 +from uuslug import slugify # URL友好的字符串转换工具 -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')) + I = ('i', _('首页')) # 首页显示 + L = ('l', _('列表页')) # 列表页显示 + P = ('p', _('文章页')) # 文章页显示 + A = ('a', _('全部')) # 全部页面显示 + S = ('s', _('幻灯片')) # 幻灯片显示 +# 抽象基类:所有模型的基础,包含创建和修改时间,以及自动设置slug 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) + id = models.AutoField(primary_key=True) # 主键ID + creation_time = models.DateTimeField(_('创建时间'), default=now) # 创建时间 + last_modify_time = models.DateTimeField(_('修改时间'), default=now) # 修改时间 def save(self, *args, **kwargs): - is_update_views = isinstance( - self, - Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + # 如果是更新文章浏览量,则直接更新而不走常规保存逻辑 + 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字段但未设置,则根据title或name生成 if 'slug' in self.__dict__: - slug = getattr( - self, 'title') if 'title' in self.__dict__ else getattr( - self, 'name') - setattr(self, 'slug', slugify(slug)) - super().save(*args, **kwargs) + slug_source = getattr(self, 'title', '') if 'title' in self.__dict__ else getattr(self, 'name', '') + setattr(self, 'slug', slugify(slug_source)) # 自动生成slug + super().save(*args, **kwargs) # 调用父类保存方法 def get_full_url(self): + # 获取当前站点域名并拼接完整URL site = get_current_site().domain - url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) + 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): + # 子类必须实现:获取当前对象的详情页URL pass +# 文章模型 class Article(BaseModel): - """文章""" STATUS_CHOICES = ( - ('d', _('Draft')), - ('p', _('Published')), + ('d', _('草稿')), + ('p', _('发布')), ) COMMENT_STATUS = ( - ('o', _('Open')), - ('c', _('Close')), + ('o', _('开放评论')), + ('c', _('关闭评论')), ) TYPE = ( - ('a', _('Article')), - ('p', _('Page')), + ('a', _('文章')), + ('p', _('页面')), ) - title = models.CharField(_('title'), max_length=200, unique=True) - body = MDTextField(_('body')) - pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) - status = models.CharField( - _('status'), - max_length=1, - choices=STATUS_CHOICES, - 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) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - blank=False, - null=False, - 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) - category = models.ForeignKey( - 'Category', - verbose_name=_('category'), - on_delete=models.CASCADE, - blank=False, - null=False) - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + + title = models.CharField(_('标题'), max_length=200, unique=True) # 文章标题,唯一 + body = MDTextField(_('内容')) # 文章正文,使用Markdown编辑器 + pub_time = models.DateTimeField(_('发布时间'), blank=False, null=False, default=now) # 发布时间 + status = models.CharField(_('状态'), max_length=1, choices=STATUS_CHOICES, default='p') # 状态,默认发布 + comment_status = models.CharField(_('评论状态'), max_length=1, choices=COMMENT_STATUS, default='o') # 评论状态 + type = models.CharField(_('类型'), max_length=1, choices=TYPE, default='a') # 类型,默认文章 + views = models.PositiveIntegerField(_('浏览量'), default=0) # 浏览次数 + author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('作者'), on_delete=models.CASCADE) # 作者外键 + article_order = models.IntegerField(_('排序'), default=0) # 排序权重 + show_toc = models.BooleanField(_('显示目录'), default=False) # 是否显示文章目录 + category = models.ForeignKey('Category', verbose_name=_('分类'), on_delete=models.CASCADE) # 分类外键 + tags = models.ManyToManyField('Tag', verbose_name=_('标签'), blank=True) # 标签多对多 def body_to_string(self): - return self.body + return self.body # 返回文章内容字符串 def __str__(self): - return self.title + return self.title # 对象字符串表示为标题 class Meta: - ordering = ['-article_order', '-pub_time'] - verbose_name = _('article') + ordering = ['-article_order', '-pub_time'] # 排序:先按排序权重,再按发布时间倒序 + verbose_name = _('文章') verbose_name_plural = verbose_name - get_latest_by = 'id' + get_latest_by = 'id' # 最新对象依据ID def get_absolute_url(self): + # 生成文章详情页URL,包含年、月、日和文章ID return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, @@ -125,21 +108,23 @@ class Article(BaseModel): 'day': self.creation_time.day }) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): + # 获取分类及其所有祖先分类的树状结构 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): - super().save(*args, **kwargs) + super().save(*args, **kwargs) # 调用父类保存 def viewed(self): + # 增加文章浏览量并保存 self.views += 1 self.save(update_fields=['views']) def comment_list(self): + # 获取该文章的所有启用状态的评论,并缓存10分钟 cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) if value: @@ -152,62 +137,50 @@ class Article(BaseModel): return comments def get_admin_url(self): + # 获取该文章在后台管理中的编辑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): - # 下一篇 - return Article.objects.filter( - id__gt=self.id, status='p').order_by('id').first() + # 获取当前文章的下一篇文章(按ID排序,状态为发布) + return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first() @cache_decorator(expiration=60 * 100) def prev_article(self): - # 前一篇 + # 获取当前文章的上一篇文章 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: - """ + # 从文章正文中提取第一张图片的URL match = re.search(r'!\[.*?\]\((.+?)\)', self.body) if match: return match.group(1) return "" +# 分类模型 class Category(BaseModel): - """文章分类""" - 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')) + name = models.CharField(_('分类名称'), max_length=30, unique=True) # 分类名称,唯一 + parent_category = models.ForeignKey('self', verbose_name=_('父级分类'), blank=True, null=True, on_delete=models.CASCADE) # 父分类 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # Slug字段 + index = models.IntegerField(default=0, verbose_name=_('排序')) # 排序索引 class Meta: - ordering = ['-index'] - verbose_name = _('category') + ordering = ['-index'] # 按排序索引倒序 + verbose_name = _('分类') verbose_name_plural = verbose_name def get_absolute_url(self): - return reverse( - 'blog:category_detail', kwargs={ - 'category_name': self.slug}) + # 获取分类详情页URL + return reverse('blog:category_detail', kwargs={'category_name': self.slug}) def __str__(self): - return self.name + return self.name # 对象字符串表示为分类名称 - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): - """ - 递归获得分类目录的父级 - :return: - """ + # 递归获取当前分类及其所有祖先分类 categorys = [] def parse(category): @@ -220,10 +193,7 @@ class Category(BaseModel): @cache_decorator(60 * 60 * 10) def get_sub_categorys(self): - """ - 获得当前分类目录所有子集 - :return: - """ + # 获取当前分类的所有子分类 categorys = [] all_categorys = Category.objects.all() @@ -232,7 +202,7 @@ class Category(BaseModel): categorys.append(category) childs = all_categorys.filter(parent_category=category) for child in childs: - if category not in categorys: + if child not in categorys: categorys.append(child) parse(child) @@ -240,137 +210,100 @@ class Category(BaseModel): return categorys +# 标签模型 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(_('标签名称'), max_length=30, unique=True) # 标签名称,唯一 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # Slug字段 def __str__(self): - return self.name + return self.name # 对象字符串表示为标签名称 def get_absolute_url(self): + # 获取标签详情页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): + # 获取关联该标签的文章数量 return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: - ordering = ['name'] - verbose_name = _('tag') + ordering = ['name'] # 按名称排序 + verbose_name = _('标签') 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) - is_enable = models.BooleanField( - _('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) + name = models.CharField(_('链接名称'), max_length=30, unique=True) # 链接名称,唯一 + link = models.URLField(_('链接地址')) # 链接URL + sequence = models.IntegerField(_('排序'), unique=True) # 排序,唯一 + is_enable = models.BooleanField(_('是否显示'), default=True) # 是否启用 + show_type = models.CharField(_('显示位置'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I) # 显示位置类型 + creation_time = models.DateTimeField(_('创建时间'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('修改时间'), default=now) # 修改时间 class Meta: - ordering = ['sequence'] - verbose_name = _('link') + ordering = ['sequence'] # 按排序排序 + verbose_name = _('友情链接') verbose_name_plural = verbose_name def __str__(self): - return self.name + 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) + name = models.CharField(_('标题'), max_length=100) # 侧边栏标题 + content = models.TextField(_('内容')) # 侧边栏内容,可以是HTML + sequence = models.IntegerField(_('排序'), unique=True) # 排序,唯一 + is_enable = models.BooleanField(_('是否启用'), default=True) # 是否启用 + creation_time = models.DateTimeField(_('创建时间'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('修改时间'), default=now) # 修改时间 class Meta: - ordering = ['sequence'] - verbose_name = _('sidebar') + ordering = ['sequence'] # 按排序排序 + verbose_name = _('侧边栏') verbose_name_plural = verbose_name def __str__(self): - return self.name + return self.name # 对象字符串表示为侧边栏标题 +# 博客设置模型(单例) class BlogSettings(models.Model): - """blog的配置""" - site_name = models.CharField( - _('site name'), - max_length=200, - null=False, - blank=False, - default='') - site_description = models.TextField( - _('site description'), - max_length=1000, - null=False, - blank=False, - default='') - site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') - 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) - 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='') - beian_code = models.CharField( - '备案号', - max_length=2000, - null=True, - blank=True, - default='') - analytics_code = models.TextField( - "网站统计代码", - max_length=1000, - null=False, - blank=False, - default='') - show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) - gongan_beiancode = models.TextField( - '公安备案号', - max_length=2000, - null=True, - blank=True, - default='') - comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) + site_name = models.CharField(_('站点名称'), max_length=200, null=False, blank=False, default='') # 站点名称 + site_description = models.TextField(_('站点描述'), max_length=1000, null=False, blank=False, default='') # 站点描述 + site_seo_description = models.TextField(_('SEO描述'), max_length=1000, null=False, blank=False, default='') # SEO描述 + site_keywords = models.TextField(_('站点关键词'), max_length=1000, null=False, blank=False, default='') # 站点关键词 + article_sub_length = models.IntegerField(_('文章摘要长度'), default=300) # 文章摘要显示长度 + sidebar_article_count = models.IntegerField(_('侧边栏文章数量'), default=10) # 侧边栏显示的文章数量 + sidebar_comment_count = models.IntegerField(_('侧边栏评论数量'), default=5) # 侧边栏显示的评论数量 + article_comment_count = models.IntegerField(_('文章评论数量'), default=5) # 文章页显示的评论数量 + show_google_adsense = models.BooleanField(_('显示Google广告'), default=False) # 是否显示Google广告 + google_adsense_codes = models.TextField(_('Google广告代码'), max_length=2000, null=True, blank=True, default='') # Google广告代码 + open_site_comment = models.BooleanField(_('开放站点评论'), default=True) # 是否开放站点评论 + global_header = models.TextField(_("公共头部"), null=True, blank=True, default='') # 公共头部HTML + global_footer = models.TextField(_("公共尾部"), null=True, blank=True, default='') # 公共尾部HTML + beian_code = models.CharField(_('备案号'), max_length=2000, null=True, blank=True, default='') # 备案号 + analytics_code = models.TextField(_("网站统计代码"), max_length=1000, null=False, blank=False, default='') # 统计代码,如百度统计 + show_gongan_code = models.BooleanField(_('是否显示公安备案号'), default=False) # 是否显示公安备案号 + gongan_beiancode = models.TextField(_('公安备案号'), max_length=2000, null=True, blank=True, default='') # 公安备案号 + comment_need_review = models.BooleanField(_('评论是否需要审核'), default=False) # 评论是否需要审核 class Meta: - verbose_name = _('Website configuration') + verbose_name = _('网站配置') verbose_name_plural = verbose_name def __str__(self): - return self.site_name + return self.site_name # 对象字符串表示为站点名称 def clean(self): + # 确保只能存在一个站点配置实例 if BlogSettings.objects.exclude(id=self.id).count(): - raise ValidationError(_('There can only be one configuration')) + raise ValidationError(_('只能存在一个配置')) def save(self, *args, **kwargs): super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() + cache.clear() # 保存配置后清除缓存 \ No newline at end of file diff --git a/src/DjangoBlog/blog/search_indexes.py b/src/DjangoBlog/blog/search_indexes.py index 7f1dfac..62c4312 100644 --- a/src/DjangoBlog/blog/search_indexes.py +++ b/src/DjangoBlog/blog/search_indexes.py @@ -1,13 +1,7 @@ -from haystack import indexes - -from blog.models import Article - - +# 定义 Haystack 的搜索索引,用于普通搜索(非 Elasticsearch) class ArticleIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True) - def get_model(self): return Article - def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') + return self.get_model().objects.filter(status='p') # 只索引已发布文章 \ No newline at end of file diff --git a/src/DjangoBlog/blog/static/account/css/account.css b/src/DjangoBlog/blog/static/account/css/account.css index 7d4cec7..a9c8926 100644 --- a/src/DjangoBlog/blog/static/account/css/account.css +++ b/src/DjangoBlog/blog/static/account/css/account.css @@ -1,9 +1,9 @@ .button { - border: none; - padding: 4px 80px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; - margin: 4px 2px; + border: none; /* 去掉按钮的默认边框 */ + padding: 4px 80px; /* 内边距:上下 4px,左右 80px,控制按钮文字与边缘的距离以及按钮宽度感 */ + text-align: center; /* 文字在按钮内水平居中 */ + text-decoration: none; /* 去掉文字装饰(比如下划线,通常对 标签有用) */ + display: inline-block; /* 让按钮可以设置宽高,同时还能在一行显示(不像块级元素独占一行) */ + font-size: 16px; /* 按钮内文字大小为 16 像素 */ + margin: 4px 2px; /* 外边距:上下 4px,左右 2px,控制按钮与其它元素的间距 */ } \ No newline at end of file diff --git a/src/DjangoBlog/blog/tests.py b/src/DjangoBlog/blog/tests.py index ee13505..4aa45ea 100644 --- a/src/DjangoBlog/blog/tests.py +++ b/src/DjangoBlog/blog/tests.py @@ -2,30 +2,32 @@ import os from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile -from django.core.management import call_command -from django.core.paginator import Paginator -from django.templatetags.static import static -from django.test import Client, RequestFactory, TestCase -from django.urls import reverse -from django.utils import timezone - -from accounts.models import BlogUser -from blog.forms import BlogSearchForm -from blog.models import Article, Category, Tag, SideBar, Links -from blog.templatetags.blog_tags import load_pagination_info, load_articletags -from djangoblog.utils import get_current_site, get_sha256 -from oauth.models import OAuthUser, OAuthConfig - - -# Create your tests here. - +from django.core.management import call_command # 执行Django管理命令 +from django.core.paginator import Paginator # 分页器 +from django.templatetags.static import static # 静态文件URL生成 +from django.test import Client, RequestFactory, TestCase # Django测试客户端与测试用例 +from django.urls import reverse # URL反向解析 +from django.utils import timezone # 时间工具 + +from accounts.models import BlogUser # 用户模型 +from blog.forms import BlogSearchForm # 搜索表单 +from blog.models import Article, Category, Tag, SideBar, Links # 博客相关模型 +from blog.templatetags.blog_tags import load_pagination_info, load_articletags # 模板标签 +from djangoblog.utils import get_current_site, get_sha256 # 工具函数 +from oauth.models import OAuthUser, OAuthConfig # OAuth相关模型 + + +# 文章相关测试类 class ArticleTest(TestCase): def setUp(self): + # 初始化测试客户端与请求工厂 self.client = Client() self.factory = RequestFactory() def test_validate_article(self): + # 获取当前站点域名 site = get_current_site().domain + # 创建一个超级用户 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", username="liangliangyy")[0] @@ -33,10 +35,13 @@ class ArticleTest(TestCase): user.is_staff = True user.is_superuser = True user.save() + # 访问用户详情页 response = self.client.get(user.get_absolute_url()) self.assertEqual(response.status_code, 200) + # 以下几行尝试访问不存在的admin页面,可能用于测试404 response = self.client.get('/admin/servermanager/emailsendlog/') response = self.client.get('admin/admin/logentry/') + # 创建一个侧边栏实例并保存 s = SideBar() s.sequence = 1 s.name = 'test' @@ -44,30 +49,34 @@ class ArticleTest(TestCase): 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.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()) # 应有1个标签 + # 批量创建20篇文章,均添加同一标签 for i in range(20): article = Article() article.title = "nicetitle" + str(i) @@ -79,76 +88,98 @@ 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) + # 访问某篇文章详情页 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()) + # 访问标签详情页 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) + # 搜索关键词'django' response = self.client.get('/search', {'q': 'django'}) self.assertEqual(response.status_code, 200) + # 获取文章的标签模板标签结果 s = load_articletags(article) 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, '', '') + # 按标签分页 p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) self.check_pagination(p, '分类标签归档', tag.slug) + # 按作者分页 p = Paginator( Article.objects.filter( author__username='liangliangyy'), settings.PAGINATE_BY) self.check_pagination(p, '作者文章归档', 'liangliangyy') + # 按分类分页 p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) self.check_pagination(p, '分类目录归档', category.slug) + # 测试搜索表单 f = BlogSearchForm() f.search() - # self.client.login(username='liangliangyy', password='liangliangyy') + + # 模拟百度通知 from djangoblog.spider_notify import SpiderNotify SpiderNotify.baidu_notify([article.get_full_url()]) + # 测试模板标签:gravatar头像URL from blog.templatetags.blog_tags import gravatar_url, gravatar u = gravatar_url('liangliangyy@gmail.com') u = gravatar('liangliangyy@gmail.com') + # 创建并保存一个友情链接 link = Links( sequence=1, name="lylinux", link='https://wwww.lylinux.net') link.save() + # 访问友情链接页面 response = self.client.get('/links.html') self.assertEqual(response.status_code, 200) + # 访问RSS Feed页面 response = self.client.get('/feed/') self.assertEqual(response.status_code, 200) + # 访问Sitemap页面 response = self.client.get('/sitemap.xml') self.assertEqual(response.status_code, 200) + # 尝试删除一篇文章 self.client.get("/admin/blog/article/1/delete/") + # 访问一些不存在的admin页面 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): + # 遍历所有分页,检查前后页链接是否有效 for page in range(1, p.num_pages + 1): s = load_pagination_info(p.page(page), type, value) self.assertIsNotNone(s) @@ -160,33 +191,41 @@ class ArticleTest(TestCase): self.assertEqual(response.status_code, 200) def test_image(self): + # 测试图片上传功能 import requests + # 下载Python官方Logo rsp = requests.get( 'https://www.python.org/static/img/python-logo.png') imagepath = os.path.join(settings.BASE_DIR, 'python.png') with open(imagepath, 'wb') as file: file.write(rsp.content) + # 尝试未授权上传 rsp = self.client.post('/upload') 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) + # 测试发送邮件与保存头像功能 from djangoblog.utils import save_user_avatar, send_email send_email(['qq@qq.com'], 'testTitle', 'testContent') save_user_avatar( 'https://www.python.org/static/img/python-logo.png') def test_errorpage(self): + # 测试访问不存在的页面,应返回404 rsp = self.client.get('/eee') self.assertEqual(rsp.status_code, 404) def test_commands(self): + # 创建超级用户 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", username="liangliangyy")[0] @@ -195,6 +234,7 @@ class ArticleTest(TestCase): user.is_superuser = True user.save() + # 创建OAuth配置与用户 c = OAuthConfig() c.type = 'qq' c.appkey = 'appkey' @@ -222,11 +262,13 @@ class ArticleTest(TestCase): }''' u.save() + # 如果启用了Elasticsearch,构建索引 from blog.documents import ELASTICSEARCH_ENABLED if ELASTICSEARCH_ENABLED: call_command("build_index") + # 执行一系列管理命令,如Ping百度、创建测试数据、清理缓存等 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("build_search_words") \ No newline at end of file diff --git a/src/DjangoBlog/blog/urls.py b/src/DjangoBlog/blog/urls.py index adf2703..97dcbc4 100644 --- a/src/DjangoBlog/blog/urls.py +++ b/src/DjangoBlog/blog/urls.py @@ -1,62 +1,75 @@ from django.urls import path -from django.views.decorators.cache import cache_page +from django.views.decorators.cache import cache_page # Django缓存视图装饰器 -from . import views +from . import views # 导入当前应用的视图 + +app_name = "blog" # 应用命名空间 -app_name = "blog" urlpatterns = [ + # 首页 path( r'', views.IndexView.as_view(), name='index'), + # 首页分页 path( r'page//', views.IndexView.as_view(), name='index_page'), + # 文章详情页,通过年、月、日、文章ID定位 + # 文章详情页,通过年、月、日、文章ID定位 path( r'article////.html', views.ArticleDetailView.as_view(), name='detailbyid'), + # 分类目录详情页,通过分类别名 path( r'category/.html', views.CategoryDetailView.as_view(), name='category_detail'), + # 分类目录详情页(带分页) path( r'category//.html', views.CategoryDetailView.as_view(), name='category_detail_page'), + # 作者文章详情页,通过作者名称 path( r'author/.html', views.AuthorDetailView.as_view(), name='author_detail'), + # 作者文章详情页(带分页) path( r'author//.html', views.AuthorDetailView.as_view(), name='author_detail_page'), + # 标签详情页,通过标签别名 path( r'tag/.html', views.TagDetailView.as_view(), name='tag_detail'), + # 标签详情页(带分页) path( r'tag//.html', views.TagDetailView.as_view(), name='tag_detail_page'), + # 文章归档页,使用缓存60分钟 path( 'archives.html', - cache_page( - 60 * 60)( - views.ArchivesView.as_view()), + cache_page(60 * 60)(views.ArchivesView.as_view()), name='archives'), + # 友情链接页 path( - 'links.html', + r'links.html', views.LinkListView.as_view(), name='links'), + # 文件上传接口(用于图床等功能) path( r'upload', views.fileupload, name='upload'), + # 清理缓存接口 path( r'clean', views.clean_cache_view, name='clean'), -] +] \ No newline at end of file diff --git a/src/DjangoBlog/blog/views.py b/src/DjangoBlog/blog/views.py index ace9e63..20be34d 100644 --- a/src/DjangoBlog/blog/views.py +++ b/src/DjangoBlog/blog/views.py @@ -2,69 +2,69 @@ import logging import os import uuid -from django.conf import settings -from django.core.paginator import Paginator -from django.http import HttpResponse, HttpResponseForbidden -from django.shortcuts import get_object_or_404 -from django.shortcuts import render -from django.templatetags.static import static -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ -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 - -from blog.models import Article, Category, LinkShowType, Links, Tag -from comments.forms import CommentForm -from djangoblog.plugin_manage import hooks -from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME -from djangoblog.utils import cache, get_blog_setting, get_sha256 - -logger = logging.getLogger(__name__) - - +from django.conf import settings # Django项目设置 +from django.core.paginator import Paginator # 分页工具 +from django.http import HttpResponse, HttpResponseForbidden # HTTP响应类 +from django.shortcuts import get_object_or_404, render # 快捷函数:获取对象或404,渲染模板 +from django.templatetags.static import static # 静态文件URL生成 +from django.utils import timezone # 时间工具 +from django.utils.translation import gettext_lazy as _ # 国际化翻译 +from django.views.decorators.csrf import csrf_exempt # CSRF豁免装饰器 +from django.views.generic.detail import DetailView # 详情页通用视图 +from django.views.generic.list import ListView # 列表页通用视图 +from haystack.views import SearchView # Haystack搜索视图 + +from blog.models import Article, Category, LinkShowType, Links, Tag # 博客相关模型 +from comments.forms import CommentForm # 评论表单 +from djangoblog.plugin_manage import hooks # 插件管理钩子 +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME # 插件常量 +from djangoblog.utils import cache, get_blog_setting, get_sha256 # 工具函数:缓存、获取博客设置、生成SHA256 + +logger = logging.getLogger(__name__) # 日志记录器 + + +# 文章列表视图(通用列表视图,用于首页、分类、作者、标签等) class ArticleListView(ListView): - # template_name属性用于指定使用哪个模板进行渲染 + # 指定使用的模板 template_name = 'blog/article_index.html' - - # context_object_name属性用于给上下文变量取名(在模板中使用该名字) + # 上下文中使用的变量名 context_object_name = 'article_list' - - # 页面类型,分类目录或标签列表等 + # 页面类型,用于区分不同列表页 page_type = '' + # 每页显示条数,从项目设置中获取 paginate_by = settings.PAGINATE_BY + # 分页参数名 page_kwarg = 'page' + # 链接显示类型,默认为LinkShowType.L(列表页) link_type = LinkShowType.L def get_view_cache_key(self): - return self.request.get['pages'] + # 获取视图缓存键,目前未使用request GET参数,可根据需求调整 + return self.request.GET.get('pages', '') @property def page_number(self): + # 获取当前页码,优先级:kwargs中的page > GET参数中的page > 默认1 page_kwarg = self.page_kwarg - page = self.kwargs.get( - page_kwarg) or self.request.GET.get(page_kwarg) or 1 + page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 return page def get_queryset_cache_key(self): """ - 子类重写.获得queryset的缓存key + 子类必须重写此方法,用于生成查询集的缓存键 """ raise NotImplementedError() def get_queryset_data(self): """ - 子类重写.获取queryset的数据 + 子类必须重写此方法,用于获取查询集的数据 """ raise NotImplementedError() def get_queryset_from_cache(self, cache_key): - ''' - 缓存页面数据 - :param cache_key: 缓存key - :return: - ''' + """ + 从缓存中获取查询集数据,若缓存存在则返回缓存数据,否则获取数据并设置缓存 + """ value = cache.get(cache_key) if value: logger.info('get view cache.key:{key}'.format(key=cache_key)) @@ -76,51 +76,61 @@ class ArticleListView(ListView): return article_list def get_queryset(self): - ''' - 重写默认,从缓存获取数据 - :return: - ''' + """ + 重写默认的get_queryset方法,优先从缓存中获取查询集 + """ key = self.get_queryset_cache_key() value = self.get_queryset_from_cache(key) return value def get_context_data(self, **kwargs): + # 向上下文中添加链接类型 kwargs['linktype'] = self.link_type return super(ArticleListView, self).get_context_data(**kwargs) +# 首页视图,继承自ArticleListView class IndexView(ArticleListView): ''' - 首页 + 首页视图,展示最新发布的文章 ''' - # 友情链接类型 + # 链接类型设置为首页显示 link_type = LinkShowType.I def get_queryset_data(self): + # 获取所有状态为发布、类型为文章的文章 article_list = Article.objects.filter(type='a', status='p') return article_list def get_queryset_cache_key(self): + # 缓存键根据页码生成,如 index_1, index_2, ... cache_key = 'index_{page}'.format(page=self.page_number) return cache_key +# 文章详情页视图,继承自DetailView 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() + # 获取当前文章的所有启用状态的评论 article_comments = self.object.comment_list() + # 过滤出顶级评论(没有父评论的评论) parent_comments = article_comments.filter(parent_comment=None) + # 获取博客设置 blog_setting = get_blog_setting() + # 创建评论分页器,每页显示指定数量的评论 paginator = Paginator(parent_comments, blog_setting.article_comment_count) + # 获取请求中的评论页码参数,默认为1 page = self.request.GET.get('comment_page', '1') if not page.isnumeric(): page = 1 @@ -131,51 +141,67 @@ class ArticleDetailView(DetailView): if page > paginator.num_pages: page = paginator.num_pages + # 获取当前页的评论 p_comments = paginator.page(page) + # 获取下一页页码,如果没有则None next_page = p_comments.next_page_number() if p_comments.has_next() else None + # 获取上一页页码,如果没有则None prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None + # 如果有下一页,生成下一页评论的URL if next_page: kwargs[ 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' + # 如果有上一页,生成上一页评论的URL 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['comment_count'] = len(article_comments) if article_comments else 0 + # 添加下一篇和上一篇的文章链接 kwargs['next_article'] = self.object.next_article kwargs['prev_article'] = self.object.prev_article + # 调用父类的get_context_data方法获取基础上下文 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) return context +# 分类目录视图,继承自ArticleListView class CategoryDetailView(ArticleListView): ''' - 分类目录列表 + 分类目录视图,展示某个分类下的所有文章 ''' page_type = "分类目录归档" def get_queryset_data(self): + # 从URL参数中获取分类别名 slug = self.kwargs['category_name'] + # 获取对应的分类对象,如果不存在则返回404 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): + # 缓存键根据分类名称和页码生成,如 category_list_分类名_页码 slug = self.kwargs['category_name'] category = get_object_or_404(Category, slug=slug) categoryname = category.name @@ -185,24 +211,27 @@ class CategoryDetailView(ArticleListView): return cache_key def get_context_data(self, **kwargs): - + # 处理分类名称,尝试去除可能的路径分隔符 categoryname = self.categoryname try: 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) +# 作者文章视图,继承自ArticleListView class AuthorDetailView(ArticleListView): ''' - 作者详情页 + 作者文章视图,展示某个作者的所有文章 ''' page_type = '作者文章归档' def get_queryset_cache_key(self): + # 使用uuslug将作者名称转换为Slug,生成缓存键,如 author_作者Slug_页码 from uuslug import slugify author_name = slugify(self.kwargs['author_name']) cache_key = 'author_{author_name}_{page}'.format( @@ -210,34 +239,40 @@ class AuthorDetailView(ArticleListView): return cache_key def get_queryset_data(self): + # 从URL参数中获取作者名称,获取该作者的所有状态为发布、类型为文章的文章 author_name = self.kwargs['author_name'] article_list = Article.objects.filter( author__username=author_name, type='a', status='p') return article_list def get_context_data(self, **kwargs): + # 向上下文中添加页面类型和标签名称(此处为作者名称) 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) +# 标签详情视图,继承自ArticleListView class TagDetailView(ArticleListView): ''' - 标签列表页面 + 标签详情视图,展示某个标签下的所有文章 ''' page_type = '分类标签归档' def get_queryset_data(self): + # 从URL参数中获取标签别名,获取对应的标签对象 slug = self.kwargs['tag_name'] 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): + # 缓存键根据标签名称和页码生成,如 tag_标签名_页码 slug = self.kwargs['tag_name'] tag = get_object_or_404(Tag, slug=slug) tag_name = tag.name @@ -247,129 +282,148 @@ class TagDetailView(ArticleListView): return cache_key def get_context_data(self, **kwargs): - # tag_name = self.kwargs['tag_name'] + # 向上下文中添加页面类型和标签名称 tag_name = self.name kwargs['page_type'] = TagDetailView.page_type kwargs['tag_name'] = tag_name return super(TagDetailView, self).get_context_data(**kwargs) +# 文章归档视图,继承自ArticleListView class ArchivesView(ArticleListView): ''' - 文章归档页面 + 文章归档视图,展示所有状态为发布的文章,通常按时间归档 ''' page_type = '文章归档' - paginate_by = None - page_kwarg = None - template_name = 'blog/article_archives.html' + paginate_by = None # 不进行分页 + page_kwarg = None # 不使用页码参数 + template_name = 'blog/article_archives.html' # 使用不同的模板 def get_queryset_data(self): + # 获取所有状态为发布的文章 return Article.objects.filter(status='p').all() def get_queryset_cache_key(self): + # 缓存键为 archives cache_key = 'archives' return cache_key +# 友情链接视图,展示所有启用的友情链接 class LinkListView(ListView): - model = Links - template_name = 'blog/links_list.html' + model = Links # 关联的模型 + template_name = 'blog/links_list.html' # 使用的模板 def get_queryset(self): + # 获取所有启用的友情链接 return Links.objects.filter(is_enable=True) +# Haystack搜索视图,用于全文搜索 class EsSearchView(SearchView): def get_context(self): + # 构建搜索结果的上下文,包括查询词、表单、分页器、建议等 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, # 搜索建议,初始为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()) - return context -@csrf_exempt +# 文件上传视图(图床功能),需要自行实现调用端 +@csrf_exempt # 豁免CSRF保护,生产环境应谨慎使用 def fileupload(request): """ - 该方法需自己写调用端来上传图片,该方法仅提供图床功能 - :param request: - :return: + 该方法用于上传图片,需自行实现调用端,仅提供图床功能 + :param request: HTTP请求对象 + :return: HTTP响应,包含上传后的图片URL列表或错误信息 """ if request.method == 'POST': + # 获取签名参数,用于验证请求合法性 sign = request.GET.get('sign', None) if not sign: - return HttpResponseForbidden() + return HttpResponseForbidden() # 未提供签名,禁止访问 + # 验证签名是否正确,签名应为SECRET_KEY的双重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 + 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") + return HttpResponse("only for post") # 安全校验,防止路径遍历 + # 保存上传的文件内容 with open(savepath, 'wb+') as wfile: for chunk in request.FILES[filename].chunks(): wfile.write(chunk) + # 如果是图片,使用PIL优化并压缩图片 if isimage: from PIL import Image image = Image.open(savepath) image.save(savepath, quality=20, optimize=True) + # 生成静态文件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请求 +# 自定义404错误页面视图 def page_not_found_view( request, exception, template_name='blog/error_page.html'): if exception: - logger.error(exception) - url = request.get_full_path() + logger.error(exception) # 记录错误日志 + url = request.get_full_path() # 获取请求的完整路径 return render(request, template_name, {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'), 'statuscode': '404'}, - status=404) + status=404) # 渲染404错误页面 +# 自定义500错误页面视图 def server_error_view(request, template_name='blog/error_page.html'): return render(request, template_name, {'message': _('Sorry, the server is busy, please click the home page to see other?'), 'statuscode': '500'}, - status=500) + status=500) # 渲染500错误页面 +# 自定义403权限拒绝页面视图 def permission_denied_view( request, exception, template_name='blog/error_page.html'): if exception: - logger.error(exception) + logger.error(exception) # 记录错误日志 return render( request, template_name, { 'message': _('Sorry, you do not have permission to access this page?'), - 'statuscode': '403'}, status=403) + 'statuscode': '403'}, status=403) # 渲染403错误页面 +# 清理缓存视图,调用缓存清理函数并返回成功响应 def clean_cache_view(request): - cache.clear() - return HttpResponse('ok') + cache.clear() # 清除所有缓存 + return HttpResponse('ok') # 返回简单的'ok'响应 \ No newline at end of file diff --git a/src/DjangoBlog/class_diagram.dot b/src/DjangoBlog/class_diagram.dot new file mode 100644 index 0000000..1ddc379 --- /dev/null +++ b/src/DjangoBlog/class_diagram.dot @@ -0,0 +1,1615 @@ +digraph model_graph { + // Dotfile by Django-Extensions graph_models + // Created: 2025-10-15 18:18 + // Cli Options: -a --dot -o class_diagram.dot + + fontname = "Roboto" + fontsize = 8 + splines = true + rankdir = "TB" + + + node [ + fontname = "Roboto" + fontsize = 8 + shape = "plaintext" + ] + + edge [ + fontname = "Roboto" + fontsize = 8 + ] + + // Labels + + + django_contrib_admin_models_LogEntry [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + LogEntry +
+ id + + AutoField +
+ content_type + + ForeignKey (id) +
+ user + + ForeignKey (id) +
+ action_flag + + PositiveSmallIntegerField +
+ action_time + + DateTimeField +
+ change_message + + TextField +
+ object_id + + TextField +
+ object_repr + + CharField +
+ >] + + + + + django_contrib_auth_models_Permission [label=< + + + + + + + + + + + + + + + + + + + +
+ + Permission +
+ id + + AutoField +
+ content_type + + ForeignKey (id) +
+ codename + + CharField +
+ name + + CharField +
+ >] + + django_contrib_auth_models_Group [label=< + + + + + + + + + + + +
+ + Group +
+ id + + AutoField +
+ name + + CharField +
+ >] + + + + + django_contrib_contenttypes_models_ContentType [label=< + + + + + + + + + + + + + + + +
+ + ContentType +
+ id + + AutoField +
+ app_label + + CharField +
+ model + + CharField +
+ >] + + + + + django_contrib_sessions_base_session_AbstractBaseSession [label=< + + + + + + + + + + + +
+ + AbstractBaseSession +
+ expire_date + + DateTimeField +
+ session_data + + TextField +
+ >] + + django_contrib_sessions_models_Session [label=< + + + + + + + + + + + + + + + +
+ + Session
<AbstractBaseSession> +
+ session_key + + CharField +
+ expire_date + + DateTimeField +
+ session_data + + TextField +
+ >] + + + + + django_contrib_sites_models_Site [label=< + + + + + + + + + + + + + + + +
+ + Site +
+ id + + AutoField +
+ domain + + CharField +
+ name + + CharField +
+ >] + + + + + blog_models_BaseModel [label=< + + + + + + + + + + + +
+ + BaseModel +
+ creation_time + + DateTimeField +
+ last_modify_time + + DateTimeField +
+ >] + + blog_models_Article [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Article
<BaseModel> +
+ id + + AutoField +
+ author + + ForeignKey (id) +
+ category + + ForeignKey (id) +
+ article_order + + IntegerField +
+ body + + MDTextField +
+ comment_status + + CharField +
+ creation_time + + DateTimeField +
+ last_modify_time + + DateTimeField +
+ pub_time + + DateTimeField +
+ show_toc + + BooleanField +
+ status + + CharField +
+ title + + CharField +
+ type + + CharField +
+ views + + PositiveIntegerField +
+ >] + + blog_models_Category [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Category
<BaseModel> +
+ id + + AutoField +
+ parent_category + + ForeignKey (id) +
+ creation_time + + DateTimeField +
+ index + + IntegerField +
+ last_modify_time + + DateTimeField +
+ name + + CharField +
+ slug + + SlugField +
+ >] + + blog_models_Tag [label=< + + + + + + + + + + + + + + + + + + + + + + + +
+ + Tag
<BaseModel> +
+ id + + AutoField +
+ creation_time + + DateTimeField +
+ last_modify_time + + DateTimeField +
+ name + + CharField +
+ slug + + SlugField +
+ >] + + blog_models_Links [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Links +
+ id + + BigAutoField +
+ creation_time + + DateTimeField +
+ is_enable + + BooleanField +
+ last_mod_time + + DateTimeField +
+ link + + URLField +
+ name + + CharField +
+ sequence + + IntegerField +
+ show_type + + CharField +
+ >] + + blog_models_SideBar [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + SideBar +
+ id + + BigAutoField +
+ content + + TextField +
+ creation_time + + DateTimeField +
+ is_enable + + BooleanField +
+ last_mod_time + + DateTimeField +
+ name + + CharField +
+ sequence + + IntegerField +
+ >] + + blog_models_BlogSettings [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + BlogSettings +
+ id + + BigAutoField +
+ analytics_code + + TextField +
+ article_comment_count + + IntegerField +
+ article_sub_length + + IntegerField +
+ beian_code + + CharField +
+ comment_need_review + + BooleanField +
+ global_footer + + TextField +
+ global_header + + TextField +
+ gongan_beiancode + + TextField +
+ google_adsense_codes + + TextField +
+ open_site_comment + + BooleanField +
+ show_gongan_code + + BooleanField +
+ show_google_adsense + + BooleanField +
+ sidebar_article_count + + IntegerField +
+ sidebar_comment_count + + IntegerField +
+ site_description + + TextField +
+ site_keywords + + TextField +
+ site_name + + CharField +
+ site_seo_description + + TextField +
+ >] + + + + + django_contrib_auth_models_AbstractUser [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + AbstractUser
<AbstractBaseUser,PermissionsMixin> +
+ date_joined + + DateTimeField +
+ email + + EmailField +
+ first_name + + CharField +
+ is_active + + BooleanField +
+ is_staff + + BooleanField +
+ is_superuser + + BooleanField +
+ last_login + + DateTimeField +
+ last_name + + CharField +
+ password + + CharField +
+ username + + CharField +
+ >] + + accounts_models_BlogUser [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + BlogUser
<AbstractUser> +
+ id + + BigAutoField +
+ creation_time + + DateTimeField +
+ date_joined + + DateTimeField +
+ email + + EmailField +
+ first_name + + CharField +
+ is_active + + BooleanField +
+ is_staff + + BooleanField +
+ is_superuser + + BooleanField +
+ last_login + + DateTimeField +
+ last_modify_time + + DateTimeField +
+ last_name + + CharField +
+ nickname + + CharField +
+ password + + CharField +
+ source + + CharField +
+ username + + CharField +
+ >] + + + + + comments_models_Comment [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Comment +
+ id + + BigAutoField +
+ article + + ForeignKey (id) +
+ author + + ForeignKey (id) +
+ parent_comment + + ForeignKey (id) +
+ body + + TextField +
+ creation_time + + DateTimeField +
+ is_enable + + BooleanField +
+ last_modify_time + + DateTimeField +
+ >] + + + + + oauth_models_OAuthUser [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + OAuthUser +
+ id + + BigAutoField +
+ author + + ForeignKey (id) +
+ creation_time + + DateTimeField +
+ email + + CharField +
+ last_modify_time + + DateTimeField +
+ metadata + + TextField +
+ nickname + + CharField +
+ openid + + CharField +
+ picture + + CharField +
+ token + + CharField +
+ type + + CharField +
+ >] + + oauth_models_OAuthConfig [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + OAuthConfig +
+ id + + BigAutoField +
+ appkey + + CharField +
+ appsecret + + CharField +
+ callback_url + + CharField +
+ creation_time + + DateTimeField +
+ is_enable + + BooleanField +
+ last_modify_time + + DateTimeField +
+ type + + CharField +
+ >] + + + + + servermanager_models_commands [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + commands +
+ id + + BigAutoField +
+ command + + CharField +
+ creation_time + + DateTimeField +
+ describe + + CharField +
+ last_modify_time + + DateTimeField +
+ title + + CharField +
+ >] + + servermanager_models_EmailSendLog [label=< + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + EmailSendLog +
+ id + + BigAutoField +
+ content + + TextField +
+ creation_time + + DateTimeField +
+ emailto + + CharField +
+ send_result + + BooleanField +
+ title + + CharField +
+ >] + + + + + owntracks_models_OwnTrackLog [label=< + + + + + + + + + + + + + + + + + + + + + + + +
+ + OwnTrackLog +
+ id + + BigAutoField +
+ creation_time + + DateTimeField +
+ lat + + FloatField +
+ lon + + FloatField +
+ tid + + CharField +
+ >] + + + + + // Relations + + django_contrib_admin_models_LogEntry -> accounts_models_BlogUser + [label=" user (logentry)"] [arrowhead=none, arrowtail=dot, dir=both]; + + django_contrib_admin_models_LogEntry -> django_contrib_contenttypes_models_ContentType + [label=" content_type (logentry)"] [arrowhead=none, arrowtail=dot, dir=both]; + + + django_contrib_auth_models_Permission -> django_contrib_contenttypes_models_ContentType + [label=" content_type (permission)"] [arrowhead=none, arrowtail=dot, dir=both]; + + django_contrib_auth_models_Group -> django_contrib_auth_models_Permission + [label=" permissions (group)"] [arrowhead=dot arrowtail=dot, dir=both]; + + + + django_contrib_sessions_models_Session -> django_contrib_sessions_base_session_AbstractBaseSession + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + + + blog_models_Article -> accounts_models_BlogUser + [label=" author (article)"] [arrowhead=none, arrowtail=dot, dir=both]; + + blog_models_Article -> blog_models_Category + [label=" category (article)"] [arrowhead=none, arrowtail=dot, dir=both]; + + blog_models_Article -> blog_models_Tag + [label=" tags (article)"] [arrowhead=dot arrowtail=dot, dir=both]; + + blog_models_Article -> blog_models_BaseModel + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + blog_models_Category -> blog_models_Category + [label=" parent_category (category)"] [arrowhead=none, arrowtail=dot, dir=both]; + + blog_models_Category -> blog_models_BaseModel + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + blog_models_Tag -> blog_models_BaseModel + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + django_contrib_auth_base_user_AbstractBaseUser [label=< + + +
+ AbstractBaseUser +
+ >] + django_contrib_auth_models_AbstractUser -> django_contrib_auth_base_user_AbstractBaseUser + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + django_contrib_auth_models_PermissionsMixin [label=< + + +
+ PermissionsMixin +
+ >] + django_contrib_auth_models_AbstractUser -> django_contrib_auth_models_PermissionsMixin + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + accounts_models_BlogUser -> django_contrib_auth_models_Group + [label=" groups (user)"] [arrowhead=dot arrowtail=dot, dir=both]; + + accounts_models_BlogUser -> django_contrib_auth_models_Permission + [label=" user_permissions (user)"] [arrowhead=dot arrowtail=dot, dir=both]; + + accounts_models_BlogUser -> django_contrib_auth_models_AbstractUser + [label=" abstract\ninheritance"] [arrowhead=empty, arrowtail=none, dir=both]; + + + comments_models_Comment -> accounts_models_BlogUser + [label=" author (comment)"] [arrowhead=none, arrowtail=dot, dir=both]; + + comments_models_Comment -> blog_models_Article + [label=" article (comment)"] [arrowhead=none, arrowtail=dot, dir=both]; + + comments_models_Comment -> comments_models_Comment + [label=" parent_comment (comment)"] [arrowhead=none, arrowtail=dot, dir=both]; + + + oauth_models_OAuthUser -> accounts_models_BlogUser + [label=" author (oauthuser)"] [arrowhead=none, arrowtail=dot, dir=both]; + + + + +} diff --git a/src/DjangoBlog/djangoblog/__init__.py b/src/DjangoBlog/djangoblog/__init__.py index 1e205f4..7e8d0f8 100644 --- a/src/DjangoBlog/djangoblog/__init__.py +++ b/src/DjangoBlog/djangoblog/__init__.py @@ -1 +1,16 @@ -default_app_config = 'djangoblog.apps.DjangoblogAppConfig' +""" +Django应用配置入口模块 + +本模块定义了Django应用的默认配置类路径,用于在Django启动时自动加载应用配置。 +这是Django应用的标准配置方式,确保应用初始化代码能够正确执行。 + +功能说明: +- 指定默认的应用配置类 +- 确保Django在启动时加载自定义应用配置 +- 触发应用相关的初始化流程 +""" + +# 指定默认的应用配置类路径 +# Django在启动时会自动加载此配置类,并执行其中的ready()方法 +# 这确保了插件系统和其他初始化代码能够在应用启动时正确执行 +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/admin_site.py b/src/DjangoBlog/djangoblog/admin_site.py index f120405..63d3c02 100644 --- a/src/DjangoBlog/djangoblog/admin_site.py +++ b/src/DjangoBlog/djangoblog/admin_site.py @@ -1,8 +1,22 @@ +""" +DjangoBlog 后台管理站点配置模块 + +本模块定义了自定义的Django后台管理站点,用于统一管理博客系统的所有数据模型。 +通过自定义AdminSite类,实现了权限控制、界面定制和模型注册的集中管理。 + +主要功能: +- 自定义后台管理站点外观和权限 +- 集中注册所有应用的模型到统一后台 +- 提供超级用户专属的管理界面 +- 集成日志记录、用户管理、内容管理等功能 +""" + from django.contrib.admin import AdminSite from django.contrib.admin.models import LogEntry from django.contrib.sites.admin import SiteAdmin from django.contrib.sites.models import Site +# 导入各应用的Admin配置类和模型类 from accounts.admin import * from blog.admin import * from blog.models import * @@ -18,15 +32,43 @@ from servermanager.models import * class DjangoBlogAdminSite(AdminSite): + """ + 自定义DjangoBlog后台管理站点 + + 继承自Django原生的AdminSite类,提供博客系统的定制化后台管理界面。 + 包含站点标题设置、权限控制和可选的URL扩展功能。 + """ + + # 设置后台管理站点的头部标题 site_header = 'djangoblog administration' + # 设置浏览器标签页标题 site_title = 'djangoblog site admin' def __init__(self, name='admin'): + """ + 初始化后台管理站点 + + Args: + name (str): 管理站点的名称,默认为'admin' + """ + # 调用父类初始化方法 super().__init__(name) def has_permission(self, request): + """ + 权限验证方法 + + 重写权限检查逻辑,只允许超级用户访问后台管理界面。 + + Args: + request: HTTP请求对象 + + Returns: + bool: 如果是超级用户返回True,否则返回False + """ return request.user.is_superuser + # 注释掉的URL扩展方法 - 预留用于添加自定义管理视图 # def get_urls(self): # urls = super().get_urls() # from django.urls import path @@ -38,27 +80,36 @@ class DjangoBlogAdminSite(AdminSite): # return urls + my_urls +# 创建自定义后台管理站点的实例 admin_site = DjangoBlogAdminSite(name='admin') -admin_site.register(Article, ArticlelAdmin) -admin_site.register(Category, CategoryAdmin) -admin_site.register(Tag, TagAdmin) -admin_site.register(Links, LinksAdmin) -admin_site.register(SideBar, SideBarAdmin) -admin_site.register(BlogSettings, BlogSettingsAdmin) +# 注册博客相关模型到后台管理 +admin_site.register(Article, ArticlelAdmin) # 文章模型 +admin_site.register(Category, CategoryAdmin) # 分类模型 +admin_site.register(Tag, TagAdmin) # 标签模型 +admin_site.register(Links, LinksAdmin) # 友情链接模型 +admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型 +admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型 -admin_site.register(commands, CommandsAdmin) -admin_site.register(EmailSendLog, EmailSendLogAdmin) +# 注册服务器管理相关模型 +admin_site.register(commands, CommandsAdmin) # 命令模型 +admin_site.register(EmailSendLog, EmailSendLogAdmin) # 邮件发送日志模型 -admin_site.register(BlogUser, BlogUserAdmin) +# 注册用户管理模型 +admin_site.register(BlogUser, BlogUserAdmin) # 博客用户模型 -admin_site.register(Comment, CommentAdmin) +# 注册评论管理模型 +admin_site.register(Comment, CommentAdmin) # 评论模型 -admin_site.register(OAuthUser, OAuthUserAdmin) -admin_site.register(OAuthConfig, OAuthConfigAdmin) +# 注册OAuth认证相关模型 +admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型 +admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置模型 -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) +# 注册位置追踪相关模型 +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置追踪日志模型 -admin_site.register(Site, SiteAdmin) +# 注册Django内置站点模型 +admin_site.register(Site, SiteAdmin) # 站点模型 -admin_site.register(LogEntry, LogEntryAdmin) +# 注册日志记录模型 +admin_site.register(LogEntry, LogEntryAdmin) # 管理员操作日志模型 \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/apps.py b/src/DjangoBlog/djangoblog/apps.py index d29e318..4bda911 100644 --- a/src/DjangoBlog/djangoblog/apps.py +++ b/src/DjangoBlog/djangoblog/apps.py @@ -1,11 +1,49 @@ +""" +DjangoBlog 应用配置模块 + +本模块定义了DjangoBlog应用的核心配置类,负责应用启动时的初始化工作。 +主要功能包括应用元数据配置和插件系统的自动加载。 + +关键功能: +- 配置Django应用的默认设置 +- 在应用准备就绪时自动加载插件系统 +- 确保插件在Django启动过程中正确初始化 +""" + from django.apps import AppConfig + class DjangoblogAppConfig(AppConfig): + """ + DjangoBlog 应用配置类 + + 继承自Django的AppConfig类,用于配置DjangoBlog应用的各项设置。 + 在Django启动时自动实例化,并执行ready()方法完成初始化。 + """ + + # 设置默认自增主键字段类型为BigAutoField(64位整数) default_auto_field = 'django.db.models.BigAutoField' + + # 定义应用的Python路径,Django通过此名称识别应用 name = 'djangoblog' def ready(self): + """ + 应用准备就绪回调方法 + + 当Django应用注册表完全加载后自动调用此方法。 + 在此处执行应用启动时需要完成的初始化操作,特别是插件系统的加载。 + + 执行流程: + 1. 调用父类的ready()方法确保基础初始化完成 + 2. 导入插件加载器模块 + 3. 调用load_plugins()函数加载所有激活的插件 + """ + # 调用父类ready()方法,确保Django基础初始化完成 super().ready() - # Import and load plugins here + + # 导入插件加载器模块 - 在方法内导入避免循环依赖 from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file + + # 执行插件加载函数,初始化所有配置的插件 + load_plugins() \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/blog_signals.py b/src/DjangoBlog/djangoblog/blog_signals.py index 393f441..79ce8d0 100644 --- a/src/DjangoBlog/djangoblog/blog_signals.py +++ b/src/DjangoBlog/djangoblog/blog_signals.py @@ -1,3 +1,17 @@ +""" +DjangoBlog 信号处理模块 + +本模块定义了DjangoBlog系统的所有信号处理函数,用于在特定事件发生时执行相应的操作。 +通过Django的信号机制,实现了模块间的解耦和事件驱动的编程模式。 + +主要功能: +- 邮件发送信号处理 +- OAuth用户登录信号处理 +- 模型保存后的回调处理 +- 用户登录/登出事件处理 +- 缓存管理和搜索引擎通知 +""" + import _thread import logging @@ -9,6 +23,7 @@ from django.core.mail import EmailMultiAlternatives from django.db.models.signals import post_save from django.dispatch import receiver +# 导入应用相关模块 from comments.models import Comment from comments.utils import send_comment_email from djangoblog.spider_notify import SpiderNotify @@ -16,51 +31,88 @@ from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, del from djangoblog.utils import get_current_site from oauth.models import OAuthUser +# 初始化模块级日志器 logger = logging.getLogger(__name__) +# 定义自定义信号 +# OAuth用户登录信号,传递用户ID参数 oauth_user_login_signal = django.dispatch.Signal(['id']) + +# 邮件发送信号,传递收件人、标题和内容参数 send_email_signal = django.dispatch.Signal( ['emailto', 'title', 'content']) @receiver(send_email_signal) def send_email_signal_handler(sender, **kwargs): - emailto = kwargs['emailto'] - title = kwargs['title'] - content = kwargs['content'] + """ + 邮件发送信号处理函数 + 当收到send_email_signal信号时,异步发送HTML格式邮件并记录发送日志。 + + Args: + sender: 信号发送者 + **kwargs: 包含emailto, title, content等参数 + """ + # 从信号参数中提取邮件信息 + emailto = kwargs['emailto'] # 收件人列表 + title = kwargs['title'] # 邮件标题 + content = kwargs['content'] # 邮件内容 + + # 创建邮件消息对象,设置HTML格式 msg = EmailMultiAlternatives( title, content, - from_email=settings.DEFAULT_FROM_EMAIL, + from_email=settings.DEFAULT_FROM_EMAIL, # 使用配置的默认发件人 to=emailto) - msg.content_subtype = "html" + msg.content_subtype = "html" # 设置内容类型为HTML + # 导入邮件日志模型并创建日志记录 from servermanager.models import EmailSendLog log = EmailSendLog() log.title = title log.content = content - log.emailto = ','.join(emailto) + log.emailto = ','.join(emailto) # 将收件人列表转换为字符串存储 try: + # 尝试发送邮件,send()方法返回发送成功的邮件数量 result = msg.send() - log.send_result = result > 0 + log.send_result = result > 0 # 记录发送结果(成功/失败) except Exception as e: + # 记录邮件发送异常信息 logger.error(f"失败邮箱号: {emailto}, {e}") log.send_result = False + # 保存邮件发送日志记录 log.save() @receiver(oauth_user_login_signal) def oauth_user_login_signal_handler(sender, **kwargs): + """ + OAuth用户登录信号处理函数 + + 处理第三方登录用户的头像保存和缓存清理。 + + Args: + sender: 信号发送者 + **kwargs: 包含用户ID参数 + """ + # 从信号参数中获取用户ID id = kwargs['id'] + # 根据ID获取OAuth用户对象 oauthuser = OAuthUser.objects.get(id=id) + # 获取当前站点域名 site = get_current_site().domain + + # 检查用户头像是否需要下载保存(非本站图片) if oauthuser.picture and not oauthuser.picture.find(site) >= 0: + # 导入头像保存工具函数 from djangoblog.utils import save_user_avatar + # 下载并保存用户头像,更新头像URL oauthuser.picture = save_user_avatar(oauthuser.picture) oauthuser.save() + # 清理侧边栏缓存,确保显示最新用户信息 delete_sidebar_cache() @@ -73,42 +125,79 @@ def model_post_save_callback( using, update_fields, **kwargs): - clearcache = False + """ + 模型保存后回调信号处理函数 + + 监听所有模型的post_save信号,执行相应的缓存清理和通知操作。 + + Args: + sender: 保存的模型类 + instance: 保存的模型实例 + created: 是否为新建记录 + raw: 是否为原始保存 + using: 使用的数据库别名 + update_fields: 更新的字段集合 + **kwargs: 其他参数 + """ + clearcache = False # 标记是否需要清理整个缓存 + + # 跳过管理员操作日志的保存处理 if isinstance(instance, LogEntry): return + + # 检查实例是否有get_full_url方法(通常是文章等可访问的模型) if 'get_full_url' in dir(instance): + # 判断是否为仅更新浏览量字段 is_update_views = update_fields == {'views'} + + # 非测试环境且非仅更新浏览量时,通知搜索引擎 if not settings.TESTING and not is_update_views: try: + # 获取实例的完整URL并通知百度搜索引擎 notify_url = instance.get_full_url() SpiderNotify.baidu_notify([notify_url]) except Exception as ex: logger.error("notify sipder", ex) + + # 非仅更新浏览量时标记需要清理缓存 if not is_update_views: clearcache = True + # 处理评论保存的特殊逻辑 if isinstance(instance, Comment): + # 只处理已启用的评论 if instance.is_enable: + # 获取评论所属文章的URL路径 path = instance.article.get_absolute_url() site = get_current_site().domain + # 处理端口号(如果有) if site.find(':') > 0: site = site[0:site.find(':')] + # 使文章详情页缓存失效 expire_view_cache( path, servername=site, serverport=80, key_prefix='blogdetail') + + # 清理SEO处理器缓存 if cache.get('seo_processor'): cache.delete('seo_processor') + + # 清理文章评论缓存 comment_cache_key = 'article_comments_{id}'.format( id=instance.article.id) cache.delete(comment_cache_key) + + # 清理侧边栏缓存和评论视图缓存 delete_sidebar_cache() delete_view_cache('article_comments', [str(instance.article.pk)]) + # 在新线程中发送评论通知邮件(避免阻塞主线程) _thread.start_new_thread(send_comment_email, (instance,)) + # 如果需要清理整个缓存(文章等主要内容更新时) if clearcache: cache.clear() @@ -116,7 +205,20 @@ def model_post_save_callback( @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): + """ + 用户登录/登出信号处理函数 + + 处理用户认证状态变化时的缓存清理操作。 + + Args: + sender: 信号发送者 + request: HTTP请求对象 + user: 用户对象 + **kwargs: 其他参数 + """ + # 确保用户对象存在且有用户名 if user and user.username: - logger.info(user) - delete_sidebar_cache() - # cache.clear() + logger.info(user) # 记录用户认证日志 + delete_sidebar_cache() # 清理侧边栏缓存 + # 注释掉的完整缓存清理(可根据需要启用) + # cache.clear() \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/elasticsearch_backend.py b/src/DjangoBlog/djangoblog/elasticsearch_backend.py index 4afe498..3c2b7b5 100644 --- a/src/DjangoBlog/djangoblog/elasticsearch_backend.py +++ b/src/DjangoBlog/djangoblog/elasticsearch_backend.py @@ -1,3 +1,17 @@ +""" +Elasticsearch 搜索引擎集成模块 + +本模块提供了Django Haystack与Elasticsearch的深度集成,实现了博客文章的全文搜索功能。 +包含自定义的后端、查询类、搜索表单和引擎配置,支持智能推荐和高效检索。 + +主要功能: +- Elasticsearch文档的索引管理 +- 高级布尔查询和过滤 +- 搜索词智能推荐 +- 搜索结果的高亮和评分 +- 与Django Haystack框架的无缝集成 +""" + from django.utils.encoding import force_str from elasticsearch_dsl import Q from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query @@ -5,98 +19,218 @@ from haystack.forms import ModelSearchForm from haystack.models import SearchResult from haystack.utils import log as logging +# 导入博客相关的文档定义和管理器 from blog.documents import ArticleDocument, ArticleDocumentManager from blog.models import Article +# 初始化模块级日志器 logger = logging.getLogger(__name__) class ElasticSearchBackend(BaseSearchBackend): + """ + Elasticsearch 搜索后端实现 + + 继承自Haystack的BaseSearchBackend,提供与Elasticsearch的交互功能。 + 负责索引创建、文档更新、搜索执行和推荐词生成等核心操作。 + """ + def __init__(self, connection_alias, **connection_options): + """ + 初始化Elasticsearch后端 + + Args: + connection_alias: 连接别名 + **connection_options: 连接配置选项 + """ super( ElasticSearchBackend, self).__init__( connection_alias, **connection_options) + # 初始化文章文档管理器 self.manager = ArticleDocumentManager() + # 启用拼写建议功能 self.include_spelling = True def _get_models(self, iterable): + """ + 获取模型并转换为文档 + + Args: + iterable: 模型实例集合 + + Returns: + list: 转换后的文档对象列表 + """ + # 如果提供了模型集合则使用,否则获取所有文章 models = iterable if iterable and iterable[0] else Article.objects.all() + # 将Django模型转换为Elasticsearch文档 docs = self.manager.convert_to_doc(models) return docs def _create(self, models): + """ + 创建索引并添加文档 + + Args: + models: 要创建索引的模型集合 + """ + # 创建Elasticsearch索引 self.manager.create_index() + # 获取并转换模型为文档 docs = self._get_models(models) + # 重建索引(添加所有文档) self.manager.rebuild(docs) def _delete(self, models): + """ + 删除文档 + + Args: + models: 要删除的模型集合 + + Returns: + bool: 删除操作结果 + """ + # 遍历并删除每个模型对应的文档 for m in models: m.delete() return True def _rebuild(self, models): + """ + 重建索引 + + Args: + models: 要重建索引的模型集合 + """ + # 获取所有文章或指定模型集合 models = models if models else Article.objects.all() + # 转换模型为文档 docs = self.manager.convert_to_doc(models) + # 更新文档到索引 self.manager.update_docs(docs) def update(self, index, iterable, commit=True): + """ + 更新索引文档 + Args: + index: 索引名称 + iterable: 要更新的模型集合 + commit: 是否立即提交(Elasticsearch自动提交,此参数保留) + """ + # 获取模型并转换为文档 models = self._get_models(iterable) + # 更新文档到索引 self.manager.update_docs(models) def remove(self, obj_or_string): + """ + 移除单个文档 + + Args: + obj_or_string: 要移除的模型对象或标识 + """ + # 获取要删除的模型文档 models = self._get_models([obj_or_string]) + # 执行删除操作 self._delete(models) def clear(self, models=None, commit=True): + """ + 清空索引 + + Args: + models: 要清空的模型集合(保留参数) + commit: 是否立即提交(保留参数) + """ + # 移除所有文档(传入None表示清空) self.remove(None) @staticmethod def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" + """ + 获取搜索推荐词 + + 使用Elasticsearch的suggest功能提供搜索词建议。 + 如果没有找到合适的建议词,则返回原搜索词。 + Args: + query (str): 原始搜索词 + + Returns: + str: 处理后的推荐搜索词 + """ + # 构建搜索请求,包含suggest功能 search = ArticleDocument.search() \ .query("match", body=query) \ .suggest('suggest_search', query, term={'field': 'body'}) \ .execute() keywords = [] + # 处理suggest结果 for suggest in search.suggest.suggest_search: if suggest["options"]: + # 使用推荐词 keywords.append(suggest["options"][0]["text"]) else: + # 没有推荐词时使用原词 keywords.append(suggest["text"]) + # 将推荐词列表合并为字符串返回 return ' '.join(keywords) @log_query def search(self, query_string, **kwargs): + """ + 执行搜索查询 + + 核心搜索方法,处理查询字符串并返回匹配的结果。 + 支持分页、过滤和拼写建议。 + + Args: + query_string: 搜索查询字符串 + **kwargs: 其他搜索参数(分页偏移等) + + Returns: + dict: 包含搜索结果、命中数、分面信息和拼写建议的字典 + """ + # 记录搜索查询日志 logger.info('search query_string:' + query_string) + # 获取分页参数 start_offset = kwargs.get('start_offset') end_offset = kwargs.get('end_offset') - # 推荐词搜索 + # 根据是否启用建议搜索,获取处理后的搜索词 if getattr(self, "is_suggest", None): + # 获取推荐搜索词 suggestion = self.get_suggestion(query_string) else: + # 使用原搜索词 suggestion = query_string + # 构建布尔查询:标题或正文匹配,设置最小匹配度70% q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%") + # 构建搜索请求:添加状态和类型过滤,设置分页 search = ArticleDocument.search() \ .query('bool', filter=[q]) \ .filter('term', status='p') \ .filter('term', type='a') \ .source(False)[start_offset: end_offset] + # 执行搜索 results = search.execute() + # 获取总命中数 hits = results['hits'].total raw_results = [] + + # 处理搜索结果,转换为Haystack的SearchResult格式 for raw_result in results['hits']['hits']: app_label = 'blog' model_name = 'Article' @@ -104,47 +238,75 @@ class ElasticSearchBackend(BaseSearchBackend): result_class = SearchResult + # 创建SearchResult对象 result = result_class( app_label, model_name, - raw_result['_id'], - raw_result['_score'], + raw_result['_id'], # 文档ID + raw_result['_score'], # 匹配分数 **additional_fields) raw_results.append(result) + + # 分面信息(当前未使用) facets = {} + # 拼写建议:如果推荐词与原词不同则返回推荐词 spelling_suggestion = None if query_string == suggestion else suggestion + # 返回标准格式的搜索结果 return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, + 'results': raw_results, # 搜索结果列表 + 'hits': hits, # 总命中数 + 'facets': facets, # 分面信息 + 'spelling_suggestion': spelling_suggestion, # 拼写建议 } class ElasticSearchQuery(BaseSearchQuery): + """ + Elasticsearch 查询构建器 + + 继承自Haystack的BaseSearchQuery,负责构建Elasticsearch查询。 + 处理查询字符串的清理和参数构建。 + """ + def _convert_datetime(self, date): + """ + 转换日期时间格式 + + Args: + date: 日期时间对象 + + Returns: + str: 格式化后的日期时间字符串 + """ if hasattr(date, 'hour'): + # 包含时间的完整日期时间格式 return force_str(date.strftime('%Y%m%d%H%M%S')) else: + # 仅日期格式,时间部分补零 return force_str(date.strftime('%Y%m%d000000')) def clean(self, query_fragment): """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. + 清理查询片段 + + 对用户输入的查询词进行清理和转义处理,防止注入攻击。 - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. + Args: + query_fragment: 原始查询片段 + + Returns: + str: 清理后的查询字符串 """ words = query_fragment.split() cleaned_words = [] for word in words: + # 处理保留字(转为小写) if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) + # 处理保留字符(用引号包围) for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word @@ -155,29 +317,86 @@ class ElasticSearchQuery(BaseSearchQuery): return ' '.join(cleaned_words) def build_query_fragment(self, field, filter_type, value): + """ + 构建查询片段 + + Args: + field: 字段名 + filter_type: 过滤器类型 + value: 字段值 + + Returns: + str: 查询片段字符串 + """ return value.query_string def get_count(self): + """ + 获取搜索结果数量 + + Returns: + int: 搜索结果数量 + """ results = self.get_results() return len(results) if results else 0 def get_spelling_suggestion(self, preferred_query=None): + """ + 获取拼写建议 + + Args: + preferred_query: 优先查询词 + + Returns: + str: 拼写建议 + """ return self._spelling_suggestion def build_params(self, spelling_query=None): + """ + 构建搜索参数 + + Args: + spelling_query: 拼写查询词 + + Returns: + dict: 搜索参数字典 + """ kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) return kwargs class ElasticSearchModelSearchForm(ModelSearchForm): + """ + Elasticsearch 模型搜索表单 + + 扩展Haystack的ModelSearchForm,支持建议搜索功能。 + """ def search(self): - # 是否建议搜索 + """ + 执行搜索 + + 重写搜索方法,根据表单数据设置是否启用建议搜索。 + + Returns: + SearchQuerySet: 搜索查询结果集 + """ + # 根据表单数据设置是否启用建议搜索 self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + # 调用父类搜索方法 sqs = super().search() return sqs class ElasticSearchEngine(BaseEngine): + """ + Elasticsearch 搜索引擎配置 + + 配置Haystack使用自定义的Elasticsearch后端和查询类。 + """ + + # 指定自定义的后端类 backend = ElasticSearchBackend - query = ElasticSearchQuery + # 指定自定义的查询类 + query = ElasticSearchQuery \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/feeds.py b/src/DjangoBlog/djangoblog/feeds.py index 8c4e851..1b5136e 100644 --- a/src/DjangoBlog/djangoblog/feeds.py +++ b/src/DjangoBlog/djangoblog/feeds.py @@ -1,3 +1,16 @@ +""" +RSS订阅源生成模块 + +本模块提供了DjangoBlog的RSS订阅功能,基于Django的Feed框架实现。 +生成符合RSS 2.0标准的订阅源,包含文章标题、内容、作者信息等。 + +主要功能: +- 生成博客文章的RSS订阅源 +- 支持Markdown格式的内容渲染 +- 提供作者信息和版权声明 +- 符合RSS 2.0标准规范 +""" + from django.contrib.auth import get_user_model from django.contrib.syndication.views import Feed from django.utils import timezone @@ -8,33 +21,72 @@ from djangoblog.utils import CommonMarkdown class DjangoBlogFeed(Feed): + # 指定使用RSS 2.0格式生成订阅源 feed_type = Rss201rev2Feed + # 订阅源描述信息 description = '大巧无工,重剑无锋.' + # 订阅源标题 title = "且听风吟 大巧无工,重剑无锋. " + # 订阅源链接地址 link = "/feed/" def author_name(self): + """ + 获取作者名称 + + 返回博客第一用户的昵称作为订阅源作者。 + """ return get_user_model().objects.first().nickname def author_link(self): + """ + 获取作者链接 + + 返回博客第一用户的个人主页链接。 + """ return get_user_model().objects.first().get_absolute_url() def items(self): + """ + 获取订阅项目列表 + + 返回最近发布的5篇文章,按发布时间倒序排列。 + 只包含已发布的文章类型。 + """ return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] def item_title(self, item): + """ + 获取单个项目的标题 + """ return item.title def item_description(self, item): + """ + 获取单个项目的描述内容 + + 将文章的Markdown内容转换为HTML格式。 + """ return CommonMarkdown.get_markdown(item.body) def feed_copyright(self): + """ + 获取订阅源版权信息 + + 生成包含当前年份的版权声明。 + """ now = timezone.now() return "Copyright© {year} 且听风吟".format(year=now.year) def item_link(self, item): + """ + 获取单个项目的链接 + """ return item.get_absolute_url() def item_guid(self, item): - return + """ + 获取单个项目的全局唯一标识符 + """ + return \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/logentryadmin.py b/src/DjangoBlog/djangoblog/logentryadmin.py index 2f6a535..718b591 100644 --- a/src/DjangoBlog/djangoblog/logentryadmin.py +++ b/src/DjangoBlog/djangoblog/logentryadmin.py @@ -1,3 +1,16 @@ +""" +管理员操作日志后台管理模块 + +本模块提供了Django管理员操作日志的自定义后台管理界面。 +用于查看和追踪管理员在后台的所有操作记录,包括增删改等操作。 + +主要功能: +- 自定义日志列表显示格式 +- 提供对象和用户的超链接跳转 +- 权限控制和操作限制 +- 搜索和过滤功能 +""" + from django.contrib import admin from django.contrib.admin.models import DELETION from django.contrib.contenttypes.models import ContentType @@ -9,45 +22,73 @@ from django.utils.translation import gettext_lazy as _ class LogEntryAdmin(admin.ModelAdmin): + """ + 管理员操作日志后台管理类 + + 自定义Django默认的LogEntry模型管理界面,提供更好的用户体验和功能。 + """ + + # 列表页过滤器配置 - 按内容类型过滤 list_filter = [ 'content_type' ] + # 搜索字段配置 - 支持按对象表示和变更消息搜索 search_fields = [ 'object_repr', 'change_message' ] + # 列表页可点击的字段 - 操作时间作为链接 list_display_links = [ 'action_time', 'get_change_message', ] + + # 列表页显示的字段 list_display = [ - 'action_time', - 'user_link', - 'content_type', - 'object_link', - 'get_change_message', + 'action_time', # 操作时间 + 'user_link', # 操作用户(带链接) + 'content_type', # 内容类型 + 'object_link', # 操作对象(带链接) + 'get_change_message', # 变更消息 ] def has_add_permission(self, request): + """ + 禁用添加权限 - 日志记录只能由系统自动创建 + """ return False def has_change_permission(self, request, obj=None): + """ + 控制修改权限 - 只允许超级用户或具有特定权限的用户查看 + """ return ( request.user.is_superuser or request.user.has_perm('admin.change_logentry') ) and request.method != 'POST' def has_delete_permission(self, request, obj=None): + """ + 禁用删除权限 - 防止误删重要的操作日志 + """ return False def object_link(self, obj): + """ + 生成操作对象的超链接 + + 对于非删除操作,尝试生成指向对象编辑页面的链接。 + 如果是删除操作或无法生成链接,则返回纯文本表示。 + """ + # 转义对象表示字符串,防止XSS攻击 object_link = escape(obj.object_repr) content_type = obj.content_type + # 对于非删除操作且内容类型存在的情况,尝试生成链接 if obj.action_flag != DELETION and content_type is not None: - # try returning an actual link instead of object repr string + # 尝试返回实际链接而不是对象表示字符串 try: url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, @@ -56,17 +97,26 @@ class LogEntryAdmin(admin.ModelAdmin): ) object_link = '
{}'.format(url, object_link) except NoReverseMatch: + # 如果无法生成反向URL,保持原样 pass return mark_safe(object_link) + # 设置对象链接的排序字段和显示名称 object_link.admin_order_field = 'object_repr' object_link.short_description = _('object') def user_link(self, obj): + """ + 生成操作用户的超链接 + + 尝试生成指向用户编辑页面的链接,如果无法生成则返回纯文本。 + """ + # 获取用户模型的内容类型 content_type = ContentType.objects.get_for_model(type(obj.user)) + # 转义用户表示字符串 user_link = escape(force_str(obj.user)) try: - # try returning an actual link instead of object repr string + # 尝试返回实际链接而不是用户表示字符串 url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), @@ -74,18 +124,26 @@ class LogEntryAdmin(admin.ModelAdmin): ) user_link = '{}'.format(url, user_link) except NoReverseMatch: + # 如果无法生成反向URL,保持原样 pass return mark_safe(user_link) + # 设置用户链接的排序字段和显示名称 user_link.admin_order_field = 'user' user_link.short_description = _('user') def get_queryset(self, request): + """ + 优化查询集 - 预取关联的内容类型数据 + """ queryset = super(LogEntryAdmin, self).get_queryset(request) return queryset.prefetch_related('content_type') def get_actions(self, request): + """ + 移除批量删除操作 - 防止误删日志记录 + """ actions = super(LogEntryAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] - return actions + return actions \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py b/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py index 2b4be5c..3a85baa 100644 --- a/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py +++ b/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py @@ -1,41 +1,93 @@ +""" +插件系统基础模块 + +本模块提供了插件系统的基础框架,定义了所有插件的基类BasePlugin。 +实现了插件的元数据管理、初始化流程、钩子注册和插件信息获取等核心功能。 + +主要功能: +- 插件元数据定义和验证 +- 标准化的插件初始化流程 +- 钩子注册机制 +- 插件信息统一管理 +""" + import logging +# 初始化模块级日志器,用于记录插件相关操作 logger = logging.getLogger(__name__) class BasePlugin: - # 插件元数据 - PLUGIN_NAME = None - PLUGIN_DESCRIPTION = None - PLUGIN_VERSION = None + """ + 插件基类 + + 所有具体插件的父类,定义了插件的标准接口和基本行为。 + 提供了插件元数据管理、初始化、钩子注册等基础功能。 + + 类属性: + PLUGIN_NAME: 插件名称,必须由子类定义 + PLUGIN_DESCRIPTION: 插件描述,必须由子类定义 + PLUGIN_VERSION: 插件版本,必须由子类定义 + """ + + # 插件元数据 - 必须由子类重写的类属性 + PLUGIN_NAME = None # 插件名称标识 + PLUGIN_DESCRIPTION = None # 插件功能描述 + PLUGIN_VERSION = None # 插件版本号 def __init__(self): + """ + 插件基类构造函数 + + 执行插件初始化流程,包括: + 1. 验证插件元数据完整性 + 2. 调用插件初始化方法 + 3. 注册插件钩子 + + Raises: + ValueError: 当插件元数据未完整定义时抛出 + """ + # 验证插件元数据是否完整定义 if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") + + # 执行插件初始化逻辑 self.init_plugin() + # 注册插件钩子函数 self.register_hooks() def init_plugin(self): """ 插件初始化逻辑 - 子类可以重写此方法来实现特定的初始化操作 + + 子类可以重写此方法来实现特定的初始化操作。 + 基类实现仅记录初始化日志信息。 """ + # 记录插件初始化成功日志 logger.info(f'{self.PLUGIN_NAME} initialized.') def register_hooks(self): """ 注册插件钩子 - 子类可以重写此方法来注册特定的钩子 + + 子类可以重写此方法来注册特定的钩子。 + 基类实现为空方法,由子类按需实现具体钩子注册逻辑。 """ + # 基类不实现具体钩子注册,由子类重写 pass def get_plugin_info(self): """ 获取插件信息 - :return: 包含插件元数据的字典 + + 返回包含插件完整元数据的字典,用于插件信息展示和管理。 + + Returns: + dict: 包含插件名称、描述和版本的字典对象 """ + # 构建并返回插件元数据字典 return { - 'name': self.PLUGIN_NAME, - 'description': self.PLUGIN_DESCRIPTION, - 'version': self.PLUGIN_VERSION - } + 'name': self.PLUGIN_NAME, # 插件名称 + 'description': self.PLUGIN_DESCRIPTION, # 插件功能描述 + 'version': self.PLUGIN_VERSION # 插件版本号 + } \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py b/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py index 6685b7c..efd362e 100644 --- a/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py +++ b/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py @@ -1,7 +1,26 @@ +""" +钩子事件常量定义模块 + +本模块定义了文章相关的钩子事件常量,用于在插件系统中标识不同的事件类型。 +这些常量作为事件触发器名称,用于在特定时机执行注册的钩子函数。 + +主要用途: +- 统一管理事件名称常量 +- 提供类型安全的钩子标识 +- 便于在插件系统中注册和触发事件 +""" + +# 文章详情加载事件 - 当文章详情数据被加载时触发 ARTICLE_DETAIL_LOAD = 'article_detail_load' + +# 文章创建事件 - 当新文章被创建时触发 ARTICLE_CREATE = 'article_create' + +# 文章更新事件 - 当现有文章被修改时触发 ARTICLE_UPDATE = 'article_update' -ARTICLE_DELETE = 'article_delete' -ARTICLE_CONTENT_HOOK_NAME = "the_content" +# 文章删除事件 - 当文章被删除时触发 +ARTICLE_DELETE = 'article_delete' +# 文章内容处理钩子名称 - 专门用于处理文章内容的钩子标识 +ARTICLE_CONTENT_HOOK_NAME = "the_content" \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/plugin_manage/hooks.py b/src/DjangoBlog/djangoblog/plugin_manage/hooks.py index d712540..9563c3b 100644 --- a/src/DjangoBlog/djangoblog/plugin_manage/hooks.py +++ b/src/DjangoBlog/djangoblog/plugin_manage/hooks.py @@ -1,44 +1,116 @@ +""" +钩子管理系统模块 + +本模块提供了完整的钩子(Hook)管理机制,支持两种类型的钩子: +1. Action Hook(动作钩子):按顺序执行注册的回调函数,不返回值 +2. Filter Hook(过滤器钩子):对输入值进行链式处理,返回处理后的值 + +主要功能: +- 钩子回调函数的注册管理 +- 动作钩子的顺序执行 +- 过滤器钩子的链式处理 +- 完善的错误处理和日志记录 +""" + import logging +# 初始化模块级日志器,用于记录钩子相关操作 logger = logging.getLogger(__name__) +# 全局钩子存储字典 +# 结构:{hook_name: [callback1, callback2, ...]} _hooks = {} def register(hook_name: str, callback: callable): """ - 注册一个钩子回调。 + 注册一个钩子回调函数 + + 将回调函数注册到指定的钩子名称下,支持同一钩子名称注册多个回调函数。 + 回调函数将按照注册顺序执行。 + + Args: + hook_name (str): 钩子名称标识 + callback (callable): 要注册的回调函数 + + Examples: + >>> register('article_create', my_callback_function) """ + # 检查钩子名称是否已存在,不存在则初始化空列表 if hook_name not in _hooks: _hooks[hook_name] = [] + + # 将回调函数添加到对应钩子的回调列表中 _hooks[hook_name].append(callback) + + # 记录调试日志,跟踪钩子注册情况 logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") def run_action(hook_name: str, *args, **kwargs): """ - 执行一个 Action Hook。 - 它会按顺序执行所有注册到该钩子上的回调函数。 + 执行一个 Action Hook(动作钩子) + + 按注册顺序执行所有注册到该钩子上的回调函数。 + 动作钩子主要用于执行副作用操作,不返回任何值。 + + Args: + hook_name (str): 要执行的钩子名称 + *args: 传递给回调函数的位置参数 + **kwargs: 传递给回调函数的关键字参数 + + Examples: + >>> run_action('article_create', article_obj, user_obj) """ + # 检查指定钩子是否有注册的回调函数 if hook_name in _hooks: + # 记录钩子执行开始日志 logger.debug(f"Running action hook '{hook_name}'") + + # 遍历该钩子下的所有回调函数 for callback in _hooks[hook_name]: try: + # 执行回调函数,传入所有参数 callback(*args, **kwargs) except Exception as e: - logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + # 捕获并记录回调函数执行中的异常,但不中断其他回调的执行 + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", + exc_info=True) def apply_filters(hook_name: str, value, *args, **kwargs): """ - 执行一个 Filter Hook。 - 它会把 value 依次传递给所有注册的回调函数进行处理。 + 执行一个 Filter Hook(过滤器钩子) + + 将输入值依次传递给所有注册的回调函数进行链式处理。 + 每个回调函数的返回值将作为下一个回调函数的输入值。 + + Args: + hook_name (str): 要执行的过滤器钩子名称 + value: 初始输入值,将被回调函数处理 + *args: 传递给回调函数的额外位置参数 + **kwargs: 传递给回调函数的额外关键字参数 + + Returns: + any: 经过所有回调函数处理后的最终值 + + Examples: + >>> processed_content = apply_filters('the_content', raw_content) """ + # 检查指定过滤器钩子是否有注册的回调函数 if hook_name in _hooks: + # 记录过滤器应用开始日志 logger.debug(f"Applying filter hook '{hook_name}'") + + # 遍历该钩子下的所有回调函数 for callback in _hooks[hook_name]: try: + # 将当前值传递给回调函数处理,并更新为返回值 value = callback(value, *args, **kwargs) except Exception as e: - logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) - return value + # 捕获并记录回调函数执行中的异常,但不中断处理链 + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", + exc_info=True) + + # 返回经过所有过滤器处理后的最终值 + return value \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/plugin_manage/loader.py b/src/DjangoBlog/djangoblog/plugin_manage/loader.py index 12e824b..66e435a 100644 --- a/src/DjangoBlog/djangoblog/plugin_manage/loader.py +++ b/src/DjangoBlog/djangoblog/plugin_manage/loader.py @@ -1,19 +1,54 @@ +""" +插件动态加载模块 + +本模块提供了插件系统的动态加载功能,负责在Django应用启动时自动加载和初始化已激活的插件。 +通过扫描插件目录并导入插件模块,实现插件的热插拔管理。 + +主要功能: +- 动态扫描插件目录 +- 按配置加载激活的插件 +- 插件模块的导入和初始化 +- 加载状态的日志记录 +""" + import os import logging from django.conf import settings +# 初始化模块级日志器,用于记录插件加载过程 logger = logging.getLogger(__name__) + def load_plugins(): """ - Dynamically loads and initializes plugins from the 'plugins' directory. - This function is intended to be called when the Django app registry is ready. + 动态加载并初始化插件 + + 从配置的插件目录中加载所有激活的插件模块。 + 此函数应在Django应用注册表准备就绪后调用。 + + 加载流程: + 1. 遍历settings.ACTIVE_PLUGINS中配置的插件名称 + 2. 检查插件目录和plugin.py文件是否存在 + 3. 动态导入插件模块 + 4. 记录加载成功或失败状态 + + 注意:插件模块的导入会触发其内部代码执行,包括类定义和注册逻辑 """ + # 遍历所有在配置中激活的插件名称 for plugin_name in settings.ACTIVE_PLUGINS: + # 构建插件目录的完整路径 plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + + # 检查插件目录是否存在且包含plugin.py文件 if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): try: + # 动态导入插件模块,使用点分模块路径格式 + # 导入操作会执行插件模块中的代码,完成插件注册 __import__(f'plugins.{plugin_name}.plugin') + + # 记录插件加载成功日志 logger.info(f"Successfully loaded plugin: {plugin_name}") + except ImportError as e: - logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file + # 捕获导入异常,记录详细的错误信息 + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/settings.py b/src/DjangoBlog/djangoblog/settings.py index 784668b..0a62e90 100644 --- a/src/DjangoBlog/djangoblog/settings.py +++ b/src/DjangoBlog/djangoblog/settings.py @@ -1,14 +1,20 @@ """ -Django settings for djangoblog project. - -Generated by 'django-admin startproject' using Django 1.10.2. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.10/ref/settings/ +DjangoBlog 项目配置文件 + +本模块包含DjangoBlog项目的所有配置设置,包括数据库、应用、中间件、国际化、缓存、邮件等。 +根据Django 1.10+的配置规范组织,支持开发和生产环境的不同配置。 + +主要配置类别: +- 基础路径和密钥配置 +- 应用和中间件配置 +- 数据库和缓存配置 +- 国际化设置 +- 静态文件和媒体文件配置 +- 邮件和日志配置 +- 安全相关配置 +- 搜索和插件系统配置 """ + import os import sys from pathlib import Path @@ -17,34 +23,46 @@ from django.utils.translation import gettext_lazy as _ def env_to_bool(env, default): + """ + 环境变量转布尔值工具函数 + + 将环境变量的字符串值转换为布尔值,用于灵活的配置开关。 + + Args: + env: 环境变量名 + default: 默认值 + + Returns: + bool: 转换后的布尔值 + """ str_val = os.environ.get(env) return default if str_val is None else str_val == 'True' -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# 构建项目基础路径 - 使用pathlib现代路径处理方式 BASE_DIR = Path(__file__).resolve().parent.parent -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! +# 安全密钥配置 - 生产环境必须从环境变量获取 SECRET_KEY = os.environ.get( 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' -# SECURITY WARNING: don't run with debug turned on in production! + +# 调试模式开关 - 生产环境必须关闭 DEBUG = env_to_bool('DJANGO_DEBUG', True) -# DEBUG = False + +# 测试模式标识 - 根据命令行参数判断是否为测试环境 TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' -# ALLOWED_HOSTS = [] +# 允许的主机名配置 - 生产环境需要具体指定 ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] -# django 4.0新增配置 -CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition +# Django 4.0新增CSRF信任源配置 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# 已安装应用列表 - 定义项目使用的所有Django应用 INSTALLED_APPS = [ - # 'django.contrib.admin', + # 使用简化的Admin配置 'django.contrib.admin.apps.SimpleAdminConfig', + # Django核心功能应用 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -52,37 +70,54 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor', - 'djangoblog' + # 第三方应用 + 'mdeditor', # Markdown编辑器 + 'haystack', # 搜索框架 + 'compressor', # 静态文件压缩 + # 项目自定义应用 + 'blog', # 博客核心功能 + 'accounts', # 用户账户管理 + 'comments', # 评论系统 + 'oauth', # OAuth认证 + 'servermanager', # 服务器管理 + 'owntracks', # 位置追踪 + 'djangoblog' # 项目主应用 ] +# 中间件配置 - 定义请求处理管道 MIDDLEWARE = [ - + # 安全相关中间件 'django.middleware.security.SecurityMiddleware', + # 会话管理中间件 'django.contrib.sessions.middleware.SessionMiddleware', + # 国际化中间件 'django.middleware.locale.LocaleMiddleware', + # Gzip压缩中间件 'django.middleware.gzip.GZipMiddleware', + # 缓存中间件(注释状态) # 'django.middleware.cache.UpdateCacheMiddleware', + # 通用中间件 'django.middleware.common.CommonMiddleware', + # 缓存中间件(注释状态) # 'django.middleware.cache.FetchFromCacheMiddleware', + # CSRF保护中间件 'django.middleware.csrf.CsrfViewMiddleware', + # 认证中间件 'django.contrib.auth.middleware.AuthenticationMiddleware', + # 消息中间件 'django.contrib.messages.middleware.MessageMiddleware', + # 点击劫持保护中间件 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # 条件GET中间件 'django.middleware.http.ConditionalGetMiddleware', + # 自定义在线用户中间件 'blog.middleware.OnlineMiddleware' ] +# 根URL配置 ROOT_URLCONF = 'djangoblog.urls' +# 模板配置 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -94,18 +129,17 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + # 自定义SEO处理器 'blog.context_processors.seo_processor' ], }, }, ] +# WSGI应用配置 WSGI_APPLICATION = 'djangoblog.wsgi.application' -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - +# 数据库配置 - 使用MySQL作为默认数据库 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -117,9 +151,7 @@ DATABASES = { } } -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - +# 密码验证器配置 AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -135,62 +167,75 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# 国际化配置 LANGUAGES = ( ('en', _('English')), ('zh-hans', _('Simplified Chinese')), ('zh-hant', _('Traditional Chinese')), ) + +# 本地化文件路径 LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), ) +# 默认语言代码 LANGUAGE_CODE = 'zh-hans' +# 时区配置 TIME_ZONE = 'Asia/Shanghai' +# 国际化开关 USE_I18N = True +# 本地化开关 USE_L10N = True +# 时区支持开关 USE_TZ = False -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - - +# Haystack搜索配置 HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), }, } -# Automatically update searching index + +# 实时更新搜索索引 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# Allow user login with username and password + +# 认证后端配置 - 支持邮箱或用户名登录 AUTHENTICATION_BACKENDS = [ 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# 静态文件配置 STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') - STATIC_URL = '/static/' STATICFILES = os.path.join(BASE_DIR, 'static') +# 自定义用户模型 AUTH_USER_MODEL = 'accounts.BlogUser' + +# 登录URL LOGIN_URL = '/login/' +# 时间格式配置 TIME_FORMAT = '%Y-%m-%d %H:%M:%S' DATE_TIME_FORMAT = '%Y-%m-%d' -# bootstrap color styles +# Bootstrap颜色类型 BOOTSTRAP_COLOR_TYPES = [ 'default', 'primary', 'success', 'info', 'warning', 'danger' ] -# paginate +# 分页配置 PAGINATE_BY = 10 -# http cache timeout + +# HTTP缓存超时时间 CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting + +# 缓存配置 - 默认使用本地内存缓存 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -198,7 +243,8 @@ CACHES = { 'LOCATION': 'unique-snowflake', } } -# 使用redis作为缓存 + +# 如果配置了Redis环境变量,则使用Redis缓存 if os.environ.get("DJANGO_REDIS_URL"): CACHES = { 'default': { @@ -207,11 +253,14 @@ if os.environ.get("DJANGO_REDIS_URL"): } } +# 站点ID SITE_ID = 1 + +# 百度站长平台通知URL BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' -# Email: +# 邮件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) @@ -221,12 +270,15 @@ EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER -# Setting debug=false did NOT handle except email notifications + +# 管理员邮箱配置 - 用于错误报告 ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] -# WX ADMIN password(Two times md5) + +# 微信管理员密码(两次MD5加密) WXADMIN = os.environ.get( 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' +# 日志配置 LOG_PATH = os.path.join(BASE_DIR, 'logs') if not os.path.exists(LOG_PATH): os.makedirs(LOG_PATH, exist_ok=True) @@ -292,36 +344,41 @@ LOGGING = { } } +# 静态文件查找器配置 STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other + # 压缩器查找器 'compressor.finders.CompressorFinder', ) + +# 静态文件压缩配置 COMPRESS_ENABLED = True # COMPRESS_OFFLINE = True - COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones + # 从相对URL创建绝对URL 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer + # CSS压缩过滤器 'compressor.filters.cssmin.CSSMinFilter' ] COMPRESS_JS_FILTERS = [ 'compressor.filters.jsmin.JSMinFilter' ] +# 媒体文件配置 MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_URL = '/media/' + +# 框架选项配置 X_FRAME_OPTIONS = 'SAMEORIGIN' -# 安全头部配置 - 防XSS和其他攻击 +# 安全头部配置 SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' -# 内容安全策略 (CSP) - 防XSS攻击 +# 内容安全策略配置 CSP_DEFAULT_SRC = ["'self'"] CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"] CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"] @@ -331,8 +388,10 @@ CSP_CONNECT_SRC = ["'self'"] CSP_FRAME_SRC = ["'none'"] CSP_OBJECT_SRC = ["'none'"] +# 默认自增主键字段类型 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Elasticsearch配置(如果配置了环境变量) if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): ELASTICSEARCH_DSL = { 'default': { @@ -345,7 +404,7 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): }, } -# Plugin System +# 插件系统配置 PLUGINS_DIR = BASE_DIR / 'plugins' ACTIVE_PLUGINS = [ 'article_copyright', @@ -354,5 +413,4 @@ ACTIVE_PLUGINS = [ 'view_count', 'seo_optimizer', 'image_lazy_loading', -] - +] \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/sitemap.py b/src/DjangoBlog/djangoblog/sitemap.py index 8b7d446..90e23c6 100644 --- a/src/DjangoBlog/djangoblog/sitemap.py +++ b/src/DjangoBlog/djangoblog/sitemap.py @@ -1,3 +1,16 @@ +""" +站点地图生成模块 + +本模块定义了DjangoBlog的站点地图(Sitemap)配置,用于生成搜索引擎友好的XML站点地图。 +包含静态页面、文章、分类、标签和用户页面的站点地图配置。 + +主要功能: +- 生成符合搜索引擎标准的XML站点地图 +- 为不同类型的内容设置不同的更新频率和优先级 +- 提供最后修改时间信息 +- 帮助搜索引擎更好地索引网站内容 +""" + from django.contrib.sitemaps import Sitemap from django.urls import reverse @@ -5,55 +18,166 @@ from blog.models import Article, Category, Tag class StaticViewSitemap(Sitemap): + """ + 静态页面站点地图 + + 用于生成首页等静态页面的站点地图条目。 + """ + # 优先级设置(0.0-1.0) priority = 0.5 + # 内容更新频率 changefreq = 'daily' def items(self): + """ + 获取包含在站点地图中的项目 + + 返回需要生成站点地图的URL名称列表。 + """ return ['blog:index', ] def location(self, item): + """ + 生成项目的完整URL + + Args: + item: URL名称 + + Returns: + str: 完整的URL路径 + """ return reverse(item) class ArticleSiteMap(Sitemap): + """ + 文章页面站点地图 + + 用于生成所有已发布文章的站点地图条目。 + """ + # 文章更新频率 - 每月更新 changefreq = "monthly" + # 文章优先级 - 较高优先级 priority = "0.6" def items(self): + """ + 获取所有已发布的文章 + + Returns: + QuerySet: 已发布文章的查询集 + """ return Article.objects.filter(status='p') def lastmod(self, obj): + """ + 获取文章的最后修改时间 + + Args: + obj: 文章对象 + + Returns: + datetime: 最后修改时间 + """ return obj.last_modify_time class CategorySiteMap(Sitemap): + """ + 分类页面站点地图 + + 用于生成所有文章分类的站点地图条目。 + """ + # 分类更新频率 - 每周更新 changefreq = "Weekly" + # 分类优先级 - 较高优先级 priority = "0.6" def items(self): + """ + 获取所有分类 + + Returns: + QuerySet: 所有分类的查询集 + """ return Category.objects.all() def lastmod(self, obj): + """ + 获取分类的最后修改时间 + + Args: + obj: 分类对象 + + Returns: + datetime: 最后修改时间 + """ return obj.last_modify_time class TagSiteMap(Sitemap): + """ + 标签页面站点地图 + + 用于生成所有标签的站点地图条目。 + """ + # 标签更新频率 - 每周更新 changefreq = "Weekly" + # 标签优先级 - 中等优先级 priority = "0.3" def items(self): + """ + 获取所有标签 + + Returns: + QuerySet: 所有标签的查询集 + """ return Tag.objects.all() def lastmod(self, obj): + """ + 获取标签的最后修改时间 + + Args: + obj: 标签对象 + + Returns: + datetime: 最后修改时间 + """ return obj.last_modify_time class UserSiteMap(Sitemap): + """ + 用户页面站点地图 + + 用于生成所有文章作者的站点地图条目。 + """ + # 用户页面更新频率 - 每周更新 changefreq = "Weekly" + # 用户页面优先级 - 中等优先级 priority = "0.3" def items(self): + """ + 获取所有发表过文章的用户 + + 通过文章作者去重,确保每个用户只出现一次。 + + Returns: + list: 用户对象列表 + """ return list(set(map(lambda x: x.author, Article.objects.all()))) def lastmod(self, obj): - return obj.date_joined + """ + 获取用户的注册时间 + + Args: + obj: 用户对象 + + Returns: + datetime: 用户注册时间 + """ + return obj.date_joined \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/spider_notify.py b/src/DjangoBlog/djangoblog/spider_notify.py index 7b909e9..ff57d61 100644 --- a/src/DjangoBlog/djangoblog/spider_notify.py +++ b/src/DjangoBlog/djangoblog/spider_notify.py @@ -1,21 +1,65 @@ +""" +搜索引擎蜘蛛通知模块 + +本模块提供了向搜索引擎主动推送URL更新的功能,主要用于通知搜索引擎及时抓取网站内容更新。 +目前主要支持百度搜索引擎的URL推送接口。 + +主要功能: +- 向百度站长平台推送URL更新 +- 批量推送URL列表 +- 错误处理和日志记录 +- 统一的推送接口封装 +""" + import logging import requests from django.conf import settings +# 初始化模块级日志器 logger = logging.getLogger(__name__) class SpiderNotify(): + """ + 搜索引擎蜘蛛通知类 + + 提供静态方法用于向搜索引擎推送URL更新,帮助搜索引擎及时发现网站内容变化。 + """ + @staticmethod def baidu_notify(urls): + """ + 向百度站长平台推送URL更新 + + 将更新的URL列表推送给百度搜索引擎,加速内容收录。 + + Args: + urls: 需要推送的URL列表,可以是字符串或字符串列表 + + Note: + 使用settings.BAIDU_NOTIFY_URL配置的百度推送接口 + """ try: + # 将URL列表转换为换行分隔的字符串格式 data = '\n'.join(urls) + # 向百度推送接口发送POST请求 result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + # 记录推送结果日志 logger.info(result.text) except Exception as e: + # 捕获并记录推送过程中的异常 logger.error(e) @staticmethod def notify(url): - SpiderNotify.baidu_notify(url) + """ + 统一的URL推送接口 + + 提供简化的推送方法,支持单个URL或URL列表的推送。 + + Args: + url: 单个URL字符串或URL列表 + """ + # 调用百度推送方法处理URL + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/tests.py b/src/DjangoBlog/djangoblog/tests.py index 01237d9..870b2e3 100644 --- a/src/DjangoBlog/djangoblog/tests.py +++ b/src/DjangoBlog/djangoblog/tests.py @@ -1,15 +1,51 @@ +""" +DjangoBlog 单元测试模块 + +本模块包含DjangoBlog项目的单元测试用例,用于验证工具函数和核心功能的正确性。 +基于Django的TestCase框架,确保代码质量和功能稳定性。 + +测试功能: +- 加密工具函数测试 +- Markdown渲染功能测试 +- 字典转换URL参数测试 +""" + from django.test import TestCase from djangoblog.utils import * class DjangoBlogTest(TestCase): + """ + DjangoBlog 核心功能测试类 + + 继承自Django的TestCase,提供项目核心功能的自动化测试。 + """ + def setUp(self): + """ + 测试前置设置方法 + + 在每个测试方法执行前运行,用于初始化测试环境。 + 当前测试用例无需特殊设置,保留空实现。 + """ pass def test_utils(self): + """ + 工具函数综合测试方法 + + 测试工具模块中的多个核心功能: + 1. SHA256加密功能 + 2. Markdown文本渲染功能 + 3. 字典转URL参数字符串功能 + """ + # 测试SHA256加密功能 md5 = get_sha256('test') + # 验证加密结果不为空 self.assertIsNotNone(md5) + + # 测试Markdown渲染功能 c = CommonMarkdown.get_markdown(''' # Title1 @@ -23,10 +59,14 @@ class DjangoBlogTest(TestCase): ''') + # 验证Markdown渲染结果不为空 self.assertIsNotNone(c) + + # 测试字典转URL参数功能 d = { 'd': 'key1', 'd2': 'key2' } data = parse_dict_to_url(d) - self.assertIsNotNone(data) + # 验证转换结果不为空 + self.assertIsNotNone(data) \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/urls.py b/src/DjangoBlog/djangoblog/urls.py index 6a9e1de..a7c33ac 100644 --- a/src/DjangoBlog/djangoblog/urls.py +++ b/src/DjangoBlog/djangoblog/urls.py @@ -1,18 +1,19 @@ -"""djangoblog URL Configuration +""" +DjangoBlog 项目URL配置模块 + +本模块定义了DjangoBlog项目的所有URL路由配置,包括管理后台、博客、评论、用户认证等功能的URL映射。 +采用Django 1.10+的URL配置方式,支持国际化路由和静态文件服务。 -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +主要路由分组: +- 国际化路由配置 +- 管理后台路由 +- 博客应用路由 +- 第三方应用路由 +- 站点地图和订阅源 +- 搜索功能路由 +- 静态文件服务 """ + from django.conf import settings from django.conf.urls.i18n import i18n_patterns from django.conf.urls.static import static @@ -23,12 +24,14 @@ from haystack.views import search_view_factory from django.http import JsonResponse import time +# 导入项目自定义模块 from blog.views import EsSearchView from djangoblog.admin_site import admin_site from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.feeds import DjangoBlogFeed from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +# 站点地图配置字典 - 定义不同类型内容的站点地图 sitemaps = { 'blog': ArticleSiteMap, @@ -38,6 +41,7 @@ sitemaps = { 'static': StaticViewSitemap } +# 自定义错误处理视图配置 handler404 = 'blog.views.page_not_found_view' handler500 = 'blog.views.server_error_view' handle403 = 'blog.views.permission_denied_view' @@ -53,10 +57,12 @@ def health_check(request): 'timestamp': time.time() }) +# 基础URL模式配置 - 不包含语言前缀的URL urlpatterns = [ path('i18n/', include('django.conf.urls.i18n')), path('health/', health_check, name='health_check'), ] +# 国际化URL模式配置 - 自动添加语言前缀的URL urlpatterns += i18n_patterns( re_path(r'^admin/', admin_site.urls), re_path(r'', include('blog.urls', namespace='blog')), @@ -73,6 +79,7 @@ urlpatterns += i18n_patterns( re_path(r'', include('servermanager.urls', namespace='servermanager')), re_path(r'', include('owntracks.urls', namespace='owntracks')) , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +# 开发环境媒体文件服务配置 if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/utils.py b/src/DjangoBlog/djangoblog/utils.py index 91d2b91..96a8836 100644 --- a/src/DjangoBlog/djangoblog/utils.py +++ b/src/DjangoBlog/djangoblog/utils.py @@ -1,6 +1,20 @@ #!/usr/bin/env python # encoding: utf-8 +""" +DjangoBlog 通用工具函数模块 + +本模块提供了DjangoBlog项目的各种通用工具函数,包括缓存装饰器、Markdown处理、 +邮件发送、安全过滤等核心功能。这些工具函数在整个项目中广泛使用。 + +主要功能: +- 缓存管理和装饰器 +- Markdown文本处理和转换 +- 电子邮件发送功能 +- 安全HTML过滤和XSS防护 +- 随机码生成和URL处理 +- 用户头像下载和管理 +""" import logging import os @@ -17,33 +31,65 @@ from django.contrib.sites.models import Site from django.core.cache import cache from django.templatetags.static import static +# 初始化模块级日志器 logger = logging.getLogger(__name__) def get_max_articleid_commentid(): + """ + 获取最大文章ID和评论ID + + 用于生成新文章或评论时的ID参考。 + + Returns: + tuple: (最大文章ID, 最大评论ID) + """ from blog.models import Article from comments.models import Comment return (Article.objects.latest().pk, Comment.objects.latest().pk) def get_sha256(str): + """ + SHA256加密函数 + + Args: + str: 要加密的字符串 + + Returns: + str: SHA256加密后的十六进制字符串 + """ m = sha256(str.encode('utf-8')) return m.hexdigest() def cache_decorator(expiration=3 * 60): + """ + 缓存装饰器 + + 为函数添加缓存功能,减少重复计算和数据库查询。 + + Args: + expiration: 缓存过期时间(秒),默认3分钟 + + Returns: + function: 装饰后的函数 + """ + def wrapper(func): def news(*args, **kwargs): try: + # 尝试从视图类获取缓存键 view = args[0] key = view.get_cache_key() except: key = None if not key: + # 生成基于函数参数的唯一缓存键 unique_str = repr((func, args, kwargs)) - m = sha256(unique_str.encode('utf-8')) key = m.hexdigest() + # 尝试从缓存获取值 value = cache.get(key) if value is not None: # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) @@ -52,6 +98,7 @@ def cache_decorator(expiration=3 * 60): else: return value else: + # 缓存未命中,执行函数并设置缓存 logger.debug( 'cache_decorator set cache:%s key:%s' % (func.__name__, key)) @@ -70,19 +117,27 @@ def cache_decorator(expiration=3 * 60): def expire_view_cache(path, servername, serverport, key_prefix=None): ''' 刷新视图缓存 - :param path:url路径 - :param servername:host - :param serverport:端口 - :param key_prefix:前缀 - :return:是否成功 + + 使指定路径的视图缓存失效,确保内容更新后及时反映。 + + Args: + path: URL路径 + servername: 主机名 + serverport: 端口号 + key_prefix: 缓存键前缀 + + Returns: + bool: 是否成功删除缓存 ''' from django.http import HttpRequest from django.utils.cache import get_cache_key + # 创建模拟请求对象用于生成缓存键 request = HttpRequest() request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} request.path = path + # 获取缓存键并删除对应缓存 key = get_cache_key(request, key_prefix=key_prefix, cache=cache) if key: logger.info('expire_view_cache:get key:{path}'.format(path=path)) @@ -94,19 +149,43 @@ def expire_view_cache(path, servername, serverport, key_prefix=None): @cache_decorator() def get_current_site(): + """ + 获取当前站点信息 + + 返回当前Django站点的配置信息,带缓存功能。 + + Returns: + Site: 当前站点对象 + """ site = Site.objects.get_current() return site class CommonMarkdown: + """ + Markdown处理工具类 + + 提供Markdown文本到HTML的转换功能,支持代码高亮和目录生成。 + """ + @staticmethod def _convert_markdown(value): + """ + 内部Markdown转换方法 + + Args: + value: Markdown格式文本 + + Returns: + tuple: (转换后的HTML内容, 生成的目录) + """ + # 配置Markdown扩展 md = markdown.Markdown( extensions=[ - 'extra', - 'codehilite', - 'toc', - 'tables', + 'extra', # 额外语法支持 + 'codehilite', # 代码高亮 + 'toc', # 目录生成 + 'tables', # 表格支持 ] ) body = md.convert(value) @@ -115,16 +194,44 @@ class CommonMarkdown: @staticmethod def get_markdown_with_toc(value): + """ + 获取带目录的Markdown转换结果 + + Args: + value: Markdown格式文本 + + Returns: + tuple: (HTML内容, 目录HTML) + """ body, toc = CommonMarkdown._convert_markdown(value) return body, toc @staticmethod def get_markdown(value): + """ + 获取Markdown转换结果(不含目录) + + Args: + value: Markdown格式文本 + + Returns: + str: 转换后的HTML内容 + """ body, toc = CommonMarkdown._convert_markdown(value) return body def send_email(emailto, title, content): + """ + 发送电子邮件 + + 通过信号机制异步发送邮件。 + + Args: + emailto: 收件人邮箱地址 + title: 邮件标题 + content: 邮件内容 + """ from djangoblog.blog_signals import send_email_signal send_email_signal.send( send_email.__class__, @@ -139,6 +246,15 @@ def generate_code() -> str: def parse_dict_to_url(dict): + """ + 将字典转换为URL参数字符串 + + Args: + dict: 参数字典 + + Returns: + str: URL参数字符串 + """ from urllib.parse import quote url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) @@ -146,11 +262,21 @@ def parse_dict_to_url(dict): def get_blog_setting(): + """ + 获取博客设置 + + 返回博客的全局设置信息,带缓存功能。 + 如果设置不存在,则创建默认设置。 + + Returns: + BlogSettings: 博客设置对象 + """ value = cache.get('get_blog_setting') if value: return value else: from blog.models import BlogSettings + # 如果不存在设置记录,创建默认设置 if not BlogSettings.objects.count(): setting = BlogSettings() setting.site_name = 'djangoblog' @@ -176,32 +302,48 @@ def get_blog_setting(): def save_user_avatar(url): ''' 保存用户头像 - :param url:头像url - :return: 本地路径 + + 从远程URL下载用户头像并保存到本地静态文件目录。 + + Args: + url: 头像URL地址 + + Returns: + str: 本地静态文件路径 ''' logger.info(url) try: basedir = os.path.join(settings.STATICFILES, 'avatar') + # 下载头像文件 rsp = requests.get(url, timeout=2) if rsp.status_code == 200: if not os.path.exists(basedir): os.makedirs(basedir) + # 检查文件扩展名 image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 ext = os.path.splitext(url)[1] if isimage else '.jpg' + # 生成唯一文件名 save_filename = str(uuid.uuid4().hex) + ext logger.info('保存用户头像:' + basedir + save_filename) + # 保存文件 with open(os.path.join(basedir, save_filename), 'wb+') as file: file.write(rsp.content) return static('avatar/' + save_filename) except Exception as e: logger.error(e) + # 返回默认头像 return static('blog/img/avatar.png') def delete_sidebar_cache(): + """ + 删除侧边栏缓存 + + 清理所有侧边栏相关的缓存数据。 + """ from blog.models import LinkShowType keys = ["sidebar" + x for x in LinkShowType.values] for k in keys: @@ -210,12 +352,27 @@ def delete_sidebar_cache(): def delete_view_cache(prefix, keys): + """ + 删除视图缓存 + + 根据前缀和键删除特定的模板片段缓存。 + + Args: + prefix: 缓存前缀 + keys: 缓存键列表 + """ from django.core.cache.utils import make_template_fragment_key key = make_template_fragment_key(prefix, keys) cache.delete(key) def get_resource_url(): + """ + 获取资源URL基础路径 + + Returns: + str: 静态资源基础URL + """ if settings.STATIC_URL: return settings.STATIC_URL else: @@ -223,6 +380,7 @@ def get_resource_url(): return 'http://' + site.domain + '/static/' +# HTML标签白名单 - 允许的安全HTML标签 ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'p', 'span', 'div'] @@ -235,6 +393,7 @@ ALLOWED_CLASSES = [ 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il' ] + def class_filter(tag, name, value): """自定义class属性过滤器""" if name == 'class': @@ -243,10 +402,11 @@ def class_filter(tag, name, value): return ' '.join(allowed_classes) if allowed_classes else False return value + # 安全的属性白名单 ALLOWED_ATTRIBUTES = { - 'a': ['href', 'title'], - 'abbr': ['title'], + 'a': ['href', 'title'], + 'abbr': ['title'], 'acronym': ['title'], 'span': class_filter, 'div': class_filter, @@ -257,16 +417,24 @@ ALLOWED_ATTRIBUTES = { # 安全的协议白名单 - 防止javascript:等危险协议 ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] + def sanitize_html(html): """ 安全的HTML清理函数 - 使用bleach库进行白名单过滤,防止XSS攻击 + + 使用bleach库进行白名单过滤,防止XSS攻击。 + + Args: + html: 要清理的HTML内容 + + Returns: + str: 清理后的安全HTML """ return bleach.clean( - html, - tags=ALLOWED_TAGS, + html, + tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, protocols=ALLOWED_PROTOCOLS, # 限制允许的协议 strip=True, # 移除不允许的标签而不是转义 strip_comments=True # 移除HTML注释 - ) + ) \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/whoosh_cn_backend.py b/src/DjangoBlog/djangoblog/whoosh_cn_backend.py index 04e3f7f..2e934ce 100644 --- a/src/DjangoBlog/djangoblog/whoosh_cn_backend.py +++ b/src/DjangoBlog/djangoblog/whoosh_cn_backend.py @@ -1,5 +1,19 @@ # encoding: utf-8 +""" +Whoosh中文搜索后端模块 + +本模块提供了基于Whoosh搜索引擎的中文全文搜索功能,专门针对Django Haystack框架进行定制。 +集成了jieba中文分词器,支持中文文本的高效索引和搜索。 + +主要特性: +- 中文分词支持(使用jieba) +- 高性能索引和搜索 +- 拼写建议和查询高亮 +- 多字段类型支持(文本、数字、日期等) +- 与Django Haystack框架深度集成 +""" + from __future__ import absolute_import, division, print_function, unicode_literals import json @@ -40,30 +54,39 @@ except ImportError: raise MissingDependency( "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") -# Handle minimum requirement. +# 检查Whoosh版本要求 if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): raise MissingDependency( "The 'whoosh' backend requires version 2.5.0 or greater.") -# Bubble up the correct error. - +# 日期时间正则表达式 - 用于解析日期格式 DATETIME_REGEX = re.compile( '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') + +# 线程本地存储 - 用于内存索引 LOCALS = threading.local() LOCALS.RAM_STORE = None class WhooshHtmlFormatter(HtmlFormatter): """ - This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. - We use it to have consistent results across backends. Specifically, - Solr, Xapian and Elasticsearch are using this formatting. + 简化的Whoosh HTML格式化器 + + 提供跨后端一致的高亮结果显示格式。 + Solr、Xapian和Elasticsearch都使用这种格式化方式。 """ template = '<%(tag)s>%(t)s' class WhooshSearchBackend(BaseSearchBackend): - # Word reserved by Whoosh for special use. + """ + Whoosh搜索后端实现 + + 继承自Haystack的BaseSearchBackend,提供Whoosh搜索引擎的核心功能。 + 支持文件存储和内存存储两种方式。 + """ + + # Whoosh保留关键字 RESERVED_WORDS = ( 'AND', 'NOT', @@ -71,15 +94,20 @@ class WhooshSearchBackend(BaseSearchBackend): 'TO', ) - # Characters reserved by Whoosh for special use. - # The '\\' must come first, so as not to overwrite the other slash - # replacements. + # Whoosh保留字符 RESERVED_CHARACTERS = ( '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '.', ) def __init__(self, connection_alias, **connection_options): + """ + 初始化Whoosh搜索后端 + + Args: + connection_alias: 连接别名 + **connection_options: 连接配置选项 + """ super( WhooshSearchBackend, self).__init__( @@ -93,9 +121,11 @@ class WhooshSearchBackend(BaseSearchBackend): 128 * 1024 * 1024) self.path = connection_options.get('PATH') + # 检查存储类型 if connection_options.get('STORAGE', 'file') != 'file': self.use_file_storage = False + # 文件存储必须指定路径 if self.use_file_storage and not self.path: raise ImproperlyConfigured( "You must specify a 'PATH' in your settings for connection '%s'." % @@ -105,21 +135,26 @@ class WhooshSearchBackend(BaseSearchBackend): def setup(self): """ - Defers loading until needed. + 初始化设置 + + 延迟加载,在需要时进行初始化。 + 创建或打开索引,构建schema。 """ from haystack import connections new_index = False - # Make sure the index is there. + # 确保索引目录存在 if self.use_file_storage and not os.path.exists(self.path): os.makedirs(self.path) new_index = True + # 检查目录写入权限 if self.use_file_storage and not os.access(self.path, os.W_OK): raise IOError( "The path to your Whoosh index '%s' is not writable for the current user/group." % self.path) + # 初始化存储 if self.use_file_storage: self.storage = FileStorage(self.path) else: @@ -130,10 +165,12 @@ class WhooshSearchBackend(BaseSearchBackend): self.storage = LOCALS.RAM_STORE + # 构建schema和解析器 self.content_field_name, self.schema = self.build_schema( connections[self.connection_alias].get_unified_index().all_searchfields()) self.parser = QueryParser(self.content_field_name, schema=self.schema) + # 创建或打开索引 if new_index is True: self.index = self.storage.create_index(self.schema) else: @@ -145,18 +182,30 @@ class WhooshSearchBackend(BaseSearchBackend): self.setup_complete = True def build_schema(self, fields): + """ + 构建Whoosh schema + + 根据字段定义创建Whoosh索引schema。 + + Args: + fields: 字段定义字典 + + Returns: + tuple: (内容字段名, schema对象) + """ + # 基础字段 schema_fields = { ID: WHOOSH_ID(stored=True, unique=True), DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True), } - # Grab the number of keys that are hard-coded into Haystack. - # We'll use this to (possibly) fail slightly more gracefully later. initial_key_count = len(schema_fields) content_field_name = '' + # 处理每个字段 for field_name, field_class in fields.items(): if field_class.is_multivalued: + # 多值字段 if field_class.indexed is False: schema_fields[field_class.index_fieldname] = IDLIST( stored=True, field_boost=field_class.boost) @@ -164,35 +213,42 @@ class WhooshSearchBackend(BaseSearchBackend): schema_fields[field_class.index_fieldname] = KEYWORD( stored=True, commas=True, scorable=True, field_boost=field_class.boost) elif field_class.field_type in ['date', 'datetime']: + # 日期时间字段 schema_fields[field_class.index_fieldname] = DATETIME( stored=field_class.stored, sortable=True) elif field_class.field_type == 'integer': + # 整数字段 schema_fields[field_class.index_fieldname] = NUMERIC( stored=field_class.stored, numtype=int, field_boost=field_class.boost) elif field_class.field_type == 'float': + # 浮点数字段 schema_fields[field_class.index_fieldname] = NUMERIC( stored=field_class.stored, numtype=float, field_boost=field_class.boost) elif field_class.field_type == 'boolean': - # Field boost isn't supported on BOOLEAN as of 1.8.2. + # 布尔字段 schema_fields[field_class.index_fieldname] = BOOLEAN( stored=field_class.stored) elif field_class.field_type == 'ngram': + # N-gram字段 schema_fields[field_class.index_fieldname] = NGRAM( minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) elif field_class.field_type == 'edge_ngram': + # 边缘N-gram字段 schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost) else: + # 文本字段 - 使用中文分析器 # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) schema_fields[field_class.index_fieldname] = TEXT( stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) + + # 标记内容字段 if field_class.document is True: content_field_name = field_class.index_fieldname schema_fields[field_class.index_fieldname].spelling = True - # Fail more gracefully than relying on the backend to die if no fields - # are found. + # 检查是否有有效字段 if len(schema_fields) <= initial_key_count: raise SearchBackendError( "No fields were found in any search_indexes. Please correct this before attempting to search.") @@ -200,6 +256,14 @@ class WhooshSearchBackend(BaseSearchBackend): return (content_field_name, Schema(**schema_fields)) def update(self, index, iterable, commit=True): + """ + 更新索引 + + Args: + index: 搜索索引 + iterable: 可迭代对象 + commit: 是否提交更改 + """ if not self.setup_complete: self.setup() @@ -212,12 +276,11 @@ class WhooshSearchBackend(BaseSearchBackend): except SkipDocument: self.log.debug(u"Indexing for object `%s` skipped", obj) else: - # Really make sure it's unicode, because Whoosh won't have it any - # other way. + # 确保所有值为unicode for key in doc: doc[key] = self._from_python(doc[key]) - # Document boosts aren't supported in Whoosh 2.5.0+. + # Whoosh 2.5.0+不支持文档boost if 'boost' in doc: del doc['boost'] @@ -227,9 +290,6 @@ class WhooshSearchBackend(BaseSearchBackend): if not self.silently_fail: raise - # We'll log the object identifier but won't include the actual object - # to avoid the possibility of that generating encoding errors while - # processing the log message: self.log.error( u"%s while preparing object for update" % e.__class__.__name__, @@ -239,12 +299,18 @@ class WhooshSearchBackend(BaseSearchBackend): "index": index, "object": get_identifier(obj)}}) + # 提交更改 if len(iterable) > 0: - # For now, commit no matter what, as we run into locking issues - # otherwise. writer.commit() def remove(self, obj_or_string, commit=True): + """ + 移除文档 + + Args: + obj_or_string: 对象或标识符 + commit: 是否提交更改 + """ if not self.setup_complete: self.setup() @@ -267,6 +333,13 @@ class WhooshSearchBackend(BaseSearchBackend): exc_info=True) def clear(self, models=None, commit=True): + """ + 清空索引 + + Args: + models: 要清空的模型列表 + commit: 是否提交更改 + """ if not self.setup_complete: self.setup() @@ -304,17 +377,27 @@ class WhooshSearchBackend(BaseSearchBackend): "Failed to clear Whoosh index: %s", e, exc_info=True) def delete_index(self): - # Per the Whoosh mailing list, if wiping out everything from the index, - # it's much more efficient to simply delete the index files. + """ + 删除索引 + + 彻底删除索引文件并重新创建。 + """ + # 文件存储:直接删除目录 if self.use_file_storage and os.path.exists(self.path): shutil.rmtree(self.path) elif not self.use_file_storage: + # 内存存储:清理存储 self.storage.clean() - # Recreate everything. + # 重新创建 self.setup() def optimize(self): + """ + 优化索引 + + 提高搜索性能。 + """ if not self.setup_complete: self.setup() @@ -322,12 +405,21 @@ class WhooshSearchBackend(BaseSearchBackend): self.index.optimize() def calculate_page(self, start_offset=0, end_offset=None): - # Prevent against Whoosh throwing an error. Requires an end_offset - # greater than 0. + """ + 计算分页参数 + + Args: + start_offset: 起始偏移量 + end_offset: 结束偏移量 + + Returns: + tuple: (页码, 页大小) + """ + # 防止Whoosh错误 if end_offset is not None and end_offset <= 0: end_offset = 1 - # Determine the page. + # 确定页码 page_num = 0 if end_offset is None: @@ -341,7 +433,7 @@ class WhooshSearchBackend(BaseSearchBackend): if page_length and page_length > 0: page_num = int(start_offset / page_length) - # Increment because Whoosh uses 1-based page numbers. + # Whoosh使用1-based页码 page_num += 1 return page_num, page_length @@ -366,10 +458,15 @@ class WhooshSearchBackend(BaseSearchBackend): limit_to_registered_models=None, result_class=None, **kwargs): + """ + 执行搜索查询 + + 核心搜索方法,处理各种搜索参数和选项。 + """ if not self.setup_complete: self.setup() - # A zero length query should return no results. + # 空查询返回无结果 if len(query_string) == 0: return { 'results': [], @@ -378,8 +475,7 @@ class WhooshSearchBackend(BaseSearchBackend): query_string = force_str(query_string) - # A one-character query (non-wildcard) gets nabbed by a stopwords - # filter and should yield zero results. + # 单字符查询(非通配符)返回无结果 if len(query_string) <= 1 and query_string != u'*': return { 'results': [], @@ -388,10 +484,8 @@ class WhooshSearchBackend(BaseSearchBackend): reverse = False + # 处理排序 if sort_by is not None: - # Determine if we need to reverse the results and if Whoosh can - # handle what it's being asked to sort by. Reversing is an - # all-or-nothing action, unfortunately. sort_by_list = [] reverse_counter = 0 @@ -399,6 +493,7 @@ class WhooshSearchBackend(BaseSearchBackend): if order_by.startswith('-'): reverse_counter += 1 + # Whoosh要求所有排序字段方向一致 if reverse_counter and reverse_counter != len(sort_by): raise SearchBackendError("Whoosh requires all order_by fields" " to use the same sort direction") @@ -406,17 +501,16 @@ class WhooshSearchBackend(BaseSearchBackend): for order_by in sort_by: if order_by.startswith('-'): sort_by_list.append(order_by[1:]) - if len(sort_by_list) == 1: reverse = True else: sort_by_list.append(order_by) - if len(sort_by_list) == 1: reverse = False sort_by = sort_by_list[0] + # Whoosh不支持facet功能 if facets is not None: warnings.warn( "Whoosh does not handle faceting.", @@ -438,6 +532,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrowed_results = None self.index = self.index.refresh() + # 模型限制处理 if limit_to_registered_models is None: limit_to_registered_models = getattr( settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) @@ -445,12 +540,11 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: - # Using narrow queries, limit the results to only models handled - # with the current routers. model_choices = self.build_models_list() else: model_choices = [] + # 构建窄查询 if len(model_choices) > 0: if narrow_queries is None: narrow_queries = set() @@ -460,9 +554,8 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None + # 处理窄查询 if narrow_queries is not None: - # Potentially expensive? I don't see another way to do it in - # Whoosh... narrow_searcher = self.index.searcher() for nq in narrow_queries: @@ -482,11 +575,12 @@ class WhooshSearchBackend(BaseSearchBackend): self.index = self.index.refresh() + # 执行搜索 if self.index.doc_count(): searcher = self.index.searcher() parsed_query = self.parser.parse(query_string) - # In the event of an invalid/stopworded query, recover gracefully. + # 处理无效查询 if parsed_query is None: return { 'results': [], @@ -502,7 +596,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'reverse': reverse, } - # Handle the case where the results have been narrowed. + # 应用窄查询过滤 if narrowed_results is not None: search_kwargs['filter'] = narrowed_results @@ -522,8 +616,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'spelling_suggestion': None, } - # Because as of Whoosh 2.5.1, it will return the wrong page of - # results if you request something too high. :( + # 检查页码有效性 if raw_page.pagenum < page_num: return { 'results': [], @@ -531,6 +624,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'spelling_suggestion': None, } + # 处理搜索结果 results = self._process_results( raw_page, highlight=highlight, @@ -544,6 +638,7 @@ class WhooshSearchBackend(BaseSearchBackend): return results else: + # 无文档时的处理 if self.include_spelling: if spelling_query: spelling_suggestion = self.create_spelling_suggestion( @@ -570,18 +665,21 @@ class WhooshSearchBackend(BaseSearchBackend): limit_to_registered_models=None, result_class=None, **kwargs): + """ + 查找相似文档 + + 基于给定模型实例查找相似内容。 + """ if not self.setup_complete: self.setup() - # Deferred models will have a different class ("RealClass_Deferred_fieldname") - # which won't be in our registry: model_klass = model_instance._meta.concrete_model - field_name = self.content_field_name narrow_queries = set() narrowed_results = None self.index = self.index.refresh() + # 模型限制处理 if limit_to_registered_models is None: limit_to_registered_models = getattr( settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) @@ -589,12 +687,11 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: - # Using narrow queries, limit the results to only models handled - # with the current routers. model_choices = self.build_models_list() else: model_choices = [] + # 构建查询 if len(model_choices) > 0: if narrow_queries is None: narrow_queries = set() @@ -607,9 +704,8 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None + # 处理窄查询 if narrow_queries is not None: - # Potentially expensive? I don't see another way to do it in - # Whoosh... narrow_searcher = self.index.searcher() for nq in narrow_queries: @@ -632,6 +728,7 @@ class WhooshSearchBackend(BaseSearchBackend): self.index = self.index.refresh() raw_results = EmptyResults() + # 执行相似文档搜索 if self.index.doc_count(): query = "%s:%s" % (ID, get_identifier(model_instance)) searcher = self.index.searcher() @@ -642,7 +739,7 @@ class WhooshSearchBackend(BaseSearchBackend): raw_results = results[0].more_like_this( field_name, top=end_offset) - # Handle the case where the results have been narrowed. + # 应用窄查询过滤 if narrowed_results is not None and hasattr(raw_results, 'filter'): raw_results.filter(narrowed_results) @@ -658,8 +755,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'spelling_suggestion': None, } - # Because as of Whoosh 2.5.1, it will return the wrong page of - # results if you request something too high. :( + # 检查页码有效性 if raw_page.pagenum < page_num: return { 'results': [], @@ -667,6 +763,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'spelling_suggestion': None, } + # 处理结果 results = self._process_results(raw_page, result_class=result_class) searcher.close() @@ -682,11 +779,15 @@ class WhooshSearchBackend(BaseSearchBackend): query_string='', spelling_query=None, result_class=None): + """ + 处理搜索结果 + + 将Whoosh原始结果转换为Haystack格式。 + """ from haystack import connections results = [] - # It's important to grab the hits first before slicing. Otherwise, this - # can cause pagination failures. + # 获取命中数 hits = len(raw_page) if result_class is None: @@ -697,6 +798,7 @@ class WhooshSearchBackend(BaseSearchBackend): unified_index = connections[self.connection_alias].get_unified_index() indexed_models = unified_index.get_indexed_models() + # 处理每个结果 for doc_offset, raw_result in enumerate(raw_page): score = raw_page.score(doc_offset) or 0 app_label, model_name = raw_result[DJANGO_CT].split('.') @@ -704,13 +806,14 @@ class WhooshSearchBackend(BaseSearchBackend): model = haystack_get_model(app_label, model_name) if model and model in indexed_models: + # 处理字段值 for key, value in raw_result.items(): index = unified_index.get_index(model) string_key = str(key) if string_key in index.fields and hasattr( index.fields[string_key], 'convert'): - # Special-cased due to the nature of KEYWORD fields. + # 多值字段特殊处理 if index.fields[string_key].is_multivalued: if value is None or len(value) == 0: additional_fields[string_key] = [] @@ -723,9 +826,11 @@ class WhooshSearchBackend(BaseSearchBackend): else: additional_fields[string_key] = self._to_python(value) + # 移除系统字段 del (additional_fields[DJANGO_CT]) del (additional_fields[DJANGO_ID]) + # 高亮处理 if highlight: sa = StemmingAnalyzer() formatter = WhooshHtmlFormatter('em') @@ -742,6 +847,7 @@ class WhooshSearchBackend(BaseSearchBackend): self.content_field_name: [whoosh_result], } + # 创建结果对象 result = result_class( app_label, model_name, @@ -752,6 +858,7 @@ class WhooshSearchBackend(BaseSearchBackend): else: hits -= 1 + # 拼写建议 if self.include_spelling: if spelling_query: spelling_suggestion = self.create_spelling_suggestion( @@ -768,6 +875,15 @@ class WhooshSearchBackend(BaseSearchBackend): } def create_spelling_suggestion(self, query_string): + """ + 创建拼写建议 + + Args: + query_string: 查询字符串 + + Returns: + str: 拼写建议 + """ spelling_suggestion = None reader = self.index.reader() corrector = reader.corrector(self.content_field_name) @@ -776,14 +892,14 @@ class WhooshSearchBackend(BaseSearchBackend): if not query_string: return spelling_suggestion - # Clean the string. + # 清理查询字符串 for rev_word in self.RESERVED_WORDS: cleaned_query = cleaned_query.replace(rev_word, '') for rev_char in self.RESERVED_CHARACTERS: cleaned_query = cleaned_query.replace(rev_char, '') - # Break it down. + # 分词并获取建议 query_words = cleaned_query.split() suggested_words = [] @@ -798,22 +914,29 @@ class WhooshSearchBackend(BaseSearchBackend): def _from_python(self, value): """ - Converts Python values to a string for Whoosh. + Python值转换为Whoosh字符串 - Code courtesy of pysolr. + Args: + value: Python值 + + Returns: + str: Whoosh格式字符串 """ if hasattr(value, 'strftime'): + # 日期时间处理 if not hasattr(value, 'hour'): value = datetime(value.year, value.month, value.day, 0, 0, 0) elif isinstance(value, bool): + # 布尔值处理 if value: value = 'true' else: value = 'false' elif isinstance(value, (list, tuple)): + # 列表元组处理 value = u','.join([force_str(v) for v in value]) elif isinstance(value, (six.integer_types, float)): - # Leave it alone. + # 数字类型保持原样 pass else: value = force_str(value) @@ -821,15 +944,20 @@ class WhooshSearchBackend(BaseSearchBackend): def _to_python(self, value): """ - Converts values from Whoosh to native Python values. + Whoosh值转换为Python值 + + Args: + value: Whoosh值 - A port of the same method in pysolr, as they deal with data the same way. + Returns: + object: Python值 """ if value == 'true': return True elif value == 'false': return False + # 日期时间解析 if value and isinstance(value, six.string_types): possible_datetime = DATETIME_REGEX.search(value) @@ -847,11 +975,10 @@ class WhooshSearchBackend(BaseSearchBackend): date_values['minute'], date_values['second']) + # JSON解析尝试 try: - # Attempt to use json to load the values. converted_value = json.loads(value) - # Try to handle most built-in types. if isinstance( converted_value, (list, @@ -863,15 +990,28 @@ class WhooshSearchBackend(BaseSearchBackend): complex)): return converted_value except BaseException: - # If it fails (SyntaxError or its ilk) or we don't trust it, - # continue on. pass return value class WhooshSearchQuery(BaseSearchQuery): + """ + Whoosh搜索查询构建器 + + 负责构建Whoosh搜索引擎的查询语句。 + """ + def _convert_datetime(self, date): + """ + 日期时间转换 + + Args: + date: 日期时间对象 + + Returns: + str: 格式化字符串 + """ if hasattr(date, 'hour'): return force_str(date.strftime('%Y%m%d%H%M%S')) else: @@ -879,20 +1019,25 @@ class WhooshSearchQuery(BaseSearchQuery): def clean(self, query_fragment): """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. + 清理查询片段 + + 对用户输入进行清理和转义处理。 - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. + Args: + query_fragment: 查询片段 + + Returns: + str: 清理后的查询字符串 """ words = query_fragment.split() cleaned_words = [] for word in words: + # 保留字转为小写 if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) + # 保留字符用引号包围 for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word @@ -903,12 +1048,23 @@ class WhooshSearchQuery(BaseSearchQuery): return ' '.join(cleaned_words) def build_query_fragment(self, field, filter_type, value): + """ + 构建查询片段 + + Args: + field: 字段名 + filter_type: 过滤器类型 + value: 字段值 + + Returns: + str: 查询片段 + """ from haystack import connections query_frag = '' is_datetime = False + # 值类型处理 if not hasattr(value, 'input_type_name'): - # Handle when we've got a ``ValuesListQuerySet``... if hasattr(value, 'values_list'): value = list(value) @@ -916,26 +1072,24 @@ class WhooshSearchQuery(BaseSearchQuery): is_datetime = True if isinstance(value, six.string_types) and value != ' ': - # It's not an ``InputType``. Assume ``Clean``. value = Clean(value) else: value = PythonData(value) - # Prepare the query using the InputType. + # 准备值 prepared_value = value.prepare(self) if not isinstance(prepared_value, (set, list, tuple)): - # Then convert whatever we get back to what pysolr wants if needed. prepared_value = self.backend._from_python(prepared_value) - # 'content' is a special reserved word, much like 'pk' in - # Django's ORM layer. It indicates 'no special field'. + # 字段名处理 if field == 'content': index_fieldname = '' else: index_fieldname = u'%s:' % connections[self._using].get_unified_index( ).get_index_fieldname(field) + # 过滤器类型映射 filter_types = { 'content': '%s', 'contains': '*%s*', @@ -949,6 +1103,7 @@ class WhooshSearchQuery(BaseSearchQuery): 'fuzzy': u'%s~', } + # 查询片段构建 if value.post_process is False: query_frag = prepared_value else: @@ -961,8 +1116,6 @@ class WhooshSearchQuery(BaseSearchQuery): if value.input_type_name == 'exact': query_frag = prepared_value else: - # Iterate over terms & incorportate the converted form of - # each into the query. terms = [] if isinstance(prepared_value, six.string_types): @@ -1026,19 +1179,19 @@ class WhooshSearchQuery(BaseSearchQuery): query_frag = filter_types[filter_type] % prepared_value + # 添加括号 if len(query_frag) and not isinstance(value, Raw): if not query_frag.startswith('(') and not query_frag.endswith(')'): query_frag = "(%s)" % query_frag return u"%s%s" % (index_fieldname, query_frag) - # if not filter_type in ('in', 'range'): - # # 'in' is a bit of a special case, as we don't want to - # # convert a valid list/tuple to string. Defer handling it - # # until later... - # value = self.backend._from_python(value) - class WhooshEngine(BaseEngine): + """ + Whoosh搜索引擎配置 + + 配置Haystack使用Whoosh作为搜索后端。 + """ backend = WhooshSearchBackend - query = WhooshSearchQuery + query = WhooshSearchQuery \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/wsgi.py b/src/DjangoBlog/djangoblog/wsgi.py index 2295efd..b57b6b4 100644 --- a/src/DjangoBlog/djangoblog/wsgi.py +++ b/src/DjangoBlog/djangoblog/wsgi.py @@ -1,16 +1,25 @@ """ -WSGI config for djangoblog project. +DjangoBlog WSGI 配置模块 -It exposes the WSGI callable as a module-level variable named ``application``. +本模块定义了DjangoBlog项目的WSGI(Web Server Gateway Interface)配置, +用于将Django应用部署到支持WSGI的Web服务器(如Apache、Nginx + uWSGI等)。 -For more information on this file, see -https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +WSGI是Python Web应用与Web服务器之间的标准接口,确保应用能够在生产环境中正确运行。 + +主要功能: +- 设置Django环境变量 +- 创建WSGI应用实例 +- 提供Web服务器与Django应用之间的桥梁 """ import os from django.core.wsgi import get_wsgi_application +# 设置Django设置模块的环境变量 +# 告诉Django使用哪个配置文件,这里设置为'djangoblog.settings' os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") -application = get_wsgi_application() +# 创建WSGI应用实例 +# 这是Web服务器将调用的入口点,用于处理HTTP请求 +application = get_wsgi_application() \ No newline at end of file diff --git a/src/DjangoBlog/models.json b/src/DjangoBlog/models.json new file mode 100644 index 0000000..820d49b --- /dev/null +++ b/src/DjangoBlog/models.json @@ -0,0 +1 @@ +{"created_at": "2025-10-15 18:20", "cli_options": "-a --json -o models.json", "disable_fields": false, "disable_abstract_fields": false, "display_field_choices": false, "use_subgraph": false, "rankdir": "TB", "ordering": null, "graphs": [{"True": true, "False": false, "None": null, "name": "\"django.contrib.admin\"", "app_name": "django.contrib.admin", "cluster_app_name": "cluster_django_contrib_admin", "models": [{"app_name": "django_contrib_admin_models", "name": "LogEntry", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "content_type", "label": "content_type", "type": "ForeignKey (id)", "blank": true, "abstract": false, "relation": true, "primary_key": false}, {"name": "user", "label": "user", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "action_flag", "label": "action_flag", "type": "PositiveSmallIntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "action_time", "label": "action_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "change_message", "label": "change_message", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "object_id", "label": "object_id", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "object_repr", "label": "object_repr", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "accounts_models", "target": "BlogUser", "type": "ForeignKey", "name": "user", "label": "user (logentry)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "django_contrib_contenttypes_models", "target": "ContentType", "type": "ForeignKey", "name": "content_type", "label": "content_type (logentry)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}], "label": "LogEntry"}]}, {"True": true, "False": false, "None": null, "name": "\"django.contrib.auth\"", "app_name": "django.contrib.auth", "cluster_app_name": "cluster_django_contrib_auth", "models": [{"app_name": "django_contrib_auth_models", "name": "Permission", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "content_type", "label": "content_type", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "codename", "label": "codename", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "django_contrib_contenttypes_models", "target": "ContentType", "type": "ForeignKey", "name": "content_type", "label": "content_type (permission)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}], "label": "Permission"}, {"app_name": "django_contrib_auth_models", "name": "Group", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "django_contrib_auth_models", "target": "Permission", "type": "ManyToManyField", "name": "permissions", "label": "permissions (group)", "arrows": "[arrowhead=dot arrowtail=dot, dir=both]", "needs_node": false}], "label": "Group"}]}, {"True": true, "False": false, "None": null, "name": "\"django.contrib.contenttypes\"", "app_name": "django.contrib.contenttypes", "cluster_app_name": "cluster_django_contrib_contenttypes", "models": [{"app_name": "django_contrib_contenttypes_models", "name": "ContentType", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "app_label", "label": "app_label", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "model", "label": "model", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "ContentType"}]}, {"True": true, "False": false, "None": null, "name": "\"django.contrib.sessions\"", "app_name": "django.contrib.sessions", "cluster_app_name": "cluster_django_contrib_sessions", "models": [{"app_name": "django_contrib_sessions_base_session", "name": "AbstractBaseSession", "abstracts": [], "fields": [{"name": "expire_date", "label": "expire_date", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "session_data", "label": "session_data", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "AbstractBaseSession"}, {"app_name": "django_contrib_sessions_models", "name": "Session", "abstracts": ["AbstractBaseSession"], "fields": [{"name": "session_key", "label": "session_key", "type": "CharField", "blank": false, "abstract": true, "relation": false, "primary_key": true}, {"name": "expire_date", "label": "expire_date", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "session_data", "label": "session_data", "type": "TextField", "blank": false, "abstract": true, "relation": false, "primary_key": false}], "relations": [{"target_app": "django_contrib_sessions_base_session", "target": "AbstractBaseSession", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": false}], "label": "Session"}]}, {"True": true, "False": false, "None": null, "name": "\"django.contrib.sites\"", "app_name": "django.contrib.sites", "cluster_app_name": "cluster_django_contrib_sites", "models": [{"app_name": "django_contrib_sites_models", "name": "Site", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "domain", "label": "domain", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "Site"}]}, {"True": true, "False": false, "None": null, "name": "\"blog\"", "app_name": "blog", "cluster_app_name": "cluster_blog", "models": [{"app_name": "blog_models", "name": "BaseModel", "abstracts": [], "fields": [{"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "BaseModel"}, {"app_name": "blog_models", "name": "Article", "abstracts": ["BaseModel"], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": true, "relation": false, "primary_key": true}, {"name": "author", "label": "author", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "category", "label": "category", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "article_order", "label": "article_order", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "body", "label": "body", "type": "MDTextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "comment_status", "label": "comment_status", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "pub_time", "label": "pub_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "show_toc", "label": "show_toc", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "status", "label": "status", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "title", "label": "title", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "type", "label": "type", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "views", "label": "views", "type": "PositiveIntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "accounts_models", "target": "BlogUser", "type": "ForeignKey", "name": "author", "label": "author (article)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "blog_models", "target": "Category", "type": "ForeignKey", "name": "category", "label": "category (article)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "blog_models", "target": "Tag", "type": "ManyToManyField", "name": "tags", "label": "tags (article)", "arrows": "[arrowhead=dot arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "blog_models", "target": "BaseModel", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": false}], "label": "Article"}, {"app_name": "blog_models", "name": "Category", "abstracts": ["BaseModel"], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": true, "relation": false, "primary_key": true}, {"name": "parent_category", "label": "parent_category", "type": "ForeignKey (id)", "blank": true, "abstract": false, "relation": true, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "index", "label": "index", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "slug", "label": "slug", "type": "SlugField", "blank": true, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "blog_models", "target": "Category", "type": "ForeignKey", "name": "parent_category", "label": "parent_category (category)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "blog_models", "target": "BaseModel", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": false}], "label": "Category"}, {"app_name": "blog_models", "name": "Tag", "abstracts": ["BaseModel"], "fields": [{"name": "id", "label": "id", "type": "AutoField", "blank": true, "abstract": true, "relation": false, "primary_key": true}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "slug", "label": "slug", "type": "SlugField", "blank": true, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "blog_models", "target": "BaseModel", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": false}], "label": "Tag"}, {"app_name": "blog_models", "name": "Links", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_enable", "label": "is_enable", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_mod_time", "label": "last_mod_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "link", "label": "link", "type": "URLField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "sequence", "label": "sequence", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "show_type", "label": "show_type", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "Links"}, {"app_name": "blog_models", "name": "SideBar", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "content", "label": "content", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_enable", "label": "is_enable", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_mod_time", "label": "last_mod_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "name", "label": "name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "sequence", "label": "sequence", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "SideBar"}, {"app_name": "blog_models", "name": "BlogSettings", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "analytics_code", "label": "analytics_code", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "article_comment_count", "label": "article_comment_count", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "article_sub_length", "label": "article_sub_length", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "beian_code", "label": "beian_code", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "comment_need_review", "label": "comment_need_review", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "global_footer", "label": "global_footer", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "global_header", "label": "global_header", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "gongan_beiancode", "label": "gongan_beiancode", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "google_adsense_codes", "label": "google_adsense_codes", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "open_site_comment", "label": "open_site_comment", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "show_gongan_code", "label": "show_gongan_code", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "show_google_adsense", "label": "show_google_adsense", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "sidebar_article_count", "label": "sidebar_article_count", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "sidebar_comment_count", "label": "sidebar_comment_count", "type": "IntegerField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "site_description", "label": "site_description", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "site_keywords", "label": "site_keywords", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "site_name", "label": "site_name", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "site_seo_description", "label": "site_seo_description", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "BlogSettings"}]}, {"True": true, "False": false, "None": null, "name": "\"accounts\"", "app_name": "accounts", "cluster_app_name": "cluster_accounts", "models": [{"app_name": "django_contrib_auth_models", "name": "AbstractUser", "abstracts": ["AbstractBaseUser", "PermissionsMixin"], "fields": [{"name": "date_joined", "label": "date_joined", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "email", "label": "email", "type": "EmailField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "first_name", "label": "first_name", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_active", "label": "is_active", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_staff", "label": "is_staff", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_superuser", "label": "is_superuser", "type": "BooleanField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_login", "label": "last_login", "type": "DateTimeField", "blank": true, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_name", "label": "last_name", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "password", "label": "password", "type": "CharField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "username", "label": "username", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "django_contrib_auth_base_user", "target": "AbstractBaseUser", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": true}, {"target_app": "django_contrib_auth_models", "target": "PermissionsMixin", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": true}], "label": "AbstractUser"}, {"app_name": "accounts_models", "name": "BlogUser", "abstracts": ["AbstractUser"], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "date_joined", "label": "date_joined", "type": "DateTimeField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "email", "label": "email", "type": "EmailField", "blank": true, "abstract": true, "relation": false, "primary_key": false}, {"name": "first_name", "label": "first_name", "type": "CharField", "blank": true, "abstract": true, "relation": false, "primary_key": false}, {"name": "is_active", "label": "is_active", "type": "BooleanField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "is_staff", "label": "is_staff", "type": "BooleanField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "is_superuser", "label": "is_superuser", "type": "BooleanField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_login", "label": "last_login", "type": "DateTimeField", "blank": true, "abstract": true, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_name", "label": "last_name", "type": "CharField", "blank": true, "abstract": true, "relation": false, "primary_key": false}, {"name": "nickname", "label": "nickname", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "password", "label": "password", "type": "CharField", "blank": false, "abstract": true, "relation": false, "primary_key": false}, {"name": "source", "label": "source", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "username", "label": "username", "type": "CharField", "blank": false, "abstract": true, "relation": false, "primary_key": false}], "relations": [{"target_app": "django_contrib_auth_models", "target": "Group", "type": "ManyToManyField", "name": "groups", "label": "groups (user)", "arrows": "[arrowhead=dot arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "django_contrib_auth_models", "target": "Permission", "type": "ManyToManyField", "name": "user_permissions", "label": "user_permissions (user)", "arrows": "[arrowhead=dot arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "django_contrib_auth_models", "target": "AbstractUser", "type": "inheritance", "name": "inheritance", "label": "abstract\\ninheritance", "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", "needs_node": false}], "label": "BlogUser"}]}, {"True": true, "False": false, "None": null, "name": "\"comments\"", "app_name": "comments", "cluster_app_name": "cluster_comments", "models": [{"app_name": "comments_models", "name": "Comment", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "article", "label": "article", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "author", "label": "author", "type": "ForeignKey (id)", "blank": false, "abstract": false, "relation": true, "primary_key": false}, {"name": "parent_comment", "label": "parent_comment", "type": "ForeignKey (id)", "blank": true, "abstract": false, "relation": true, "primary_key": false}, {"name": "body", "label": "body", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_enable", "label": "is_enable", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "accounts_models", "target": "BlogUser", "type": "ForeignKey", "name": "author", "label": "author (comment)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "blog_models", "target": "Article", "type": "ForeignKey", "name": "article", "label": "article (comment)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}, {"target_app": "comments_models", "target": "Comment", "type": "ForeignKey", "name": "parent_comment", "label": "parent_comment (comment)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}], "label": "Comment"}]}, {"True": true, "False": false, "None": null, "name": "\"oauth\"", "app_name": "oauth", "cluster_app_name": "cluster_oauth", "models": [{"app_name": "oauth_models", "name": "OAuthUser", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "author", "label": "author", "type": "ForeignKey (id)", "blank": true, "abstract": false, "relation": true, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "email", "label": "email", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "metadata", "label": "metadata", "type": "TextField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "nickname", "label": "nickname", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "openid", "label": "openid", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "picture", "label": "picture", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "token", "label": "token", "type": "CharField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "type", "label": "type", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [{"target_app": "accounts_models", "target": "BlogUser", "type": "ForeignKey", "name": "author", "label": "author (oauthuser)", "arrows": "[arrowhead=none, arrowtail=dot, dir=both]", "needs_node": false}], "label": "OAuthUser"}, {"app_name": "oauth_models", "name": "OAuthConfig", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "appkey", "label": "appkey", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "appsecret", "label": "appsecret", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "callback_url", "label": "callback_url", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "is_enable", "label": "is_enable", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "type", "label": "type", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "OAuthConfig"}]}, {"True": true, "False": false, "None": null, "name": "\"servermanager\"", "app_name": "servermanager", "cluster_app_name": "cluster_servermanager", "models": [{"app_name": "servermanager_models", "name": "commands", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "command", "label": "command", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "describe", "label": "describe", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "last_modify_time", "label": "last_modify_time", "type": "DateTimeField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "title", "label": "title", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "commands"}, {"app_name": "servermanager_models", "name": "EmailSendLog", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "content", "label": "content", "type": "TextField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": true, "abstract": false, "relation": false, "primary_key": false}, {"name": "emailto", "label": "emailto", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "send_result", "label": "send_result", "type": "BooleanField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "title", "label": "title", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "EmailSendLog"}]}, {"True": true, "False": false, "None": null, "name": "\"owntracks\"", "app_name": "owntracks", "cluster_app_name": "cluster_owntracks", "models": [{"app_name": "owntracks_models", "name": "OwnTrackLog", "abstracts": [], "fields": [{"name": "id", "label": "id", "type": "BigAutoField", "blank": true, "abstract": false, "relation": false, "primary_key": true}, {"name": "creation_time", "label": "creation_time", "type": "DateTimeField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "lat", "label": "lat", "type": "FloatField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "lon", "label": "lon", "type": "FloatField", "blank": false, "abstract": false, "relation": false, "primary_key": false}, {"name": "tid", "label": "tid", "type": "CharField", "blank": false, "abstract": false, "relation": false, "primary_key": false}], "relations": [], "label": "OwnTrackLog"}]}]} \ No newline at end of file diff --git a/src/DjangoBlog/models_analysis.json b/src/DjangoBlog/models_analysis.json new file mode 100644 index 0000000..f59530e --- /dev/null +++ b/src/DjangoBlog/models_analysis.json @@ -0,0 +1,2051 @@ +{ + "admin": [ + { + "name": "LogEntry", + "db_table": "django_admin_log", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "action_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "user", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "BlogUser", + "relationship": "ForeignKey" + }, + { + "name": "content_type", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": "ContentType", + "relationship": "ForeignKey" + }, + { + "name": "object_id", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "object_repr", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "action_flag", + "type": "PositiveSmallIntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "change_message", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "BlogUser", + "field": "user" + }, + { + "type": "ForeignKey", + "target": "ContentType", + "field": "content_type" + } + ], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "auth": [ + { + "name": "Permission", + "db_table": "auth_permission", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 255, + "related_model": null, + "relationship": null + }, + { + "name": "content_type", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "ContentType", + "relationship": "ForeignKey" + }, + { + "name": "codename", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "ContentType", + "field": "content_type" + } + ], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "Group", + "db_table": "auth_group", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 150, + "related_model": null, + "relationship": null + }, + { + "name": "permissions", + "type": "ManyToManyField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "related_model": "Permission" + } + ], + "relationships": [ + { + "type": "ManyToManyField", + "target": "Permission", + "field": "permissions" + } + ], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "contenttypes": [ + { + "name": "ContentType", + "db_table": "django_content_type", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "app_label", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "model", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "sessions": [ + { + "name": "Session", + "db_table": "django_session", + "fields": [ + { + "name": "session_key", + "type": "CharField", + "primary_key": true, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 40, + "related_model": null, + "relationship": null + }, + { + "name": "session_data", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "expire_date", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "sites": [ + { + "name": "Site", + "db_table": "django_site", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "domain", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 50, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "blog": [ + { + "name": "Article", + "db_table": "blog_article", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "title", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "body", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "pub_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "status", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "p", + "max_length": 1, + "related_model": null, + "relationship": null + }, + { + "name": "comment_status", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "o", + "max_length": 1, + "related_model": null, + "relationship": null + }, + { + "name": "type", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "a", + "max_length": 1, + "related_model": null, + "relationship": null + }, + { + "name": "views", + "type": "PositiveIntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "0", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "author", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "BlogUser", + "relationship": "ForeignKey" + }, + { + "name": "article_order", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "0", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "show_toc", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "category", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "Category", + "relationship": "ForeignKey" + }, + { + "name": "tags", + "type": "ManyToManyField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "related_model": "Tag" + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "BlogUser", + "field": "author" + }, + { + "type": "ForeignKey", + "target": "Category", + "field": "category" + }, + { + "type": "ManyToManyField", + "target": "Tag", + "field": "tags" + } + ], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "Category", + "db_table": "blog_category", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 30, + "related_model": null, + "relationship": null + }, + { + "name": "parent_category", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": "Category", + "relationship": "ForeignKey" + }, + { + "name": "slug", + "type": "SlugField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": "no-slug", + "max_length": 60, + "related_model": null, + "relationship": null + }, + { + "name": "index", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "0", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "Category", + "field": "parent_category" + } + ], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "Tag", + "db_table": "blog_tag", + "fields": [ + { + "name": "id", + "type": "AutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 30, + "related_model": null, + "relationship": null + }, + { + "name": "slug", + "type": "SlugField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": "no-slug", + "max_length": 60, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "Links", + "db_table": "blog_links", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 30, + "related_model": null, + "relationship": null + }, + { + "name": "link", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "sequence", + "type": "IntegerField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "is_enable", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "True", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "show_type", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "i", + "max_length": 1, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_mod_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "SideBar", + "db_table": "blog_sidebar", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "content", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "sequence", + "type": "IntegerField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "is_enable", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "True", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_mod_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "BlogSettings", + "db_table": "blog_blogsettings", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "site_name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "site_description", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 1000, + "related_model": null, + "relationship": null + }, + { + "name": "site_seo_description", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 1000, + "related_model": null, + "relationship": null + }, + { + "name": "site_keywords", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 1000, + "related_model": null, + "relationship": null + }, + { + "name": "article_sub_length", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "300", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "sidebar_article_count", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "10", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "sidebar_comment_count", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "5", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "article_comment_count", + "type": "IntegerField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "5", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "show_google_adsense", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "google_adsense_codes", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": "", + "max_length": 2000, + "related_model": null, + "relationship": null + }, + { + "name": "open_site_comment", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "True", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "global_header", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "global_footer", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "beian_code", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": "", + "max_length": 2000, + "related_model": null, + "relationship": null + }, + { + "name": "analytics_code", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 1000, + "related_model": null, + "relationship": null + }, + { + "name": "show_gongan_code", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "gongan_beiancode", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": "", + "max_length": 2000, + "related_model": null, + "relationship": null + }, + { + "name": "comment_need_review", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "accounts": [ + { + "name": "BlogUser", + "db_table": "accounts_bloguser", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "password", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 128, + "related_model": null, + "relationship": null + }, + { + "name": "last_login", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "is_superuser", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "username", + "type": "CharField", + "primary_key": false, + "unique": true, + "null": false, + "blank": false, + "default": null, + "max_length": 150, + "related_model": null, + "relationship": null + }, + { + "name": "first_name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": 150, + "related_model": null, + "relationship": null + }, + { + "name": "last_name", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": 150, + "related_model": null, + "relationship": null + }, + { + "name": "email", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": 254, + "related_model": null, + "relationship": null + }, + { + "name": "is_staff", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "is_active", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "True", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "date_joined", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "nickname", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "source", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "groups", + "type": "ManyToManyField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "related_model": "Group" + }, + { + "name": "user_permissions", + "type": "ManyToManyField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "related_model": "Permission" + } + ], + "relationships": [ + { + "type": "ManyToManyField", + "target": "Group", + "field": "groups" + }, + { + "type": "ManyToManyField", + "target": "Permission", + "field": "user_permissions" + } + ], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "comments": [ + { + "name": "Comment", + "db_table": "comments_comment", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "body", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 300, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "author", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "BlogUser", + "relationship": "ForeignKey" + }, + { + "name": "article", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": "Article", + "relationship": "ForeignKey" + }, + { + "name": "parent_comment", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": "Comment", + "relationship": "ForeignKey" + }, + { + "name": "is_enable", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "BlogUser", + "field": "author" + }, + { + "type": "ForeignKey", + "target": "Article", + "field": "article" + }, + { + "type": "ForeignKey", + "target": "Comment", + "field": "parent_comment" + } + ], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "oauth": [ + { + "name": "OAuthUser", + "db_table": "oauth_oauthuser", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "author", + "type": "ForeignKey", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": "BlogUser", + "relationship": "ForeignKey" + }, + { + "name": "openid", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 50, + "related_model": null, + "relationship": null + }, + { + "name": "nickname", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 50, + "related_model": null, + "relationship": null + }, + { + "name": "token", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": 150, + "related_model": null, + "relationship": null + }, + { + "name": "picture", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": 350, + "related_model": null, + "relationship": null + }, + { + "name": "type", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 50, + "related_model": null, + "relationship": null + }, + { + "name": "email", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": 50, + "related_model": null, + "relationship": null + }, + { + "name": "metadata", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": true, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [ + { + "type": "ForeignKey", + "target": "BlogUser", + "field": "author" + } + ], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "OAuthConfig", + "db_table": "oauth_oauthconfig", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "type", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "a", + "max_length": 10, + "related_model": null, + "relationship": null + }, + { + "name": "appkey", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "appsecret", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "callback_url", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": 200, + "related_model": null, + "relationship": null + }, + { + "name": "is_enable", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "True", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "servermanager": [ + { + "name": "commands", + "db_table": "servermanager_commands", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "title", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 300, + "related_model": null, + "relationship": null + }, + { + "name": "command", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 2000, + "related_model": null, + "relationship": null + }, + { + "name": "describe", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 300, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "last_modify_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + }, + { + "name": "EmailSendLog", + "db_table": "servermanager_emailsendlog", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "emailto", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 300, + "related_model": null, + "relationship": null + }, + { + "name": "title", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 2000, + "related_model": null, + "relationship": null + }, + { + "name": "content", + "type": "TextField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "send_result", + "type": "BooleanField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "False", + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ], + "owntracks": [ + { + "name": "OwnTrackLog", + "db_table": "owntracks_owntracklog", + "fields": [ + { + "name": "id", + "type": "BigAutoField", + "primary_key": true, + "unique": true, + "null": false, + "blank": true, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "tid", + "type": "CharField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": 100, + "related_model": null, + "relationship": null + }, + { + "name": "lat", + "type": "FloatField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "lon", + "type": "FloatField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": null, + "max_length": null, + "related_model": null, + "relationship": null + }, + { + "name": "creation_time", + "type": "DateTimeField", + "primary_key": false, + "unique": false, + "null": false, + "blank": false, + "default": "", + "max_length": null, + "related_model": null, + "relationship": null + } + ], + "relationships": [], + "meta": { + "managed": true, + "abstract": false + } + } + ] +} \ No newline at end of file