Compare commits

..

58 Commits

Author SHA1 Message Date
盛钧涛 c36961b293 change accounts
2 months ago
盛钧涛 6143a4a432 Merge branch 'master' into develop
2 months ago
盛钧涛 f91eca2d4d add DjangoBlog
2 months ago
盛钧涛 51967e47d9 add DjangoBlog
2 months ago
盛钧涛 0d52b1cd2e Merge branch 'develop'
2 months ago
盛钧涛 12ee321c7f del DjangoBlog
2 months ago
盛钧涛 017f932fe9 del DjangoBlog
2 months ago
盛钧涛 506541d6f7 add DjangoBlog
2 months ago
mk c06d15ba90 Merge branch 'mk_branch' into develop
2 months ago
盛钧涛 074e9f5476 Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 dcbee62795 add change
3 months ago
盛钧涛 58fcb6b24a Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 9e1408389e add change
3 months ago
mk 746a6adc0b add
3 months ago
mk d208e833ff Merge remote-tracking branch 'origin/develop' into develop
3 months ago
盛钧涛 2c96334b4b add week6work
3 months ago
盛钧涛 260a92ff65 Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 7fd7fde93d del test
3 months ago
盛钧涛 f2cdcfba87 add week5work
3 months ago
盛钧涛 245a18b53f add test
3 months ago
盛钧涛 59897191ac del 2test
3 months ago
盛钧涛 a07a4e068b Merge branch 'frr_branch' of https://bdgit.educoder.net/puhanfmc3/tentest into develop
3 months ago
pxksbc67f 9792d18d9a Add frrweek4work2
3 months ago
盛钧涛 ee8fcfefcb add doc and src
3 months ago
盛钧涛 3e0fc26d2c shanchu
3 months ago
盛钧涛 22a24281be Merge branch 'develop' of https://bdgit.educoder.net/puhanfmc3/tentest into develop
3 months ago
puhanfmc3 62e81357dd Add week4work3
3 months ago
盛钧涛 15da01e6e9 rm week4work
3 months ago
puhanfmc3 84f4c9e8f3 Add week4work
3 months ago
盛钧涛 77df3e9e9e add src
3 months ago
盛钧涛 ac177291fe add doc
3 months ago
盛钧涛 43fabaeb51 add doc
3 months ago
盛钧涛 580968353e add src
3 months ago
盛钧涛 65f769084f add doc
3 months ago
盛钧涛 a7541d3093 test2
3 months ago
盛钧涛 1a332d9c92 test
3 months ago
盛钧涛 ed17f8fd02 Merge branch 'sjt_branch' of https://bdgit.educoder.net/puhanfmc3/tentest into sjt_branch
3 months ago
盛钧涛 9bdaac90a5 add doc and src
3 months ago
puhanfmc3 afc75321c9 Add src
3 months ago
puhanfmc3 3b612a260f Delete 'src'
3 months ago
puhanfmc3 04a70ac42b Add src
3 months ago
puhanfmc3 feddbef978 Add doc
3 months ago
puhanfmc3 cf77dfa0e7 Add src
3 months ago
puhanfmc3 2bb0abdc49 Add doc
3 months ago
puhanfmc3 cca871a53b Delete 'src.md'
3 months ago
puhanfmc3 4762f52d39 Delete 'doc.md'
3 months ago
puhanfmc3 cc874b667f Delete '.idea/vcs.xml'
3 months ago
puhanfmc3 797f57fe57 Delete '.idea/modules.xml'
3 months ago
puhanfmc3 45f725295e Delete '.idea/misc.xml'
3 months ago
puhanfmc3 2b8a829300 Delete '.idea/PythonGITproject.iml'
3 months ago
puhanfmc3 c6fb2238ac Delete '.idea/inspectionProfiles/profiles_settings.xml'
3 months ago
puhanfmc3 d4f097745d Delete '.idea/.gitignore'
3 months ago
puhanfmc3 fd175c6b8f Delete 'test1.py'
3 months ago
盛钧涛 731d3625dd add test1
3 months ago
盛钧涛 d0115e3660 add test1
3 months ago
pxksbc67f c138ab8d57 Add src
4 months ago
pxksbc67f c3c378b76b Add doc
4 months ago
盛钧涛 51df660506 xiugaichenggtest
4 months ago

@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.13 (DjangoBlog-master)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,26 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -1,5 +0,0 @@
from django.apps import AppConfig # mk导入Django的应用配置类#
class BlogConfig(AppConfig): # mk定义博客应用的配置类继承自AppConfig#
name = 'blog' # mk指定应用的名称为'blog'#

@ -1,77 +0,0 @@
import logging # mk导入日志记录模块#
from django.utils import timezone # mk导入Django的时间工具模块#
from djangoblog.utils import cache, get_blog_setting # mk从项目工具模块导入缓存和获取博客设置的函数#
from .models import Category, Article # mk从当前应用的模型中导入分类和文章模型#
logger = logging.getLogger(__name__) # mk创建当前模块的日志记录器#
def seo_processor(requests): # mkSEO处理器函数用于获取网站SEO相关配置信息并缓存#
"""
SEO处理器函数用于获取网站SEO相关配置信息并缓存
该函数从缓存中获取SEO配置信息如果缓存不存在则从数据库获取并设置缓存
主要包含网站基本信息SEO配置导航分类列表页面列表等数据
Args:
requests: HTTP请求对象用于获取请求协议和主机信息
Returns:
dict: 包含SEO配置信息的字典具体包含
- SITE_NAME: 网站名称
- SHOW_GOOGLE_ADSENSE: 是否显示Google Adsense
- GOOGLE_ADSENSE_CODES: Google Adsense代码
- SITE_SEO_DESCRIPTION: 网站SEO描述
- SITE_DESCRIPTION: 网站描述
- SITE_KEYWORDS: 网站关键词
- SITE_BASE_URL: 网站基础URL
- ARTICLE_SUB_LENGTH: 文章摘要长度
- nav_category_list: 导航分类列表
- nav_pages: 导航页面列表
- OPEN_SITE_COMMENT: 是否开启网站评论
- BEIAN_CODE: 备案号
- ANALYTICS_CODE: 统计代码
- BEIAN_CODE_GONGAN: 公安备案号
- SHOW_GONGAN_CODE: 是否显示公安备案号
- CURRENT_YEAR: 当前年份
- GLOBAL_HEADER: 全局头部代码
- GLOBAL_FOOTER: 全局底部代码
- COMMENT_NEED_REVIEW: 评论是否需要审核
"""
key = 'seo_processor' # mk设置缓存键名#
value = cache.get(key) # mk尝试从缓存中获取SEO数据#
# mk检查缓存是否存在如果存在则直接返回缓存数据#
if value:
return value
else:
logger.info('set processor cache.') # mk记录设置缓存的日志信息#
# mk缓存不存在从数据库获取配置信息#
setting = get_blog_setting()
value = { # mk构建包含网站SEO配置和导航数据的字典对象#
'SITE_NAME': setting.site_name, # mk网站名称显示在网页标题等位置#
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # mk是否显示Google Adsense广告的开关#
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # mkGoogle Adsense广告代码内容#
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # mk网站SEO描述信息用于meta标签#
'SITE_DESCRIPTION': setting.site_description, # mk网站描述信息#
'SITE_KEYWORDS': setting.site_keywords, # mk网站关键词用于SEO优化#
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # mk网站基础URL地址由请求协议和主机名组成#
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # mk文章摘要显示长度设置#
'nav_category_list': Category.objects.all(), # mk获取所有文章分类用于导航菜单显示#
'nav_pages': Article.objects.filter( # mk获取所有已发布的页面文章用于导航菜单显示#
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # mk网站评论功能开关设置#
'BEIAN_CODE': setting.beian_code, # mk网站备案号信息#
'ANALYTICS_CODE': setting.analytics_code, # mk网站统计分析代码#
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # mk公安备案号信息#
"SHOW_GONGAN_CODE": setting.show_gongan_code, # mk是否显示公安备案信息的开关#
"CURRENT_YEAR": timezone.now().year, # mk当前年份用于页面底部显示版权年份#
"GLOBAL_HEADER": setting.global_header, # mk全局页头HTML代码#
"GLOBAL_FOOTER": setting.global_footer, # mk全局页脚HTML代码#
"COMMENT_NEED_REVIEW": setting.comment_need_review, # mk评论是否需要审核的设置#
}
# mk将获取到的数据缓存10小时减少数据库查询压力#
cache.set(key, value, 60 * 60 * 10)
return value # mk返回构建的SEO和导航数据#

@ -1,39 +0,0 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
"""
mk:
博客搜索表单类
继承自haystack的SearchForm添加了自定义的查询字段和搜索逻辑
"""
querydata = forms.CharField(required=True)
def search(self):
"""
mk:
执行搜索操作
首先调用父类的搜索方法获取基础搜索结果然后验证表单数据的有效性
如果表单数据有效且查询数据存在则记录查询日志
Returns:
搜索结果数据集
"""
# mk:调用父类的搜索方法获取基础搜索结果
datas = super(BlogSearchForm, self).search()
# mk:验证表单数据,如果无效则返回无查询结果
if not self.is_valid():
return self.no_query_found()
# mk:如果查询数据存在,记录查询日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -1,36 +0,0 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# mk:TODO 参数化
class Command(BaseCommand):
"""
mk:
Django管理命令类用于构建搜索词列表
该命令从Tag和Category模型中提取名称数据合并去重后输出
"""
help = 'build search words'
def handle(self, *args, **options):
"""
mk:
处理命令的主要逻辑
从Tag和Category模型中获取所有名称合并为一个去重集合
然后将每个名称作为独立行打印输出
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
#mk: 从Tag和Category模型中提取所有名称并合并去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
#mk: 将去重后的名称集合按行打印输出
print('\n'.join(datas))

@ -1,32 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
"""
mk:
Django管理命令类用于清除整个缓存
该类继承自Django的BaseCommand提供了一个自定义的管理命令
可以通过命令行调用来清除应用的所有缓存数据
"""
help = 'clear the whole cache'
def handle(self, *args, **options):
"""
mk:
处理管理命令的主要逻辑
参数:
*args: 位置参数元组
**options: 关键字参数字典
返回值:
None
"""
# mk:清除所有缓存数据
cache.clear()
# mk:输出成功信息到标准输出
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,27 +0,0 @@
# mk:Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
"""
mk:
数据库迁移类用于向BlogSettings模型添加评论审核功能
该迁移依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
主要操作是为BlogSettings模型添加一个布尔类型的字段用于控制
评论是否需要管理员审核后才能显示
"""
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
# mk:添加评论审核控制字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -1,45 +0,0 @@
# mk:Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
""""
mk:
数据库迁移类用于重命名博客设置模型中的字段名
该迁移将以下字段进行重命名
- analyticscode -> analytics_code
- beiancode -> beian_code
- sitename -> site_name
继承自 migrations.Migration 遵循Django的数据库迁移机制
"""
# mk:定义迁移依赖关系确保在执行当前迁移前blog应用的0003_blogsettings_comment_need_review迁移已经完成
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# mk:定义具体的迁移操作列表
operations = [
# mk:将BlogSettings模型中的analyticscode字段重命名为analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
# mk:将BlogSettings模型中的beiancode字段重命名为beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
#mk:将BlogSettings模型中的sitename字段重命名为site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -1,651 +0,0 @@
# mk: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
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
# mk: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
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
"""
mk:
Django 数据库迁移类用于更新博客应用中的模型字段和选项
此迁移主要完成以下操作
- 更新多个模型的元数据如排序方式显示名称等
- 移除旧的时间字段created_time last_mod_time
- 添加新的时间字段creation_time last_modify_time
- 修改多个模型字段的属性 verbose_namedefault 最大长度等
Attributes:
dependencies (list): 指定当前迁移所依赖的其他迁移文件
operations (list): 包含所有数据库变更操作的列表
"""
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
# mk:更新 Article 模型的默认排序规则和其他元信息
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# mk:更新 Category 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# mk:更新 Links 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# mk:更新 Sidebar 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# mk:更新 Tag 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# mk:删除 Article 模型中已废弃的时间字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# mk:删除 Category 模型中已废弃的时间字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# mk:删除 Links 模型中已废弃的时间字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# mk:删除 Sidebar 模型中已废弃的时间字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# mk:删除 Tag 模型中已废弃的时间字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# mk:为 Article 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#mk: 为 Category 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# mk:为 Links 模型添加新的创建时间字段
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# mk:为 Sidebar 模型添加新的创建时间字段
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# mk:为 Tag 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# mk:修改 Article 模型各字段定义以增强可读性和规范性
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'),
),
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'),
),
# mk:修改 BlogSettings 模型相关配置项字段定义
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'),
),
# mk:修改 Category 模型字段定义
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'),
),
# mk:修改 Links 模型字段定义
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'),
),
# mk:修改 Sidebar 模型字段定义
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'),
),
# mk:修改 Tag 模型字段定义
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
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',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
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'),
),
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'),
),
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'),
),
]

@ -1,26 +0,0 @@
# mk:Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
"""
mk:
Django数据库迁移类用于修改BlogSettings模型的显示选项配置
该迁移类继承自Django的Migration基类主要作用是更新blogsettings模型的元数据选项
设置更友好的模型名称显示
"""
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
# mk:修改BlogSettings模型的显示选项设置单数和复数形式的可读名称
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -1,39 +0,0 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""
mk:
文章搜索索引类
用于为Article模型创建搜索引擎索引继承自haystack的SearchIndex和Indexable类
"""
# 定义搜索文档的文本字段document=True表示这是主要的搜索字段
# use_template=True表示使用模板来定义索引内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
mk:
获取索引关联的模型类
Returns:
Article: 返回Article模型类
"""
return Article
def index_queryset(self, using=None):
"""
mk:
获取需要建立索引的查询集
Args:
using (str, optional): 数据库别名默认为None
Returns:
QuerySet: 返回状态为'p'(已发布)的文章查询集
"""
return self.get_model().objects.filter(status='p')

@ -1,60 +0,0 @@
/*
HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
/**
mk:
* 获取HTML5元素列表
* @returns {Array|string} 返回元素名称数组或字符串
*/
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}
/**
mk:
* 获取与文档关联的缓存对象
* @param {Object} a - 文档对象
* @returns {Object} 返回对应的缓存对象
*/
function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}
/**
mk:
* 创建HTML元素
* @param {string} a - 元素标签名
* @param {Object} b - 文档对象可选
* @param {Object} c - 缓存对象可选
* @returns {Object} 返回创建的元素节点
*/
function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}
/**
mk:
* 重写文档的createElement和createDocumentFragment方法以支持HTML5元素
* @param {Object} a - 文档对象
* @param {Object} b - 缓存配置对象
*/
function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}
/**
mk:
* 初始化并应用HTML5 Shiv到指定文档
* @param {Object} a - 文档对象可选默认为当前文档
* @returns {Object} 返回处理后的文档对象
*/
function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}
// mk:初始化变量和配置
var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;
// mk:检测浏览器是否原生支持未知元素和相关API
(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();
// mk:定义HTML5 Shiv的核心配置和公共接口
var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};
// mk:应用HTML5 Shiv到当前文档
l.html5=e;q(f)})(this,document);

@ -1,95 +0,0 @@
/**
mk:
* fadeToggle - 切换元素的透明度动画效果
*
* @param {string} e - 动画类型标识符
* @param {object} r - 包含动画属性的对象 opacity
* @returns {function} 返回一个用于执行动画的函数
*/
fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}},
/**
mk:
* S.timers - 存储所有活动定时器的数组
*/
S.timers=[],
/**
mk:
* S.fx.tick - 执行所有活动动画帧更新
* 遍历并执行定时器队列中的每一项并清理已完成或无效的定时器
*/
S.fx.tick=function(){
var e,t=0,n=S.timers;
for(Ze=Date.now();t<n.length;t++)
(e=n[t])()||n[t]!==e||n.splice(t--,1);
n.length||S.fx.stop(),
Ze=void 0
},
/**
mk:
* S.fx.timer - 将新的动画定时器加入到定时器队列中
*
* @param {function} e - 要添加的定时器回调函数
*/
S.fx.timer=function(e){
S.timers.push(e),
S.fx.start()
},
/**
mk:
* S.fx.interval - 设置动画刷新间隔时间毫秒
*/
S.fx.interval=13,
/**
mk:
* S.fx.start - 启动动画循环处理机制
* 若尚未启动则初始化动画循环
*/
S.fx.start=function(){
et||(et=!0,ot())
},
/**
mk:
* S.fx.stop - 停止动画循环处理机制
*/
S.fx.stop=function(){
et=null
},
/**
mk:
* S.fx.speeds - 定义预设动画速度常量
* slow: 慢速动画持续时间
* fast: 快速动画持续时间
* _default: 默认动画持续时间
*/
S.fx.speeds={
slow:600,
fast:200,
_default:400
},
/**
mk:
* S.fn.delay - 在动画队列中插入延迟操作
*
* @param {number} r - 延迟的时间长度单位毫秒
* @param {string} e - 动画队列名称默认为 "fx"
* @returns {object} jQuery 对象本身以支持链式调用
*/
S.fn.delay=function(r,e){
return r=S.fx&&S.fx.speeds[r]||r,
e=e||"fx",
this.queue(e,function(e,t){
var n=C.setTimeout(e,r);
t.stop=function(){
C.clearTimeout(n)
}
})
};

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (accounts)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
</module>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (DjangoBlog-master)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (accounts)" project-jdk-type="Python SDK" />
</project>

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog-master.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog-master.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/accounts.iml" filepath="$PROJECT_DIR$/.idea/accounts.iml" />
</modules>
</component>
</project>

@ -9,15 +9,16 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
#sjt 后台创建用户表单:处理密码设置和验证
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # sjt 密码字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # sjt 确认密码字段
class Meta:
model = BlogUser
fields = ('email',)
model = BlogUser # sjt 关联BlogUser模型
fields = ('email',) # sjt 表单包含的字段
def clean_password2(self):
# Check that the two password entries match
#sjt 验证两次输入密码一致性
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
@ -25,29 +26,31 @@ class BlogUserCreationForm(forms.ModelForm):
return password2
def save(self, commit=True):
# Save the provided password in hashed format
#sjt 保存用户:加密密码并标记来源为后台
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
user.set_password(self.cleaned_data["password1"]) # sjt 加密密码
if commit:
user.source = 'adminsite'
user.source = 'adminsite' # sjt 标记来源为后台添加
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
#sjt 后台修改用户表单:配置修改用户的字段
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
model = BlogUser # sjt 关联BlogUser模型
fields = '__all__' # sjt 包含所有字段
field_classes = {'username': UsernameField} # sjt 用户名字段类
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
#sjt Admin后台用户管理配置定义显示和操作方式
form = BlogUserChangeForm # sjt 修改用户表单
add_form = BlogUserCreationForm # sjt 创建用户表单
list_display = ( # sjt 列表页显示的字段
'id',
'nickname',
'username',
@ -55,6 +58,6 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')
list_display_links = ('id', 'username') # sjt 列表页可点击的链接字段
ordering = ('-id',) # sjt 按ID倒序排列
search_fields = ('username', 'nickname', 'email') # sjt 搜索字段

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
#sjt accounts应用的配置类定义应用名称
name = 'accounts'

@ -9,39 +9,48 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
#sjt 登录表单:定义登录表单字段样式
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#sjt 注册表单:验证用户名、邮箱、密码合法性
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# sjt 设置确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#sjt 验证邮箱唯一性:已存在则抛出异常
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
model = get_user_model() # sjt 关联用户模型
fields = ("username", "email") # sjt 表单包含的字段
class ForgetPasswordForm(forms.Form):
#sjt 密码找回表单:验证邮箱、验证码、新密码合法性
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -50,7 +59,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("New password")
}
),
)
) # sjt 新密码字段
new_password2 = forms.CharField(
label="确认密码",
@ -60,7 +69,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Confirm password")
}
),
)
) # sjt 确认密码字段
email = forms.EmailField(
label='邮箱',
@ -70,7 +79,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Email")
}
),
)
) # sjt 邮箱字段
code = forms.CharField(
label=_('Code'),
@ -80,27 +89,28 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Code")
}
),
)
) # sjt 验证码字段
def clean_new_password2(self):
#sjt 验证两次输入密码一致性,并检查密码强度
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
password_validation.validate_password(password2) # sjt 调用Django密码验证器
return password2
def clean_email(self):
#sjt 验证邮箱是否已注册
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#sjt 验证验证码有效性
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
@ -112,6 +122,7 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
#sjt 发送验证码表单:验证邮箱格式
email = forms.EmailField(
label=_('Email'),
)
) # sjt 邮箱字段(用于发送验证码)

@ -9,27 +9,31 @@ from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
#sjt 扩展Django内置用户模型增加博客系统所需的用户属性
nickname = models.CharField(_('nick name'), max_length=100, blank=True) # sjt 用户昵称
creation_time = models.DateTimeField(_('creation time'), default=now) # sjt 账号创建时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # sjt 信息最后修改时间
source = models.CharField(_('create source'), max_length=100, blank=True) # sjt 账号创建来源(如注册、后台添加)
def get_absolute_url(self):
#sjt 获取用户详情页的相对URL
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
#sjt 对象字符串表示,返回用户邮箱
return self.email
def get_full_url(self):
#sjt 获取用户详情页的完整URL包含域名
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
ordering = ['-id'] # sjt 按ID倒序排列
verbose_name = _('user') # sjt 模型显示名称
verbose_name_plural = verbose_name
get_latest_by = 'id'
get_latest_by = 'id' # sjt 最新记录按ID判断

@ -13,6 +13,7 @@ from . import utils
class AccountTest(TestCase):
def setUp(self):
#sjt 测试初始化:创建测试客户端和测试用户
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
@ -20,16 +21,19 @@ class AccountTest(TestCase):
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
self.new_test = "xxx123--=" # sjt 测试用新密码
def test_validate_account(self):
#sjt 测试用户账号验证:管理员登录、文章创建等功能
site = get_current_site().domain
# sjt 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# sjt 测试登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
@ -37,12 +41,14 @@ class AccountTest(TestCase):
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# sjt 测试创建分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# sjt 测试创建文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -52,24 +58,30 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# sjt 测试访问文章管理页
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#sjt 测试注册流程:注册->邮箱验证->登录->功能验证
# sjt 验证初始状态无此用户
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 提交注册表单
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# sjt 验证用户创建成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 验证邮箱并激活用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
@ -78,59 +90,32 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# sjt 测试登录及权限
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# ... 后续测试创建分类、文章等功能
def test_verify_email_code(self):
#sjt 测试验证码验证功能
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
code = generate_code() # sjt 生成验证码
utils.set_code(to_email, code) # sjt 缓存验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
# sjt 验证正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# sjt 验证错误邮箱的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#sjt 测试成功发送密码找回验证码
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
@ -140,6 +125,7 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#sjt 测试发送密码找回验证码失败情况(无效邮箱)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
@ -153,6 +139,7 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#sjt 测试成功找回密码流程
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@ -167,14 +154,15 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
# sjt 验证密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
).first()
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#sjt 测试使用未注册邮箱找回密码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -188,20 +176,19 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#sjt 测试使用错误验证码找回密码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
code="111111", # sjt 错误验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200)

@ -4,25 +4,25 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
app_name = "accounts" # sjt 应用命名空间
urlpatterns = [re_path(r'^login/$',
urlpatterns = [re_path(r'^login/$', # sjt 登录页面路由
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
re_path(r'^register/$', # sjt 注册页面路由
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
re_path(r'^logout/$', # sjt 注销功能路由
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
path(r'account/result.html', # sjt 注册/验证结果页路由
views.account_result,
name='result'),
re_path(r'^forget_password/$',
re_path(r'^forget_password/$', # sjt 密码找回页面路由
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
re_path(r'^forget_password_code/$', # sjt 发送密码找回验证码路由
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
]

@ -0,0 +1,26 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
#sjt 自定义认证后端:支持使用用户名或邮箱登录
def authenticate(self, request, username=None, password=None, **kwargs):
#sjt 认证逻辑:根据输入判断是邮箱还是用户名,验证密码
if '@' in username: # sjt 包含@符号则视为邮箱登录
kwargs = {'email': username}
else: # sjt 否则视为用户名登录
kwargs = {'username': username}
try:
user = get_user_model().objects.get(** kwargs) # sjt 查询用户
if user.check_password(password): # sjt 验证密码
return user
except get_user_model().DoesNotExist: # sjt 用户不存在
return None
def get_user(self, username):
#sjt 根据用户ID获取用户对象
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -7,16 +7,16 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
_code_ttl = timedelta(minutes=5) # sjt 验证码有效期5分钟
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
'''sjt 发送密码重置验证码邮件
Args:
to_mail: 邮箱
to_mail: 邮箱
subject: 邮件主题
code: 验证码
"""
'''
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
@ -24,26 +24,23 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
'''sjt 验证验证码有效性
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
如果有错误返回错误信息字符串否则返回None
'''
cache_code = get_code(email) # sjt 从缓存获取验证码
if cache_code != code: # sjt 验证码不匹配
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
#sjt 将验证码存入缓存有效期5分钟
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)
#sjt 从缓存获取验证码
return cache.get(email)

@ -32,28 +32,34 @@ logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
#sjt 用户注册视图:处理注册表单提交,发送邮箱验证链接
form_class = RegisterForm # sjt 注册表单类
template_name = 'account/registration_form.html' # sjt 注册页面模板
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#sjt 处理请求分发添加CSRF保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:创建未激活用户,生成验证链接并发送邮件
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
user = form.save(False) # sjt 不立即保存用户
user.is_active = False # sjt 初始为未激活状态(需邮箱验证)
user.source = 'Register' # sjt 标记来源为注册
user.save(True) # sjt 保存用户
site = get_current_site().domain # sjt 获取当前站点域名
# sjt 生成双重加密签名(防止链接篡改)
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
if settings.DEBUG: # sjt 开发环境下使用本地域名
site = '127.0.0.1:8000'
path = reverse('account:result')
path = reverse('account:result') # sjt 获取结果页路由
# sjt 构建验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# sjt 构建验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -64,6 +70,7 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# sjt 发送验证邮件
send_email(
emailto=[
user.email,
@ -71,6 +78,7 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
# sjt 重定向到注册结果页
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
@ -81,60 +89,66 @@ class RegisterView(FormView):
class LogoutView(RedirectView):
url = '/login/'
#sjt 用户注销视图:处理用户注销并跳转登录页
url = '/login/' # sjt 注销后跳转的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发,禁止缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
#sjt 处理GET请求执行注销操作并清除缓存
logout(request) # sjt 注销用户
delete_sidebar_cache() # sjt 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
#sjt 用户登录视图:支持用户名/邮箱登录,处理登录状态
form_class = LoginForm # sjt 登录表单类
template_name = 'account/login.html' # sjt 登录页面模板
success_url = '/' # sjt 登录成功默认跳转页
redirect_field_name = REDIRECT_FIELD_NAME # sjt 重定向字段名
login_ttl = 2626560 # sjt 记住登录状态的有效期(一个月,单位秒)
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发保护敏感参数、CSRF保护、禁止缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#sjt 构建上下文数据获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
return super(LoginView, self).get_context_data(** kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:验证用户凭据,处理登录状态(含"记住我"功能)
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
if form.is_valid(): # sjt 验证用户凭据
delete_sidebar_cache() # sjt 删除侧边栏缓存
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
auth.login(self.request, form.get_user()) # sjt 登录用户
if self.request.POST.get("remember"): # sjt 如果勾选"记住我"
self.request.session.set_expiry(self.login_ttl) # sjt 设置会话有效期
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
#sjt 获取登录成功后的跳转URL验证安全性
redirect_to = self.request.POST.get(self.redirect_field_name)
# sjt 验证跳转URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@ -143,31 +157,33 @@ class LoginView(FormView):
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
#sjt 注册/验证结果页:处理邮箱验证逻辑,激活用户账号
type = request.GET.get('type') # sjt 获取操作类型(注册/验证)
id = request.GET.get('id') # sjt 获取用户ID
user = get_object_or_404(get_user_model(), id=id)
user = get_object_or_404(get_user_model(), id=id) # sjt 获取用户对象
logger.info(type)
if user.is_active:
if user.is_active: # sjt 如果用户已激活,直接跳转首页
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
if type == 'register': # sjt 注册成功结果页
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
else: # sjt 邮箱验证结果页
# sjt 验证签名是否正确
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
if sign != c_sign: # sjt 签名错误返回403
return HttpResponseForbidden()
user.is_active = True
user.is_active = True # sjt 激活用户
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
return render(request, 'account/result.html', { # sjt 渲染结果页
'title': title,
'content': content
})
@ -176,29 +192,34 @@ def account_result(request):
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
#sjt 密码找回视图:验证验证码后重置密码
form_class = ForgetPasswordForm # sjt 密码找回表单
template_name = 'account/forget_password.html' # sjt 密码找回页面模板
def form_valid(self, form):
#sjt 表单验证通过后:更新用户密码并跳转登录页
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# sjt 加密并更新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
return HttpResponseRedirect('/login/') # sjt 跳转登录页
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#sjt 发送密码找回验证码视图:处理发送验证码请求
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
#sjt 处理POST请求验证邮箱并发送验证码
form = ForgetPasswordCodeForm(request.POST) # sjt 验证邮箱表单
if not form.is_valid(): # sjt 邮箱验证失败
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
to_email = form.cleaned_data["email"] # sjt 获取目标邮箱
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
code = generate_code() # sjt 生成验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
utils.set_code(to_email, code) # sjt 缓存验证码有效期5分钟
return HttpResponse("ok")
return HttpResponse("ok") # sjt 发送成功返回"ok"

@ -13,32 +13,26 @@ class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
# mk: 指定表单关联的模型和字段
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
# mk: 批量发布文章操作,将选中的文章状态更新为已发布('p')
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
# mk: 批量将文章设为草稿状态,将选中的文章状态更新为草稿('d')
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
# mk: 批量关闭文章评论功能,将选中的文章评论状态更新为关闭('c')
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
# mk: 批量开启文章评论功能,将选中的文章评论状态更新为开启('o')
queryset.update(comment_status='o')
# mk: 为自定义管理操作设置显示名称
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
@ -46,13 +40,9 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
# mk: 设置文章管理界面每页显示20条记录
list_per_page = 20
# mk: 设置搜索字段,支持在文章正文和标题中搜索
search_fields = ('body', 'title')
# mk: 指定使用的表单类
form = ArticleForm
# mk: 设置在管理界面列表中显示的字段
list_display = (
'id',
'title',
@ -63,49 +53,36 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
# mk: 设置可以作为链接点击的字段
list_display_links = ('id', 'title')
# mk: 设置过滤器,可以在右侧边栏按状态、类型、分类筛选
list_filter = ('status', 'type', 'category')
# mk: 设置日期层级结构,用于按日期筛选文章
date_hierarchy = 'creation_time'
# mk: 设置多对多字段的横向筛选器
filter_horizontal = ('tags',)
# mk: 排除某些字段在表单中显示
exclude = ('creation_time', 'last_modify_time')
# mk: 启用"在站点上查看"功能
view_on_site = True
# mk: 注册自定义的批量操作
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# mk: 设置使用原始ID字段的外键字段提升性能
raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
# mk: 创建指向分类编辑页面的链接
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'<a href="%s">%s</a>' % (link, obj.category.name))
# mk: 设置分类链接字段的显示名称
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
# mk: 自定义表单,限制作者字段只能选择超级用户
form = super(ArticlelAdmin, 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):
# mk: 保存模型实例,调用父类的保存方法
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
# mk: 返回在站点上查看文章的URL
if obj:
url = obj.get_full_url()
return url
@ -116,29 +93,22 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
# mk: 标签管理界面排除某些字段
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
# mk: 设置分类管理界面显示的字段
list_display = ('name', 'parent_category', 'index')
# mk: 分类管理界面排除某些字段
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
# mk: 链接管理界面排除某些字段
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
# mk: 设置侧边栏管理界面显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# mk: 侧边栏管理界面排除某些字段
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
# mk: 博客设置管理界面使用默认配置
pass

@ -0,0 +1,5 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'

@ -0,0 +1,43 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_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'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
cache.set(key, value, 60 * 60 * 10)
return value

@ -7,11 +7,9 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# mk:检查是否启用了Elasticsearch配置
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# mk:创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -21,7 +19,6 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
# mk:尝试获取geoip管道如果不存在则创建
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{
@ -37,11 +34,6 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
"""
mk:
地理位置信息文档类
用于存储IP地址对应的地理位置信息
"""
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
@ -49,41 +41,21 @@ class GeoIp(InnerDoc):
class UserAgentBrowser(InnerDoc):
"""
mk:
用户代理浏览器信息类
存储浏览器的家族和版本信息
"""
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
"""
mk:
用户代理操作系统信息类
继承自UserAgentBrowser存储操作系统的家族和版本信息
"""
pass
class UserAgentDevice(InnerDoc):
"""
mk:
用户代理设备信息类
存储设备的家族品牌和型号信息
"""
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
"""
mk:
用户代理完整信息类
包含浏览器操作系统设备等完整用户代理信息
"""
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
@ -92,11 +64,6 @@ class UserAgent(InnerDoc):
class ElapsedTimeDocument(Document):
"""
mk:
性能监控文档类
用于记录页面访问性能数据包括URL响应时间访问时间等信息
"""
url = Keyword()
time_taken = Long()
log_datetime = Date()
@ -116,19 +83,8 @@ class ElapsedTimeDocument(Document):
class ElaspedTimeDocumentManager:
"""
mk:
性能监控文档管理类
提供性能监控数据的索引创建删除和保存功能
"""
@staticmethod
def build_index():
"""
mk:
构建性能监控索引
检查索引是否存在如果不存在则初始化索引
"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -137,28 +93,12 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
"""
mk:
删除性能监控索引
删除名为'performance'的索引忽略400和404错误
"""
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):
"""
mk:
创建并保存性能监控记录
Args:
url (str): 访问的URL地址
time_taken (int): 请求耗时毫秒
log_datetime (datetime): 日志记录时间
useragent (object): 用户代理对象包含浏览器系统设备信息
ip (str): 访问者IP地址
"""
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
@ -191,11 +131,6 @@ class ElaspedTimeDocumentManager:
class ArticleDocument(Document):
"""
mk:
文章文档类
用于Elasticsearch中的文章搜索索引包含文章的完整信息
"""
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={
@ -230,49 +165,19 @@ class ArticleDocument(Document):
class ArticleDocumentManager():
"""
mk:
文章文档管理类
提供文章索引的创建删除重建和更新功能
"""
def __init__(self):
"""
mk:
初始化文章文档管理器
自动创建索引
"""
self.create_index()
def create_index(self):
"""
mk:
创建文章索引
初始化ArticleDocument索引结构
"""
ArticleDocument.init()
def delete_index(self):
"""
mk:
删除文章索引
删除名为'blog'的索引忽略400和404错误
"""
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):
"""
mk:
将文章模型对象转换为文档对象
Args:
articles (list): Article模型对象列表
Returns:
list: 转换后的ArticleDocument文档对象列表
"""
return [
ArticleDocument(
meta={
@ -297,13 +202,6 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
"""
mk:
重建文章索引
Args:
articles (list, optional): 指定要重建索引的文章列表如果为None则重建所有文章
"""
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
@ -311,12 +209,5 @@ class ArticleDocumentManager():
doc.save()
def update_docs(self, docs):
"""
mk:
批量更新文档
Args:
docs (list): ArticleDocument文档对象列表
"""
for doc in docs:
doc.save()

@ -0,0 +1,19 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
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

@ -4,37 +4,15 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
ELASTICSEARCH_ENABLED
# mk:TODO 参数化
# TODO 参数化
class Command(BaseCommand):
"""
mk:
Django管理命令类用于构建搜索索引
该命令负责初始化和重建Elasticsearch索引包括_elapsed_time和article两种文档类型
"""
help = 'build search index'
def handle(self, *args, **options):
"""
mk:
处理命令执行逻辑
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
# mk:检查Elasticsearch是否启用
if ELASTICSEARCH_ENABLED:
# mk:构建_elapsed_time文档的索引
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
# mk:重新构建article文档的索引
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
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))

@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
help = 'clear the whole cache'
def handle(self, *args, **options):
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -6,47 +6,22 @@ from blog.models import Article, Tag, Category
class Command(BaseCommand):
"""
mk:
Django管理命令类用于创建测试数据
该命令会创建测试用户分类标签和文章数据用于开发和测试环境
继承自Django的BaseCommand基类
"""
help = 'create test datas'
def handle(self, *args, **options):
"""
mk:
处理命令执行的主要逻辑
参数:
*args: 位置参数元组
**options: 命令行选项字典
返回值:
None
"""
# mk:创建或获取测试用户
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# mk:创建父级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# mk:创建子级分类
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
# mk:创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# mk:批量创建20篇文章及其对应标签
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
@ -60,8 +35,6 @@ class Command(BaseCommand):
article.tags.add(basetag)
article.save()
#mk: 清除缓存并输出成功信息
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -8,26 +8,9 @@ site = get_current_site().domain
class Command(BaseCommand):
"""
mk:
Django管理命令类用于向百度搜索引擎推送网站URL
该命令支持推送文章标签分类等不同类型的页面URL到百度搜索引擎
以提高网站内容的收录效率
Attributes:
help (str): 命令帮助信息
"""
help = 'notify baidu url'
def add_arguments(self, parser):
"""
mk:
添加命令行参数
:param parser: 参数解析器对象用于定义命令行参数
:return: None
"""
parser.add_argument(
'data_type',
type=str,
@ -39,39 +22,21 @@ class Command(BaseCommand):
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
"""
mk:
根据相对路径生成完整的HTTPS URL
:param path: 相对路径
:return: 完整的HTTPS URL字符串
"""
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
"""
mk:
命令处理函数根据指定的数据类型收集URL并推送到百度
:param args: 位置参数
:param options: 命令行选项字典包含data_type键
:return: None
"""
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
# mk:收集文章URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
# mk:收集标签URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
# mk:收集分类URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
@ -83,4 +48,3 @@ class Command(BaseCommand):
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -8,26 +8,9 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
"""
mk:
Django管理命令类用于同步用户头像
该命令会遍历所有OAuth用户检查并更新他们的头像URL
确保头像图片可以正常访问如果无法访问则尝试重新获取或使用默认头像
"""
help = 'sync user avatar'
def test_picture(self, url):
"""
mk:
测试图片URL是否可以正常访问
Args:
url (str): 要测试的图片URL地址
Returns:
bool: 如果URL可以正常访问返回True否则返回False
"""
try:
if requests.get(url, timeout=2).status_code == 200:
return True
@ -35,17 +18,6 @@ class Command(BaseCommand):
pass
def handle(self, *args, **options):
"""
mk:
命令处理函数执行用户头像同步逻辑
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
@ -53,25 +25,19 @@ class Command(BaseCommand):
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
# mk:如果头像URL以静态URL开头说明可能是本地资源
if url.startswith(static_url):
#mk: 测试图片是否可以正常访问
if self.test_picture(url):
continue
else:
# mk:如果无法访问且用户有元数据则通过OAuth管理器重新获取头像
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
# mk:否则使用默认头像
url = static('blog/img/avatar.png')
else:
# mk:对于非本地资源,直接保存用户头像
url = save_user_avatar(url)
else:
# mk:如果没有头像URL使用默认头像
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
@ -79,4 +45,3 @@ class Command(BaseCommand):
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -10,34 +10,11 @@ logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""
mk:
在线用户中间件类用于记录页面加载时间和用户访问信息
该中间件会在每个请求处理前后记录时间计算页面渲染耗时
并将相关信息存储到Elasticsearch中如果启用的话
Args:
get_response: Django中间件的get_response回调函数
"""
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
mk:
中间件调用方法处理请求并记录页面加载时间
记录请求开始时间处理请求计算耗时并将相关信息存储到Elasticsearch
同时替换响应内容中的加载时间占位符
Args:
request: Django HttpRequest对象包含当前请求信息
Returns:
HttpResponse: 处理后的HTTP响应对象
"""
''' page render time '''
start_time = time.time()
response = self.get_response(request)
@ -46,24 +23,20 @@ class OnlineMiddleware(object):
user_agent = parse(http_user_agent)
if not response.streaming:
try:
# mk:计算页面渲染耗时
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
# mk:将耗时转换为毫秒并四舍五入到小数点后两位
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
# mk:创建Elasticsearch文档记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# mk:替换响应内容中的加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response
return response

@ -1,4 +1,4 @@
#mk: Generated by Django 4.1.7 on 2023-03-02 07:14
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
@ -8,24 +8,14 @@ import mdeditor.fields
class Migration(migrations.Migration):
"""
mk:
Django 数据库迁移类用于初始化博客应用所需的数据库表结构
该迁移文件定义了博客系统所需的核心数据模型包括网站配置友情链接侧边栏标签分类和文章等
"""
# mk:标记此迁移为初始迁移
initial = True
# mk:定义依赖关系,确保在用户模型之后执行
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# mk:定义具体的数据库操作
operations = [
# mk:创建网站配置模型 BlogSettings
migrations.CreateModel(
name='BlogSettings',
fields=[
@ -51,8 +41,6 @@ class Migration(migrations.Migration):
'verbose_name_plural': '网站配置',
},
),
# mk:创建友情链接模型 Links
migrations.CreateModel(
name='Links',
fields=[
@ -71,8 +59,6 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# mk:创建侧边栏模型 SideBar
migrations.CreateModel(
name='SideBar',
fields=[
@ -90,8 +76,6 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# mk:创建标签模型 Tag
migrations.CreateModel(
name='Tag',
fields=[
@ -107,8 +91,6 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
# mk:创建分类模型 Category
migrations.CreateModel(
name='Category',
fields=[
@ -126,8 +108,6 @@ class Migration(migrations.Migration):
'ordering': ['-index'],
},
),
# mk:创建文章模型 Article
migrations.CreateModel(
name='Article',
fields=[
@ -155,4 +135,3 @@ class Migration(migrations.Migration):
},
),
]

@ -1,34 +1,23 @@
#mk: Generated by Django 4.1.7 on 2023-03-29 06:08
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
"""
mk:
Django数据库迁移类用于向BlogSettings模型添加全局头部和尾部字段
该迁移依赖于blog应用的0001_initial初始迁移包含了两个操作
1. 向BlogSettings模型添加global_footer字段
2. 向BlogSettings模型添加global_header字段
"""
dependencies = [
('blog', '0001_initial'),
]
operations = [
# mk:添加全局尾部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# mk:添加全局头部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,17 @@
# 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'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,27 @@
# 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'),
]
operations = [
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -0,0 +1,300 @@
# 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
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
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',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
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'),
),
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'),
),
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'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -18,17 +18,6 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""
mk:
友情链接显示类型枚举
属性:
I: 首页显示
L: 列表页显示
P: 文章页显示
A: 全部页面显示
S: 幻灯片显示
"""
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
@ -37,31 +26,11 @@ class LinkShowType(models.TextChoices):
class BaseModel(models.Model):
"""
mk:
基础模型类提供通用字段和方法
属性:
id: 主键ID
creation_time: 创建时间
last_modify_time: 最后修改时间
"""
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
"""
mk:
保存模型实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
@ -76,13 +45,6 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
"""
mk:
获取完整的URL地址
Returns:
str: 完整的URL地址
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
@ -93,35 +55,11 @@ class BaseModel(models.Model):
@abstractmethod
def get_absolute_url(self):
"""
mk:
抽象方法获取绝对URL地址
Returns:
str: 绝对URL地址
"""
pass
class Article(BaseModel):
"""
mk:
文章模型类
属性:
title: 标题
body: 正文内容
pub_time: 发布时间
status: 状态草稿/发布
comment_status: 评论状态开启/关闭
type: 类型文章/页面
views: 浏览量
author: 作者
article_order: 排序
show_toc: 是否显示目录
category: 分类
tags: 标签
"""
"""文章"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
@ -168,23 +106,9 @@ class Article(BaseModel):
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
"""
mk:
将正文内容转换为字符串
Returns:
str: 正文内容字符串
"""
return self.body
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 文章标题
"""
return self.title
class Meta:
@ -194,13 +118,6 @@ class Article(BaseModel):
get_latest_by = 'id'
def get_absolute_url(self):
"""
mk:
获取文章详情页的绝对URL
Returns:
str: 文章详情页URL
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -210,48 +127,19 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
mk:
获取分类树结构
Returns:
list: 分类名称和URL的元组列表
"""
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):
"""
mk:
保存文章实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
def viewed(self):
"""
mk:
增加文章浏览量并保存
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
mk:
获取文章评论列表带缓存功能
Returns:
QuerySet: 评论查询集
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -264,48 +152,24 @@ class Article(BaseModel):
return comments
def get_admin_url(self):
"""
mk:
获取后台管理URL
Returns:
str: 后台管理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)
def next_article(self):
"""
mk:
获取下一篇已发布的文章
Returns:
Article|None: 下一篇文章对象或None
"""
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
mk:
获取上一篇已发布的文章
Returns:
Article|None: 上一篇文章对象或None
"""
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
mk:
从文章正文中提取第一张图片的URL
Returns:
str: 第一张图片的URL如果没有找到则返回空字符串
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
@ -314,16 +178,7 @@ class Article(BaseModel):
class Category(BaseModel):
"""
mk:
文章分类模型类
属性:
name: 分类名称
parent_category: 父级分类
slug: URL别名
index: 排序索引
"""
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
@ -340,35 +195,18 @@ class Category(BaseModel):
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
mk:
获取分类详情页的绝对URL
Returns:
str: 分类详情页URL
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 分类名称
"""
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
mk:
递归获取分类目录的父级分类树
Returns:
list: 包含当前分类及其所有父级分类的列表
递归获得分类目录的父级
:return:
"""
categorys = []
@ -383,11 +221,8 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
mk:
获取当前分类目录的所有子分类
Returns:
list: 包含当前分类及其所有子分类的列表
获得当前分类目录所有子集
:return:
"""
categorys = []
all_categorys = Category.objects.all()
@ -406,46 +241,18 @@ class Category(BaseModel):
class Tag(BaseModel):
"""
mk:
文章标签模型类
属性:
name: 标签名称
slug: URL别名
"""
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 标签名称
"""
return self.name
def get_absolute_url(self):
"""
mk:
获取标签详情页的绝对URL
Returns:
str: 标签详情页URL
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
mk:
获取使用该标签的文章数量
Returns:
int: 文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
@ -455,19 +262,8 @@ class Tag(BaseModel):
class Links(models.Model):
"""
mk:
友情链接模型类
属性:
name: 链接名称
link: 链接地址
sequence: 排序
is_enable: 是否启用
show_type: 显示类型
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
@ -487,29 +283,11 @@ class Links(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 链接名称
"""
return self.name
class SideBar(models.Model):
"""
mk:
侧边栏模型类用于展示HTML内容
属性:
name: 标题
content: 内容
sequence: 排序
is_enable: 是否启用
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
@ -523,41 +301,11 @@ class SideBar(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 侧边栏标题
"""
return self.name
class BlogSettings(models.Model):
"""
mk:
博客配置模型类
属性:
site_name: 网站名称
site_description: 网站描述
site_seo_description: SEO描述
site_keywords: SEO关键词
article_sub_length: 文章摘要长度
sidebar_article_count: 侧边栏文章数量
sidebar_comment_count: 侧边栏评论数量
article_comment_count: 文章评论数量
show_google_adsense: 是否显示Google广告
google_adsense_codes: Google广告代码
open_site_comment: 是否开启网站评论
global_header: 公共头部代码
global_footer: 公共尾部代码
beian_code: 备案号
analytics_code: 网站统计代码
show_gongan_code: 是否显示公安备案号
gongan_beiancode: 公安备案号
comment_need_review: 评论是否需要审核
"""
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
@ -616,38 +364,13 @@ class BlogSettings(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 网站名称
"""
return self.site_name
def clean(self):
"""
mk:
数据验证确保只存在一个配置实例
Raises:
ValidationError: 当已存在配置实例时抛出异常
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
mk:
保存配置并清除缓存
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,13 @@
from haystack import indexes
from blog.models import Article
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')

@ -1881,7 +1881,7 @@ img#wpstats {
}
}
/* mk:Minimum width of 960 pixels. */
/* Minimum width of 960 pixels. */
@media screen and (min-width: 960px) {
body {
background-color: #e6e6e6;
@ -2145,14 +2145,14 @@ div {
word-break: break-all;
}
/* mk:评论整体布局 - 使用相对定位实现头像左侧布局 */
/* 评论整体布局 - 使用相对定位实现头像左侧布局 */
.commentlist .comment-body {
position: relative;
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
}
/*mk: 评论作者信息 - 用户名和时间在同一行 */
/* 评论作者信息 - 用户名和时间在同一行 */
.commentlist .comment-author {
display: inline-block;
margin: 0 10px 5px 0;
@ -2173,7 +2173,7 @@ div {
line-height: 22px;
}
/*mk: 头像样式 - 绝对定位到左侧 */
/* 头像样式 - 绝对定位到左侧 */
.commentlist .comment-author .avatar {
position: absolute !important;
left: -60px; /* 定位到容器左侧 */
@ -2187,7 +2187,7 @@ div {
border: 1px solid #ddd;
}
/*mk: 评论作者名称样式 */
/* 评论作者名称样式 */
.commentlist .comment-author .fn {
display: inline;
margin: 0;
@ -2205,7 +2205,7 @@ div {
text-decoration: underline;
}
/* mk:评论内容样式 */
/* 评论内容样式 */
.commentlist .comment-body p {
margin: 5px 0 10px 0;
line-height: 1.5;
@ -2222,7 +2222,7 @@ div {
display: none;
}
/* mk:通用头像样式 */
/* 通用头像样式 */
.commentlist .avatar {
width: 48px !important;
height: 48px !important;
@ -2266,12 +2266,12 @@ div {
font-style: normal;
}
/* mk:pings */
/* pings */
.pinglist li {
padding-left: 0;
}
/*mk:comment text */
/* comment text */
.commentlist .comment-body p {
margin-bottom: 8px;
color: #777;
@ -2294,7 +2294,7 @@ div {
list-style: square;
}
/*mk: post author & admin comment */
/* post author & admin comment */
.commentlist li.bypostauthor > .comment-body:after,
.commentlist li.comment-author-admin > .comment-body:after {
display: block;
@ -2331,7 +2331,7 @@ div {
border-radius: 100%;
}
/* mk:child comment */
/* child comment */
.commentlist li ul {
}
@ -2340,42 +2340,42 @@ div {
padding-left: 48px;
}
/*mk: 嵌套评论整体布局 */
/* 嵌套评论整体布局 */
.commentlist li li .comment-body {
padding-left: 60px; /* mk:为48px头像 + 12px间距留出空间 */
min-height: 48px; /* mk:确保有足够高度容纳头像 */
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
}
/*mk: 嵌套评论作者信息 */
/* 嵌套评论作者信息 */
.commentlist li li .comment-author {
display: inline-block;
margin: 0 8px 5px 0;
font-size: 12px; /*mk: 稍小一点 */
font-size: 12px; /* 稍小一点 */
}
.commentlist li li .comment-meta {
display: inline-block;
margin: 0 0 8px 0;
font-size: 11px; /*mk: 稍小一点 */
font-size: 11px; /* 稍小一点 */
color: #666;
}
/* mk:评论容器整体左移 - 使用更高优先级 */
/* 评论容器整体左移 - 使用更高优先级 */
#comments #commentlist-container.comment-tab {
margin-left: -15px !important; /* mk:在小屏幕上向左移动15px */
padding-left: 0 !important; /* mk:移除左内边距 */
position: relative !important; /*mk: 确保定位正确 */
margin-left: -15px !important; /* 在小屏幕上向左移动15px */
padding-left: 0 !important; /* 移除左内边距 */
position: relative !important; /* 确保定位正确 */
}
/* mk:在较大屏幕上进一步左移 */
/* 在较大屏幕上进一步左移 */
@media screen and (min-width: 600px) {
#comments #commentlist-container.comment-tab {
margin-left: -30px !important; /* mk:在大屏幕上向左移动30px */
margin-left: -30px !important; /* 在大屏幕上向左移动30px */
}
/*mk: 响应式设计下的评论布局 - 保持48px头像 */
/* 响应式设计下的评论布局 - 保持48px头像 */
.commentlist .comment-body {
padding-left: 60px !important; /* mk:为48px头像 + 12px间距留出空间 */
padding-left: 60px !important; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px !important;
}
@ -2389,14 +2389,14 @@ div {
margin: 0 0 8px 0 !important;
}
/* mk:响应式设计下头像保持48px */
/* 响应式设计下头像保持48px */
.commentlist .comment-author .avatar {
left: -60px !important;
width: 48px !important;
height: 48px !important;
}
/* mk:嵌套评论在响应式设计下也保持48px头像 */
/* 嵌套评论在响应式设计下也保持48px头像 */
.commentlist li li .comment-body {
padding-left: 60px !important;
min-height: 48px !important;
@ -2409,10 +2409,10 @@ div {
}
}
/* mk:嵌套评论头像 */
/* 嵌套评论头像 */
.commentlist li li .comment-author .avatar {
position: absolute !important;
left: -60px; /* mk:定位到容器左侧 */
left: -60px; /* 定位到容器左侧 */
top: 0;
width: 48px !important;
height: 48px !important;
@ -2423,7 +2423,7 @@ div {
border: 1px solid #ddd;
}
/* mk:comments : nav
/* comments : nav
/* ------------------------------------ */
.comments-nav {
margin-bottom: 20px;
@ -2441,7 +2441,7 @@ div {
float: right;
}
/* mk: comments : form
/* comments : form
/* ------------------------------------ */
.logged-in-as,
.comment-notes,
@ -2626,12 +2626,11 @@ li #reply-title {
}
/* =============================================================================
mk:
============================================================================= */
/* mk:评论容器基础样式 */
/* 评论容器基础样式 */
.comment-body {
overflow-wrap: break-word;
word-wrap: break-word;
@ -2640,7 +2639,7 @@ mk:
box-sizing: border-box;
}
/* mk:修复评论中的代码块溢出 */
/* 修复评论中的代码块溢出 */
.comment-content pre,
.comment-body pre {
white-space: pre-wrap !important;
@ -2657,7 +2656,7 @@ mk:
margin: 10px 0;
}
/* mk:修复评论中的行内代码 */
/* 修复评论中的行内代码 */
.comment-content code,
.comment-body code {
word-wrap: break-word !important;
@ -2668,7 +2667,7 @@ mk:
vertical-align: top;
}
/* mk:修复评论中的长链接 */
/* 修复评论中的长链接 */
.comment-content a,
.comment-body a {
word-wrap: break-word !important;
@ -2677,7 +2676,7 @@ mk:
max-width: 100%;
}
/* mk:修复评论段落 */
/* 修复评论段落 */
.comment-content p,
.comment-body p {
word-wrap: break-word !important;
@ -2686,7 +2685,7 @@ mk:
margin: 10px 0;
}
/* mk:特殊处理代码高亮块 - 关键修复! */
/* 特殊处理代码高亮块 - 关键修复! */
.comment-content .codehilite,
.comment-body .codehilite {
max-width: 100% !important;
@ -2721,7 +2720,7 @@ mk:
box-sizing: border-box;
}
/* mk:修复代码高亮中的span标签 */
/* 修复代码高亮中的span标签 */
.comment-content .codehilite span,
.comment-body .codehilite span {
word-wrap: break-word !important;
@ -2731,7 +2730,7 @@ mk:
max-width: 100%;
}
/* mk:针对特定的代码高亮类 */
/* 针对特定的代码高亮类 */
.comment-content .codehilite .kt,
.comment-content .codehilite .nf,
.comment-content .codehilite .n,
@ -2744,7 +2743,7 @@ mk:
overflow-wrap: break-word !important;
}
/* mk:搜索结果高亮样式 */
/* 搜索结果高亮样式 */
.search-result {
margin-bottom: 30px;
padding: 20px;
@ -2786,7 +2785,7 @@ mk:
margin: 10px 0;
}
/* mk:搜索关键词高亮 */
/* 搜索关键词高亮 */
.search-excerpt em,
.search-result .entry-title em {
background-color: #fff3cd;
@ -2818,14 +2817,14 @@ mk:
overflow-wrap: break-word;
}
/* mk:修复评论列表项 */
/* 修复评论列表项 */
.commentlist li {
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
/* mk:确保评论内容不超出容器 */
/* 确保评论内容不超出容器 */
.commentlist .comment-body {
max-width: calc(100% - 20px); /* 留出一些边距 */
margin-left: 10px;
@ -2834,21 +2833,21 @@ mk:
word-wrap: break-word;
}
/* mk:重要:限制评论列表项的最大宽度 */
/* 重要:限制评论列表项的最大宽度 */
.commentlist li[style*="margin-left"] {
max-width: calc(100% - 2rem) !important;
overflow: hidden;
box-sizing: border-box;
}
/* mk:特别处理深层嵌套的评论 */
/* 特别处理深层嵌套的评论 */
.commentlist li[style*="margin-left: 3rem"],
.commentlist li[style*="margin-left: 6rem"],
.commentlist li[style*="margin-left: 9rem"] {
max-width: calc(100% - 1rem) !important;
}
/*mk: 移动端优化 */
/* 移动端优化 */
@media (max-width: 768px) {
.comment-content pre,
.comment-body pre {
@ -2863,14 +2862,14 @@ mk:
margin-right: 5px;
}
/* mk:移动端评论缩进调整 */
/* 移动端评论缩进调整 */
.commentlist li[style*="margin-left"] {
margin-left: 1rem !important;
max-margin-left: 2rem !important;
}
}
/* mk:防止表格溢出 */
/* 防止表格溢出 */
.comment-content table,
.comment-body table {
max-width: 100%;
@ -2879,14 +2878,14 @@ mk:
white-space: nowrap;
}
/* mk:修复图片溢出 */
/* 修复图片溢出 */
.comment-content img,
.comment-body img {
max-width: 100% !important;
height: auto !important;
}
/* mk:修复引用块 */
/* 修复引用块 */
.comment-content blockquote,
.comment-body blockquote {
max-width: 100%;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save