Compare commits

..

5 Commits

BIN
.DS_Store vendored

Binary file not shown.

8
.gitignore vendored

@ -1,8 +0,0 @@
.DS_Store
*.DS_Store
src/DjangoBlog/.venv/
src/DjangoBlog/venv/
src/DjangoBlog/.idea/
src/DjangoBlog/logs/
src/DjangoBlog/collectedstatic/
src/DjangoBlog/uploads/

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.11 (AI实验)" jdkType="Python SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (AI实验)" />
<option name="sdkName" value="Python 3.11 (git-test)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (AI实验)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (git-test)" project-jdk-type="Python SDK" />
</project>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/src/DjangoBlog" vcs="Git" />
</component>
</project>

@ -0,0 +1,2 @@
# git-test

@ -1,68 +0,0 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# 批量禁用评论的admin动作
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
# 批量启用评论的admin动作
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
# 设置admin动作在后台显示的名称
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# 评论模型的后台管理配置
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20# 每页显示20条记录
# 列表页显示的字段
list_display = (
'id',
'body',# 评论内容
'link_to_userinfo',# 自定义:链接到用户信息
'link_to_article',# 自定义:链接到文章
'is_enable',# 是否启用
'creation_time' # 创建时间
)
# 可点击跳转的字段
list_display_links = ('id', 'body', 'is_enable')
# 侧边栏过滤器
list_filter = ('is_enable',)
# 表单中排除的字段
exclude = ('creation_time', 'last_modify_time')
# 批量动作
actions = [disable_commentstatus, enable_commentstatus]
# 外键字段使用原始ID输入框避免下拉列表性能问题
raw_id_fields = ('author', 'article')
# 搜索字段
search_fields = ('body',)
# 自定义方法:显示用户链接
def link_to_userinfo(self, obj):
# 获取用户模型的admin URL信息
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 显示用户昵称或邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
#自定义方法:显示文章链接
def link_to_article(self, obj):
# 获取文章模型的admin URL信息
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 设置自定义方法的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,5 +0,0 @@
from django.apps import AppConfig
#评论应用的配置类
class CommentsConfig(AppConfig):
name = 'comments'# 指定应用名称为'comments',对应安装的应用名

@ -1,16 +0,0 @@
from django import forms # 导入Django表单模块
from django.forms import ModelForm # 导入模型表单基类
from .models import Comment # 从当前应用导入Comment模型
#评论表单类
class CommentForm(ModelForm):
# 父评论ID用于回复功能隐藏输入框非必填
# 该字段不在模型中,仅用于表单处理嵌套评论
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment # 指定关联的模型
fields = ['body'] # 表单只包含body字段评论内容

@ -1,49 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
# 数据库迁移类:创建评论模型
class Migration(migrations.Migration):
initial = True# 标记为comments应用的初始迁移
# 依赖项需要先执行blog的初始迁移和用户模型
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 数据库操作列表
operations = [
# 创建Comment模型
migrations.CreateModel(
name='Comment',# 模型名称
fields=[
# 主键ID自增大数据类型
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 评论正文文本类型最大长度300字符
('body', models.TextField(max_length=300, verbose_name='正文')),
# 创建时间,默认当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间,默认当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 是否启用/显示布尔类型默认True
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 外键:关联文章,级联删除
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 外键:关联用户(评论作者),级联删除
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 自关联外键:上级评论(用于嵌套评论),可为空
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
# 模型元数据配置
options={
'verbose_name': '评论',# 单数显示名称
'verbose_name_plural': '评论',# 复数显示名称
'ordering': ['-id'],# 按ID降序排列
'get_latest_by': 'id',# 获取最新记录的字段
},
),
]

@ -1,19 +0,0 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
# 数据库迁移:修改评论模型字段默认值
class Migration(migrations.Migration):
# 依赖项需要先执行comments应用的0001_initial迁移
dependencies = [
('comments', '0001_initial'),
]
# 数据库操作列表
operations = [
# 修改is_enable字段将默认值从True改为False评论默认不显示
migrations.AlterField(
model_name='comment',#模型名称
name='is_enable',#字段名称
field=models.BooleanField(default=False, verbose_name='是否显示'),# 新字段定义
),
]

@ -1,69 +0,0 @@
# 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
# 数据库迁移:评论模型字段重命名及国际化调整
class Migration(migrations.Migration):
# 依赖项需要user模型、blog的0005迁移及comments的0002迁移
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
# 数据库操作列表
operations = [
# 修改模型选项将verbose_name从中文改为英文
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
# 移除旧字段删除created_time字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 移除旧字段删除last_mod_time字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 添加新字段creation_time替代created_time
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加新字段last_modify_time替代last_mod_time
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# 修改字段article的verbose_name国际化
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
# 修改字段author的verbose_name国际化
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#修改字段is_enable的verbose_name国际化
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
# 修改字段parent_comment的verbose_name国际化
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
),
]

@ -1,52 +0,0 @@
from django.conf import settings # 导入Django设置
from django.db import models # 导入模型模块
from django.utils.timezone import now # 导入当前时间函数
from django.utils.translation import gettext_lazy as _ # 导入国际化翻译函数
from blog.models import Article # 导入文章模型
# 评论模型
class Comment(models.Model):
# 评论正文最大长度300字符
body = models.TextField('正文', max_length=300)
# 评论创建时间,默认当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间,默认当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 外键:评论作者,关联用户模型,级联删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# 外键:评论所属文章,关联文章模型,级联删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# 自关联外键:父评论(用于实现嵌套评论),可为空
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#是否启用/显示评论默认False需审核
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id'] # 按ID降序排列
verbose_name = _('comment') # 单数名称
verbose_name_plural = verbose_name # 复数名称
get_latest_by = 'id' # 获取最新评论的字段
# 返回评论正文作为字符串表示
def __str__(self):
return self.body

@ -1,32 +0,0 @@
from django import template
# 注册模板标签库
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
def parse(c):
# 过滤出当前评论的子评论(仅显示启用的)
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child)
# 递归查找子评论的子评论
parse(child)
parse(comment)
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
depth = 1 if ischild else 2
return {
'comment_item': comment,# 评论对象
'depth': depth# 缩进深度子评论1级父评论2级
}

@ -1,85 +0,0 @@
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# 评论功能测试类(使用数据库事务隔离)
class CommentsTest(TransactionTestCase):
def setUp(self):
# 初始化测试客户端和请求工厂
self.client = Client()
self.factory = RequestFactory()
# 设置评论需要审核才能显示
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
# 创建超级用户用于登录测试
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
"""批量启用文章下的所有评论"""
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
# 登录测试用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# 获取评论提交的URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# 测试发布第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302) # 验证重定向
#断言:评论未审核时不显示
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# 审核通过所有评论
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1) # 验证评论已显示
# 测试发布第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
article

@ -1,15 +0,0 @@
from django.urls import path
from . import views
# 设置应用命名空间用于URL反向解析
app_name = "comments"
# URL模式列表
urlpatterns = [
#文章评论提交接口
# 捕获整数类型article_id参数映射到CommentPostView视图
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),# URL名称模板中可通过{% url 'comments:postcomment' article_id %}调用
]

@ -1,43 +0,0 @@
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
def send_comment_email(comment):
"""发送评论相关邮件的主函数"""
site = get_current_site().domain# 获取当前站点域名
subject = _('Thanks for your comment')# 邮件标题(国际化)
article_url = f"https://{site}{comment.article.get_absolute_url()}" # 构建文章完整URL
# 1. 给评论作者发送感谢邮件
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email# 收件人邮箱
send_email([tomail], subject, html_content)
# 2. 如果是回复评论,给被回复者发送通知邮件
try:
if comment.parent_comment:
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
tomail = comment.parent_comment.author.email # 被回复者邮箱
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)#记录邮件发送异常

@ -1,72 +0,0 @@
# Create your views here.
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
# 评论提交处理视图
class CommentPostView(FormView):
form_class = CommentForm# 指定使用的表单类
template_name = 'blog/article_detail.html'# 表单无效时渲染的模板
#为视图添加CSRF保护
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# 处理GET请求直接访问评论提交URL时重定向到文章详情页评论区
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
# 表单验证失败时的处理:重新渲染文章详情页并显示表单错误
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
# 表单验证成功后的处理:保存评论并发送通知邮件
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user# 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk)# 获取完整的用户信息
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# 检查文章是否允许评论(评论状态或文章状态为关闭时抛出异常)
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# 创建评论实例但不保存到数据库
comment = form.save(False)
comment.article = article
# 获取站点配置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True# 无需审核则直接启用评论
comment.author = author# 设置评论作者
# 处理父评论ID如果存在则设置为回复评论
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# 保存评论到数据库
comment.save(True)
# 重定向到文章详情页并定位到该评论锚点位置
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

Binary file not shown.

@ -0,0 +1 @@
Subproject commit 76918f2c7f4bd4db4ad3877be1ee20c256446d21

Binary file not shown.

Binary file not shown.

@ -1,10 +0,0 @@
[run]
source = .
include = *.py
omit =
*migrations*
*tests*
*.html
*whoosh_cn_backend*
*settings.py*
*venv*

@ -1,11 +0,0 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/

@ -1,6 +0,0 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

@ -1,18 +0,0 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->
**我确定我已经查看了** (标注`[ ]`为`[x]`)
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
----
**我要申请** (标注`[ ]`为`[x]`)
- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -1,47 +0,0 @@
name: "CodeQL"
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
schedule:
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -1,136 +0,0 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
build-normal:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
run: |
python manage.py makemigrations
python manage.py migrate
python manage.py test
build-with-es:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
run: |
python manage.py makemigrations
python manage.py migrate
coverage run manage.py test
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: djangoblog/djangoblog:dev

@ -1,43 +0,0 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set env to docker dev tag
if: endsWith(github.ref, '/dev')
run: |
echo "DOCKER_TAG=test" >> $GITHUB_ENV
- name: Set env to docker latest tag
if: endsWith(github.ref, '/master')
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}

@ -1,39 +0,0 @@
name: publish release
on:
release:
types: [ published ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: name/app
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
linux/386
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}

@ -1,80 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/

@ -1,15 +0,0 @@
FROM python:3.11
ENV PYTHONUNBUFFERED 1
WORKDIR /code/djangoblog/
RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2025 车亮亮
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,158 +0,0 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>
<p align="center">
<b>一款功能强大、设计优雅的现代化博客系统</b>
<br>
<a href="/docs/README-en.md">English</a><b>简体中文</b>
</p>
---
DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能还通过一个灵活的插件系统让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
## ✨ 特性亮点
- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能代码解耦易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
## 🛠️ 技术栈
- **后端**: Python 3.10, Django 4.0
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis
- **前端**: HTML5, CSS3, JavaScript
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)
## 🚀 快速开始
### 1. 环境准备
确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
### 2. 克隆与安装
```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# 安装依赖
pip install -r requirements.txt
```
### 3. 项目配置
- **数据库**:
打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
在 MySQL 中创建数据库:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **更多配置**:
关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
### 4. 初始化数据库
```bash
python manage.py makemigrations
python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
```
### 5. 运行项目
```bash
# (可选) 生成一些测试数据
python manage.py create_testdata
# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
# 启动开发服务器
python manage.py runserver
```
现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
## 部署
- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
## 🧩 插件系统
插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
- **现有插件**: `view_count`(浏览计数), `seo_optimizer`SEO优化等都是通过插件系统实现的。
- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
## 🤝 贡献指南
我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug请随时提交 Issue 或 Pull Request。
## 📄 许可证
本项目基于 [MIT License](LICENSE) 开源。
---
## ❤️ 支持与赞助
如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
<img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
<i>(左) 支付宝 / (右) 微信</i>
</p>
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
</a>
</p>
---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

@ -1,16 +0,0 @@
DjangoBlog 项目
===============
这是一个基于Django的博客系统项目。
项目结构:
- manage.py: Django管理脚本
- requirements.txt: Python依赖包
- blog/: 博客主应用
- accounts/: 用户认证应用
- templates/: HTML模板
如何运行:
1. 安装依赖: pip install -r requirements.txt
2. 运行服务器: python manage.py runserver
3. 访问: http://127.0.0.1:8000

@ -1,247 +0,0 @@
from django import forms
from django.contrib import admin # 必须导入
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from .models import BlogUser, UserChangeHistory # 导入两个模型
class BlogUserCreationForm(forms.ModelForm):
"""
自定义用户创建表单用于Django管理后台
扩展了默认的用户创建功能添加密码确认逻辑
"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',) # ZXY: 创建用户时只需要邮箱字段
def clean_password2(self):
"""
验证两次输入的密码是否一致
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""
保存用户实例处理密码哈希化和来源标记
"""
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) # ZXY: 使用Django的密码哈希
if commit:
user.source = 'adminsite' # ZXY: 标记用户来源为管理后台
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""
自定义用户修改表单
继承Django默认的用户修改表单可以自定义字段显示
"""
class Meta:
model = BlogUser
fields = '__all__' # ZXY: 包含所有字段
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class UserChangeHistoryInline(admin.TabularInline):
"""用户变更历史内联显示"""
model = UserChangeHistory
extra = 0
max_num = 10 # 只显示最近10条记录
can_delete = False
readonly_fields = ('change_time', 'changed_by', 'change_description')
def has_add_permission(self, request, obj=None):
return False
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
# 添加内联显示
inlines = [UserChangeHistoryInline]
# 列表显示
list_display = (
'id',
'username',
'email',
'nickname',
'is_active',
'is_staff',
'last_login',
'date_joined'
)
# 列表过滤器
list_filter = (
'is_active',
'is_staff',
'is_superuser',
'source',
'date_joined'
)
# 搜索功能 - 支持用户名、邮箱、昵称、姓名搜索
search_fields = (
'username',
'email',
'nickname',
'first_name',
'last_name',
)
# 自定义搜索方法,支持完整姓名搜索
def get_search_results(self, request, queryset, search_term):
"""
自定义搜索逻辑支持完整姓名搜索
例如搜索"张三"会同时匹配first_name=""和last_name=""的用户
"""
# 首先调用父类的搜索
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
# 额外的搜索逻辑:支持完整姓名搜索
if search_term.strip():
from django.db.models import Q
# 检查是否是中文姓名格式2-4个字符
search_term = search_term.strip()
if 2 <= len(search_term) <= 4:
# 尝试分割姓名(支持"张三"和"张 三"两种格式)
name_parts = search_term.split()
if len(name_parts) == 1:
# 如果是"张三"这样的连续字符串
if len(search_term) == 2:
first_name = search_term[0]
last_name = search_term[1]
elif len(search_term) == 3:
first_name = search_term[:1]
last_name = search_term[1:]
else: # 4个字符
first_name = search_term[:2]
last_name = search_term[2:]
elif len(name_parts) == 2:
# 如果是"张 三"这样的格式
first_name, last_name = name_parts
else:
first_name = last_name = search_term
# 搜索名字和姓氏匹配的用户
name_matches = BlogUser.objects.filter(
Q(first_name__icontains=first_name) &
Q(last_name__icontains=last_name)
)
queryset = queryset | name_matches
# 同时支持只搜索姓或只搜索名的情况
simple_name_matches = BlogUser.objects.filter(
Q(first_name__icontains=search_term) |
Q(last_name__icontains=search_term)
)
queryset = queryset | simple_name_matches
return queryset, use_distinct
# 只读字段
readonly_fields = (
'date_joined',
'last_login',
'last_modify_time',
'creation_time'
)
# 重写 fieldsets
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('nickname', 'email', 'first_name', 'last_name')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'creation_time', 'last_modify_time')}),
(_('Source'), {'fields': ('source',)}),
)
# 重写 add_fieldsets
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'email', 'password1', 'password2'),
}),
)
# 添加批量操作
actions = ['activate_users', 'deactivate_users']
def activate_users(self, request, queryset):
"""批量激活用户"""
updated = queryset.update(is_active=True)
self.message_user(request, f'{updated}个用户已激活')
activate_users.short_description = "激活选中的用户"
def deactivate_users(self, request, queryset):
"""批量禁用用户"""
updated = queryset.update(is_active=False)
self.message_user(request, f'{updated}个用户已禁用')
deactivate_users.short_description = "禁用选中的用户"
def save_model(self, request, obj, form, change):
"""
重写保存方法记录变更历史
"""
if change: # 如果是更新操作
# 获取原始对象
original = BlogUser.objects.get(pk=obj.pk)
# 找出变化的字段
changes = []
tracked_fields = [
('nickname', _('Nick name')),
('first_name', _('First name')),
('last_name', _('Last name')),
('email', _('Email')),
('is_active', _('Active status')),
('is_staff', _('Staff status')),
]
for field, field_display in tracked_fields:
original_value = getattr(original, field)
new_value = getattr(obj, field)
if original_value != new_value:
changes.append(
f"{field_display}: {original_value or '(空)'}{new_value or '(空)'}"
)
# 如果有字段变化,记录历史
if changes:
change_description = "".join(changes)
UserChangeHistory.objects.create(
user=obj,
changed_by=request.user.username,
change_description=change_description
)
# 更新最后修改时间
obj.last_modify_time = timezone.now()
# 避免递归调用,直接更新数据库
BlogUser.objects.filter(pk=obj.pk).update(last_modify_time=timezone.now())
super().save_model(request, obj, form, change)

@ -1,9 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
账户应用的Django配置类
用于配置应用的元数据和启动时的初始化行为
"""
name = 'accounts' #ZXY: 应用的Python路径

@ -1,154 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""
用户登录表单
继承Django的AuthenticationForm添加Bootstrap样式支持
"""
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
#ZXY: 自定义表单字段的widget添加placeholder和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""
用户注册表单
扩展Django的UserCreationForm添加邮箱字段和样式支持
"""
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
#ZXY: 为所有字段添加Bootstrap样式和占位符文本
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""
邮箱验证方法
确保注册的邮箱地址在系统中是唯一的
"""
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() #ZXY: 使用项目中自定义的用户模型
fields = ("username", "email") #ZXY: 表单包含的字段
class ForgetPasswordForm(forms.Form):
"""
忘记密码重置表单
包含新密码设置邮箱验证和验证码确认
"""
#ZXY: 新密码字段 - 使用Django的密码输入组件
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
#ZXY: 确认密码字段 - 标签没有国际化,需要改进
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
#ZXY: 邮箱字段 - 用于识别用户和发送验证码
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
#ZXY: 验证码字段 - 用于安全验证
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
"""
验证两次输入的新密码是否一致
并使用Django的密码验证器检查密码强度
"""
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) #ZXY: Django密码强度验证
return password2
def clean_email(self):
"""
验证邮箱是否存在系统中
"""
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):
"""
验证邮箱验证码是否正确
调用utils模块的验证功能
"""
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
"""
获取忘记密码验证码的表单
简单的邮箱输入表单用于请求发送验证码邮件
"""
email = forms.EmailField(
label=_('Email'),
)

@ -1,62 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
"""
初始迁移类 - 创建BlogUser用户模型的数据库表结构
迁移是Django用于管理数据库模式变更的机制
"""
initial = True #ZXY: 标记这是应用的初始迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'), #ZXY: 依赖Django认证系统的迁移
]
operations = [
#ZXY: 创建BlogUser表的操作
migrations.CreateModel(
name='BlogUser',
fields=[
#ZXY: ID主键字段 - Django自动创建的自增BigInteger主键
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#ZXY: Django内置用户模型的标准字段 ↓
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
#ZXY: 自定义扩展字段 ↓
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), #ZXY: 记录创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), #ZXY: 最后修改时间
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), #ZXY: 用户注册来源
#ZXY: Django权限系统的多对多关系字段
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户', #ZXY: 单数显示名称
'verbose_name_plural': '用户', #ZXY: 复数显示名称
'ordering': ['-id'], #ZXY: 默认按ID倒序排列
'get_latest_by': 'id', #ZXY: 获取最新记录的依据字段
},
managers=[
#ZXY: 指定模型管理器使用Django默认的用户管理器
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -1,68 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
#ZXY: 第二次迁移 - 主要进行字段名称国际化和结构调整
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
"""
第二次迁移 - 修复字段命名和国际化问题
这次迁移展示了数据库模式演变的常见模式
"""
dependencies = [
('accounts', '0001_initial'), #ZXY: 依赖第一次迁移
]
operations = [
#ZXY: 操作1: 修改模型选项 - 将中文显示名改为英文
migrations.AlterModelOptions(
name='bloguser',
options={
'get_latest_by': 'id',
'ordering': ['-id'],
'verbose_name': 'user', #ZXY: 改为英文单数
'verbose_name_plural': 'user'}, #ZXY: 改为英文复数
),
#ZXY: 操作2: 删除旧的创建时间字段
migrations.RemoveField(
model_name='bloguser',
name='created_time', #ZXY: 移除旧的字段命名
),
#ZXY: 操作3: 删除旧的修改时间字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time', #ZXY: 移除旧的字段命名
),
#ZXY: 操作4: 添加新的创建时间字段(国际化命名)
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#ZXY: 操作5: 添加新的修改时间字段(国际化命名)
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#ZXY: 操作6: 修改昵称字段的显示名称(国际化)
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), # 改为英文
),
#ZXY: 操作7: 修改来源字段的显示名称(国际化)
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -1,30 +0,0 @@
# Generated by Django 5.2.4 on 2025-11-17 20:23
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_alter_bloguser_options_remove_bloguser_created_time_and_more'),
]
operations = [
migrations.CreateModel(
name='UserChangeHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('changed_by', models.CharField(blank=True, max_length=150, verbose_name='changed by')),
('change_time', models.DateTimeField(auto_now_add=True, verbose_name='change time')),
('change_description', models.TextField(blank=True, verbose_name='change description')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'user change history',
'verbose_name_plural': 'user change histories',
'ordering': ['-change_time'],
},
),
]

@ -1,112 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
"""
博客用户模型继承自Django内置的AbstractUser基类
扩展了标准用户模型增加博客系统特有的字段
"""
#ZXY: 昵称字段:用户显示名称,可为空
nickname = models.CharField(
_('nick name'), #ZXY: 国际化标签在admin中显示为"昵称"
max_length=100, #ZXY: 最大长度100个字符
blank=True) #ZXY: 允许为空,非必填字段
#ZXY: 创建时间:记录用户账号创建的时间点
creation_time = models.DateTimeField(
_('creation time'), #ZXY: 显示为"创建时间"
default=now) #ZXY: 默认值为当前时间,自动记录创建时间戳
#ZXY: 最后修改时间:记录用户信息最后一次修改的时间
last_modify_time = models.DateTimeField(
_('last modify time'), #ZXY: 显示为"最后修改时间"
default=now) #ZXY: 默认值为当前时间
#ZXY: 注册来源:记录用户是通过什么渠道注册的
source = models.CharField(
_('create source'), #ZXY: 显示为"创建来源"
max_length=100, #ZXY: 最大长度100个字符
blank=True) #ZXY: 允许为空
def get_absolute_url(self):
"""
获取用户详情页的URL路径不含域名
用于Django的通用视图和admin中的"查看现场"链接
Returns:
str: 用户详情页的URL路径 '/author/testuser/'
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username}) #ZXY: 使用用户名作为URL参数
def __str__(self):
"""
定义模型的字符串表示在admin和shell中显示
这里使用邮箱而不是用户名便于管理员识别用户
Returns:
str: 用户的邮箱地址
"""
return self.email
def get_full_url(self):
"""
获取包含完整域名的用户详情页URL
用于邮件通知外部链接等需要完整URL的场景
Returns:
str: 完整的用户URL 'https://example.com/author/testuser/'
"""
site = get_current_site().domain #ZXY: 获取当前站点的域名
url = "https://{site}{path}".format(
site=site,
path=self.get_absolute_url()) #ZXY: 组合域名和路径
return url
class Meta:
"""
模型的元数据配置类
用于定义模型级别的选项和配置
"""
ordering = ['-id'] #ZXY: 默认按ID降序排列新的用户显示在前面
verbose_name = _('user') #ZXY: 单数形式的显示名称
verbose_name_plural = verbose_name #ZXY: 复数形式与单数相同
get_latest_by = 'id' #ZXY: 指定获取最新记录的字段
class UserChangeHistory(models.Model):
"""
用户信息变更历史记录模型 - 简化版本
"""
user = models.ForeignKey(
BlogUser,
on_delete=models.CASCADE,
verbose_name=_('user')
)
changed_by = models.CharField(
_('changed by'),
max_length=150,
blank=True
)
change_time = models.DateTimeField(
_('change time'),
auto_now_add=True
)
change_description = models.TextField(
_('change description'),
blank=True
)
class Meta:
verbose_name = _('user change history')
verbose_name_plural = _('user change histories')
ordering = ['-change_time']
def __str__(self):
return f"{self.user.username} - {self.change_time.strftime('%Y-%m-%d %H:%M')}"

@ -1,287 +0,0 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
"""
账户功能测试套件
测试用户注册登录密码重置等核心功能的正确性
继承Django的TestCase提供数据库事务支持和断言方法
"""
def setUp(self):
"""
测试前置设置方法
在每个测试方法执行前运行用于准备测试数据
避免测试间的数据污染确保测试独立性
"""
self.client = Client() #ZXY: Django测试客户端用于模拟HTTP请求
self.factory = RequestFactory() #ZXY: 请求工厂,用于创建请求对象进行视图测试
#ZXY: 创建测试用户 - 使用create_user方法创建普通用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678" #ZXY: 测试密码,实际项目中应使用更强密码
)
self.new_test = "xxx123--=" #ZXY: 测试用的新密码
def test_validate_account(self):
"""
测试账户验证功能
验证超级用户创建登录权限和管理后台访问
这是一个综合性测试覆盖用户生命周期多个环节
"""
site = get_current_site().domain #ZXY: 获取当前站点域名
#ZXY: 创建超级用户 - 测试管理员账户创建功能
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg") #ZXY: 复杂密码测试
testuser = BlogUser.objects.get(username='liangliangyy1') #ZXY: 从数据库检索用户验证创建成功
#ZXY: 测试登录功能 - 使用测试客户端模拟用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #ZXY: 断言登录成功返回True
#ZXY: 测试管理后台访问权限 - 超级用户应该能访问/admin/
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) #ZXY: 断言HTTP 200响应
#ZXY: 创建测试分类 - 为后续文章测试准备数据
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#ZXY: 创建测试文章 - 验证用户与文章的关联关系
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user #ZXY: 关联刚创建的超级用户
article.category = category
article.type = 'a' #ZXY: 文章类型
article.status = 'p' #ZXY: 发布状态
article.save()
#ZXY: 测试文章管理页面访问 - 验证用户对自有文章的编辑权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) #ZXY: 断言可以正常访问文章管理页
def test_validate_register(self):
"""
测试用户注册完整流程
覆盖注册表单提交邮箱验证账户激活等关键步骤
验证新用户能否成功注册并正常使用系统
"""
#ZXY: 验证注册前用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com'))) #ZXY: 断言邮箱尚未注册
#ZXY: 提交注册表单 - 模拟用户注册请求
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T', #ZXY: 强密码测试
'password2': 'password123!q@wE#R$T', #ZXY: 确认密码
})
#ZXY: 验证用户创建成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com'))) #ZXY: 断言用户已创建
user = BlogUser.objects.filter(email='user123@user.com')[0] #ZXY: 获取新创建的用户
#ZXY: 生成邮箱验证签名 - 测试安全验证机制
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) #ZXY: 构建验证URL
#ZXY: 测试邮箱验证页面访问
response = self.client.get(url)
self.assertEqual(response.status_code, 200) #ZXY: 断言验证页面可访问
#ZXY: 测试新用户登录功能
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
#ZXY: 提升用户权限进行进一步测试
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache() #ZXY: 清理缓存
#ZXY: 创建测试数据验证完整功能流程
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()
#ZXY: 测试文章管理权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#ZXY: 测试退出登录功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) #ZXY: 退出可能重定向
#ZXY: 验证退出后权限失效
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) #ZXY: 应该无法直接访问
#ZXY: 测试重新登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
#ZXY: 验证重新登录后的权限恢复
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""
测试邮箱验证码功能
验证验证码的生成发送存储和验证全流程
这是密码重置功能的核心安全机制测试
"""
to_email = "admin@admin.com"
code = generate_code() #ZXY: 生成随机验证码
#ZXY: 测试验证码存储功能
utils.set_code(to_email, code)
#ZXY: 测试邮件发送功能 - 在实际环境中会真正发送邮件
utils.send_verify_email(to_email, code)
#ZXY: 测试正确验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) #ZXY: 断言验证成功返回None
#ZXY: 测试错误验证码验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) #ZXY: 断言验证失败返回错误信息字符串
def test_forget_password_email_code_success(self):
"""
测试忘记密码验证码请求 - 成功场景
验证系统能正确处理合法的密码重置请求
"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com") #ZXY: 使用已注册的邮箱
)
self.assertEqual(resp.status_code, 200) #ZXY: 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") #ZXY: 断言返回正确消息
def test_forget_password_email_code_fail(self):
"""
测试忘记密码验证码请求 - 失败场景
验证系统能正确处理各种异常输入情况
"""
#ZXY: 测试空数据提交
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict() #ZXY: 空数据
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #ZXY: 断言错误处理
#ZXY: 测试无效邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") #ZXY: 无效邮箱格式
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #ZXY: 断言格式验证
def test_forget_password_email_success(self):
"""
测试完整密码重置流程 - 成功场景
验证用户能通过验证码成功重置密码
"""
code = generate_code()
utils.set_code(self.blog_user.email, code) #ZXY: 存储验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code, #ZXY: 正确的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) #ZXY: 断言重定向响应
#ZXY: 验证用户密码是否修改成功 - 从数据库重新获取用户验证密码变更
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) #ZXY: 断言用户存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) #ZXY: 断言密码验证通过
def test_forget_password_email_not_user(self):
"""
测试密码重置 - 用户不存在场景
验证系统对未注册邮箱的处理
"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", #ZXY: 未注册的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) #ZXY: 断言停留在原页面(表单验证失败)
def test_forget_password_email_code_error(self):
"""
测试密码重置 - 验证码错误场景
验证系统对错误验证码的拒绝处理
"""
code = generate_code()
utils.set_code(self.blog_user.email, code) #ZXY: 存储正确验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", #ZXY: 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) #ZXY: 断言表单验证失败,停留在原页面

@ -1,44 +0,0 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts" #ZXY: 应用命名空间用于反向解析URL时避免冲突
"""
账户模块的URL配置
定义用户相关的所有页面路由和对应的视图处理函数
"""
urlpatterns = [
#ZXY: 登录页面 - 使用正则表达式匹配路径
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'), #ZXY: 类视图,登录成功后跳转到首页
name='login', #ZXY: URL名称用于反向解析
kwargs={'authentication_form': LoginForm}), #ZXY: 传递额外的参数到视图
#ZXY: 注册页面
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"), #ZXY: 注册成功后跳转到首页
name='register'),
#ZXY: 退出登录
re_path(r'^logout/$',
views.LogoutView.as_view(), #ZXY: 退出登录视图
name='logout'),
#ZXY: 账户操作结果页面 - 使用path函数更现代的URL定义方式
path(r'account/result.html',
views.account_result, #ZXY: 函数视图
name='result'),
#ZXY: 忘记密码页面
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(), #ZXY: 密码重置表单视图
name='forget_password'),
#ZXY: 获取忘记密码验证码
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(), #ZXY: 验证码发送API
name='forget_password_code'),
]

@ -1,72 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义用户认证后端
扩展Django默认认证支持使用邮箱或用户名登录
继承自ModelBackend重写authenticate方法实现多字段登录
Django默认只支持用户名登录这个后端增加了邮箱登录支持
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证核心方法
根据输入判断是邮箱还是用户名然后进行密码验证
Args:
request: HttpRequest对象包含请求信息
username (str): 用户输入的用户名或邮箱
password (str): 用户输入的密码
**kwargs: 其他可能的参数
Returns:
User: 认证成功返回用户对象
None: 认证失败返回None
Logic:
1. 判断输入是否包含@符号 邮箱登录
2. 不包含@符号 用户名登录
3. 查询数据库获取用户
4. 验证密码是否正确
"""
#ZXY: 步骤1判断登录方式邮箱 or 用户名)
if '@' in username:
#ZXY: 输入包含@符号,认为是邮箱登录
kwargs = {'email': username} #ZXY: 构建邮箱查询条件
else:
#ZXY: 输入不包含@符号,认为是用户名登录
kwargs = {'username': username} #ZXY: 构建用户名查询条件
try:
#ZXY: 步骤2根据查询条件获取用户对象
#ZXY: get_user_model() 获取项目中使用的用户模型
user = get_user_model().objects.get(**kwargs)
#ZXY: 步骤3验证密码
#ZXY: check_password() 方法会自动处理密码哈希比较
if user.check_password(password):
return user #ZXY: 密码正确,返回用户对象
except get_user_model().DoesNotExist:
#ZXY: 用户不存在返回None表示认证失败
return None
def get_user(self, username):
"""
根据用户ID获取用户对象
用于session认证时从数据库加载用户信息
Args:
username (str): 实际上是用户IDDjango的命名历史遗留
Returns:
User: 用户对象
None: 用户不存在
"""
try:
#ZXY: 根据主键用户ID获取用户对象
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -1,103 +0,0 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
#ZXY: 验证码有效期配置5分钟使用timedelta便于时间计算
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送邮箱验证码邮件
用于密码重置等需要邮箱验证的场景
Args:
to_mail (str): 接收邮件的邮箱地址
code (str): 6位数字验证码
subject (str): 邮件主题默认为"验证邮件"支持国际化
Example:
>>> send_verify_email("user@example.com", "123456")
# 发送包含验证码123456的邮件到user@example.com
"""
#ZXY: 构建邮件正文,使用国际化翻译
#ZXY: %(code)s是字符串格式化占位符会被实际的验证码替换
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
#ZXY: 调用项目通用的邮件发送函数
send_email(
[to_mail], #ZXY: 收件人列表,包装成列表形式
subject, #ZXY: 邮件主题
html_content) #ZXY:邮件内容HTML格式
def verify(email: str, code: str) -> typing.Optional[str]:
"""
验证用户输入的验证码是否正确
Args:
email (str): 用户邮箱作为缓存键
code (str): 用户输入的验证码
Returns:
Optional[str]:
- 验证成功返回 None
- 验证失败返回错误信息字符串
Note:
当前设计问题返回错误字符串的方式不够优雅
建议改进使用异常抛出让调用方处理异常
Example:
>>> result = verify("user@example.com", "123456")
>>> if result:
... print(f"验证失败: {result}")
"""
#ZXY: 从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
#ZXY: 比较缓存中的验证码和用户输入的验证码
if cache_code != code:
#ZXY: 验证失败,返回错误信息(支持国际化)
return gettext("Verification code error")
#ZXY: 验证成功返回None
return None
def set_code(email: str, code: str):
"""
将验证码存储到缓存中设置5分钟有效期
Args:
email (str): 邮箱地址作为缓存键
code (str): 要存储的验证码
Technical:
使用Django的缓存框架支持多种缓存后端数据库Redis内存等
timedelta.seconds 将时间差转换为秒数
"""
"""设置code"""
cache.set(
email, #ZXY: 缓存键:使用邮箱地址
code, #ZXY: 缓存值:验证码字符串
_code_ttl.seconds) #ZXY: 过期时间5分钟300秒
def get_code(email: str) -> typing.Optional[str]:
"""
从缓存中获取指定邮箱的验证码
Args:
email (str): 邮箱地址作为缓存键
Returns:
Optional[str]:
- 存在验证码则返回验证码字符串
- 不存在或已过期则返回None
"""
"""获取code"""
return cache.get(email) #ZXY: 从缓存中获取值不存在返回None

@ -1,313 +0,0 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
"""
用户注册视图类继承自Django的FormView
处理用户注册的全流程表单展示数据验证用户创建邮件发送
"""
#ZXY: 指定使用的表单类
form_class = RegisterForm
#ZXY: 指定渲染的模板路径
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
重写dispatch方法添加CSRF保护装饰器
CSRF保护防止跨站请求伪造攻击确保表单提交来自本站
Returns:
HttpResponse: 处理后的HTTP响应
"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
表单数据验证成功后的处理逻辑
这是注册流程的核心方法包含用户创建和邮件发送
Args:
form: 已验证通过的RegisterForm实例
Returns:
HttpResponseRedirect: 重定向到结果页面
"""
if form.is_valid():
#ZXY: 第一步:创建用户对象但不立即保存到数据库
#ZXY: save(False)表示只创建对象实例不执行数据库INSERT
user = form.save(False)
#ZXY: 第二步:设置用户状态和属性
user.is_active = False #ZXY: 邮箱验证前用户处于未激活状态,无法登录
user.source = 'Register' #ZXY: 记录注册来源,便于后续统计分析
#ZXY: 第三步:保存用户到数据库
#ZXY: save(True)表示立即执行数据库保存操作
user.save(True)
#ZXY: 第四步:准备邮箱验证所需数据
site = get_current_site().domain #ZXY: 获取当前站点域名
#ZXY: 生成安全签名:对密钥+用户ID进行双重SHA256哈希防止篡改
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#ZXY: 开发环境下使用本地地址,生产环境使用真实域名
if settings.DEBUG:
site = '127.0.0.1:8000' #ZXY: 开发环境固定地址
# 第五步:构建邮箱验证链接
path = reverse('account:result') #ZXY: 生成结果页面的URL路径
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
#ZXY: 第七步:发送验证邮件
send_email(
emailto=[
user.email, #ZXY: 收件人列表,这里只有注册用户
],
title='验证您的电子邮箱', #ZXY: 邮件标题
content=content) #ZXY: 邮件正文内容
#ZXY: 步骤8重定向到注册结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
#ZXY: 表单验证失败,重新显示表单页
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
"""
用户退出登录视图
功能处理用户退出清除session重定向到登录页
"""
url = '/login/' #ZXY: 退出后重定向的URL
@method_decorator(never_cache) #ZXY: 禁止缓存,确保每次都是最新状态
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""
处理GET请求退出登录
"""
logout(request) #ZXY: Django内置的退出函数清除session
delete_sidebar_cache() #ZXY: 清除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""
用户登录视图
功能处理用户登录认证支持"记住我"功能
"""
form_class = LoginForm #ZXY: 登录表单
template_name = 'account/login.html' #ZXY: 登录页面模板
success_url = '/' #ZXY: 登录成功后的默认跳转页面
redirect_field_name = REDIRECT_FIELD_NAME #ZXY: 重定向字段名,默认为'next'
login_ttl = 2626560 #ZXY: "记住我"功能的session有效期一个月单位
@method_decorator(sensitive_post_parameters('password')) #ZXY: 敏感参数保护,不在错误报告中显示密码
@method_decorator(csrf_protect) #ZXY: CSRF保护
@method_decorator(never_cache) #ZXY: 禁止缓存
def dispatch(self, request, *args, **kwargs):
"""
添加安全装饰器的dispatch方法
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
添加上下文数据主要是重定向URL
"""
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' #ZXY: 默认重定向到首页
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""
登录表单验证成功后的处理
"""
#ZXY: 使用Django内置的AuthenticationForm进行认证
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() #ZXY: 清除侧边栏缓存
logger.info(self.redirect_field_name) #ZXY: 记录日志
#ZXY: 执行用户登录
auth.login(self.request, form.get_user())
#ZXY: 处理"记住我"功能
if self.request.POST.get("remember"):
#ZXY: 设置session过期时间为一个月
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
#ZXY: return HttpResponseRedirect('/')
else:
#ZXY: 认证失败,重新显示表单
return self.render_to_response({
'form': form
})
def get_success_url(self):
"""
获取登录成功后的跳转URL
支持next参数指定的重定向
"""
redirect_to = self.request.POST.get(self.redirect_field_name)
#ZXY: 安全检查确保重定向URL在允许的host范围内
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url #ZXY: 不安全的URL使用默认首页
return redirect_to
def account_result(request):
"""
账户操作结果页面视图函数
处理注册结果和邮箱验证结果展示
"""
type = request.GET.get('type') #ZXY: 操作类型register或validation
id = request.GET.get('id') #ZXY: 用户ID
user = get_object_or_404(get_user_model(), id=id) #ZXY: 获取用户对象不存在返回404
logger.info(type) #ZXY: 记录操作类型日志
#ZXY: 如果用户已激活,直接跳转到首页
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
#ZXY: 注册成功页面
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
#ZXY: 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
#ZXY: 签名验证防止URL篡改
if sign != c_sign:
return HttpResponseForbidden() #ZXY: 签名不匹配返回403禁止访问
#ZXY: 激活用户账号
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
#ZXY: 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
#ZXY: 无效的操作类型,跳转到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""
忘记密码视图
功能处理用户密码重置
"""
form_class = ForgetPasswordForm #ZXY: 忘记密码表单
template_name = 'account/forget_password.html' #ZXY: 模板路径
def form_valid(self, form):
"""
表单验证成功后的密码重置逻辑
"""
if form.is_valid():
#ZXY: 根据邮箱查找用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
#ZXY: 使用Django的密码哈希函数设置新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() #ZXY: 保存到数据库
#ZXY: 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
#ZXY: 表单验证失败,重新显示表单
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
忘记密码邮箱验证码发送视图
功能接收邮箱地址发送密码重置验证码
"""
def post(self, request: HttpRequest):
"""
处理POST请求发送验证码邮件
"""
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") #ZXY: 表单验证失败
to_email = form.cleaned_data["email"] #ZXY: 获取邮箱地址
#ZXY: 生成6位验证码
code = generate_code()
#ZXY: 发送验证码邮件
utils.send_verify_email(to_email, code)
#ZXY: 将验证码存储到缓存5分钟有效期
utils.set_code(to_email, code)
return HttpResponse("ok") #ZXY: 返回成功响应

Binary file not shown.

@ -1,112 +0,0 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article #ZNY 导入Article模型
class ArticleForm(forms.ModelForm): #ZNY 定义文章表单类
# body = forms.CharField(widget=AdminPagedownWidget()) #ZNY 注释掉的Markdown编辑器字段
class Meta:
model = Article #ZNY 指定表单对应的模型
fields = '__all__' #ZNY 包含所有模型字段
def makr_article_publish(modeladmin, request, queryset): #ZNY 发布文章的管理动作函数
queryset.update(status='p') #ZNY 将选中文章状态更新为发布
def draft_article(modeladmin, request, queryset): #ZNY 设为草稿的管理动作函数
queryset.update(status='d') #ZNY 将选中文章状态更新为草稿
def close_article_commentstatus(modeladmin, request, queryset): #ZNY 关闭评论的管理动作函数
queryset.update(comment_status='c') #ZNY 将选中文章评论状态更新为关闭
def open_article_commentstatus(modeladmin, request, queryset): #ZNY 打开评论的管理动作函数
queryset.update(comment_status='o') #ZNY 将选中文章评论状态更新为打开
makr_article_publish.short_description = _('Publish selected articles') #ZNY 设置管理动作显示名称
draft_article.short_description = _('Draft selected articles') #ZNY 设置管理动作显示名称
close_article_commentstatus.short_description = _('Close article comments') #ZNY 设置管理动作显示名称
open_article_commentstatus.short_description = _('Open article comments') #ZNY 设置管理动作显示名称
class ArticlelAdmin(admin.ModelAdmin): #ZNY 文章模型管理类
list_per_page = 20 #ZNY 每页显示20条记录
search_fields = ('body', 'title') #ZNY 设置搜索字段
form = ArticleForm #ZNY 指定使用的表单类
list_display = ( #ZNY 设置列表页显示的字段
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title') #ZNY 设置可点击进入编辑页的字段
list_filter = ('status', 'type', 'category') #ZNY 设置右侧过滤器字段
filter_horizontal = ('tags',) #ZNY 设置标签字段使用水平过滤器
exclude = ('creation_time', 'last_modify_time') #ZNY 排除不需要在表单中显示的字段
view_on_site = True #ZNY 启用"在站点查看"功能
actions = [ #ZNY 注册管理动作
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj): #ZNY 自定义方法:生成分类链接
info = (obj.category._meta.app_label, obj.category._meta.model_name) #ZNY 获取分类模型的app和模型名称
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) #ZNY 生成分类编辑页链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) #ZNY 返回HTML格式的链接
link_to_category.short_description = _('category') #ZNY 设置自定义列显示名称
def get_form(self, request, obj=None, **kwargs): #ZNY 重写获取表单方法
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) #ZNY 调用父类方法
form.base_fields['author'].queryset = get_user_model( #ZNY 限制作者字段只能选择超级用户
).objects.filter(is_superuser=True)
return form #ZNY 返回修改后的表单
def save_model(self, request, obj, form, change): #ZNY 重写保存模型方法
super(ArticlelAdmin, self).save_model(request, obj, form, change) #ZNY 调用父类保存方法
def get_view_on_site_url(self, obj=None): #ZNY 重写获取站点查看URL方法
if obj: #ZNY 如果有具体对象
url = obj.get_full_url() #ZNY 获取文章的完整URL
return url #ZNY 返回文章URL
else: #ZNY 如果没有对象(列表页)
from djangoblog.utils import get_current_site #ZNY 导入获取当前站点函数
site = get_current_site().domain #ZNY 获取当前站点域名
return site #ZNY 返回站点域名
class TagAdmin(admin.ModelAdmin): #ZNY 标签模型管理类
exclude = ('slug', 'last_modify_time', 'creation_time') #ZNY 排除不需要在表单中显示的字段
class CategoryAdmin(admin.ModelAdmin): #ZNY 分类模型管理类
list_display = ('name', 'parent_category', 'index') #ZNY 设置列表页显示的字段
exclude = ('slug', 'last_modify_time', 'creation_time') #ZNY 排除不需要在表单中显示的字段
class LinksAdmin(admin.ModelAdmin): #ZNY 链接模型管理类
exclude = ('last_modify_time', 'creation_time') #ZNY 排除不需要在表单中显示的字段
class SideBarAdmin(admin.ModelAdmin): #ZNY 侧边栏模型管理类
list_display = ('name', 'content', 'is_enable', 'sequence') #ZNY 设置列表页显示的字段
exclude = ('last_modify_time', 'creation_time') #ZNY 排除不需要在表单中显示的字段
class BlogSettingsAdmin(admin.ModelAdmin): #ZNY 博客设置模型管理类
pass #ZNY 使用默认管理配置

@ -1,5 +0,0 @@
from django.apps import AppConfig #ZNY 导入Django应用配置基类
class BlogConfig(AppConfig): #ZNY 定义博客应用的配置类
name = 'blog' #ZNY 指定应用的Python路径为'blog'

@ -1,43 +0,0 @@
import logging #ZNY 导入日志模块
from django.utils import timezone #ZNY 导入Django时区工具
from djangoblog.utils import cache, get_blog_setting #ZNY 导入缓存工具和博客设置获取函数
from .models import Category, Article #ZNY 导入分类和文章模型
logger = logging.getLogger(__name__) #ZNY 获取当前模块的日志记录器
def seo_processor(requests): #ZNY 定义SEO上下文处理器函数
key = 'seo_processor' #ZNY 设置缓存键名
value = cache.get(key) #ZNY 尝试从缓存获取数据
if value: #ZNY 如果缓存存在
return value #ZNY 直接返回缓存数据
else: #ZNY 如果缓存不存在
logger.info('set processor cache.') #ZNY 记录设置缓存日志
setting = get_blog_setting() #ZNY 获取博客设置
value = { #ZNY 构建上下文数据字典
'SITE_NAME': setting.site_name, #ZNY 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, #ZNY 是否显示谷歌广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, #ZNY 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, #ZNY 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, #ZNY 网站描述
'SITE_KEYWORDS': setting.site_keywords, #ZNY 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', #ZNY 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length, #ZNY 文章摘要长度
'nav_category_list': Category.objects.all(), #ZNY 导航分类列表
'nav_pages': Article.objects.filter( #ZNY 导航页面列表
type='p', #ZNY 过滤类型为页面
status='p'), #ZNY 过滤状态为发布
'OPEN_SITE_COMMENT': setting.open_site_comment, #ZNY 是否开启全站评论
'BEIAN_CODE': setting.beian_code, #ZNY ICP备案号
'ANALYTICS_CODE': setting.analytics_code, #ZNY 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, #ZNY 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, #ZNY 是否显示公安备案
"CURRENT_YEAR": timezone.now().year, #ZNY 当前年份
"GLOBAL_HEADER": setting.global_header, #ZNY 全局头部内容
"GLOBAL_FOOTER": setting.global_footer, #ZNY 全局尾部内容
"COMMENT_NEED_REVIEW": setting.comment_need_review, #ZNY 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10) #ZNY 将数据存入缓存有效期10小时
return value #ZNY 返回上下文数据

@ -1,214 +0,0 @@
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') #ZNY 检查是否配置了Elasticsearch
if ELASTICSEARCH_ENABLED: #ZNY 如果Elasticsearch已启用
connections.create_connection( #ZNY 创建Elasticsearch连接
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) #ZNY 从配置中获取主机地址
from elasticsearch import Elasticsearch #ZNY 导入Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) #ZNY 创建Elasticsearch实例
from elasticsearch.client import IngestClient #ZNY 导入Ingest客户端
c = IngestClient(es) #ZNY 创建Ingest客户端实例
try:
c.get_pipeline('geoip') #ZNY 尝试获取geoip管道
except elasticsearch.exceptions.NotFoundError: #ZNY 如果geoip管道不存在
c.put_pipeline('geoip', body='''{ #ZNY 创建geoip管道
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''') #ZNY 定义geoip处理管道配置
class GeoIp(InnerDoc): #ZNY 定义GeoIP内嵌文档类
continent_name = Keyword() #ZNY 大洲名称
country_iso_code = Keyword() #ZNY 国家ISO代码
country_name = Keyword() #ZNY 国家名称
location = GeoPoint() #ZNY 地理位置坐标
class UserAgentBrowser(InnerDoc): #ZNY 定义用户代理浏览器信息类
Family = Keyword() #ZNY 浏览器家族
Version = Keyword() #ZNY 浏览器版本
class UserAgentOS(UserAgentBrowser): #ZNY 定义用户代理操作系统信息类,继承自浏览器类
pass #ZNY 继承父类字段
class UserAgentDevice(InnerDoc): #ZNY 定义用户代理设备信息类
Family = Keyword() #ZNY 设备家族
Brand = Keyword() #ZNY 设备品牌
Model = Keyword() #ZNY 设备型号
class UserAgent(InnerDoc): #ZNY 定义完整的用户代理信息类
browser = Object(UserAgentBrowser, required=False) #ZNY 浏览器信息对象
os = Object(UserAgentOS, required=False) #ZNY 操作系统信息对象
device = Object(UserAgentDevice, required=False) #ZNY 设备信息对象
string = Text() #ZNY 原始用户代理字符串
is_bot = Boolean() #ZNY 是否为机器人
class ElapsedTimeDocument(Document): #ZNY 定义性能耗时文档类
url = Keyword() #ZNY 请求URL
time_taken = Long() #ZNY 耗时(毫秒)
log_datetime = Date() #ZNY 日志时间
ip = Keyword() #ZNY IP地址
geoip = Object(GeoIp, required=False) #ZNY GeoIP地理位置信息
useragent = Object(UserAgent, required=False) #ZNY 用户代理信息
class Index: #ZNY 定义索引配置
name = 'performance' #ZNY 索引名称
settings = { #ZNY 索引设置
"number_of_shards": 1, #ZNY 分片数量
"number_of_replicas": 0 #ZNY 副本数量
}
class Meta: #ZNY 元数据配置
doc_type = 'ElapsedTime' #ZNY 文档类型
class ElaspedTimeDocumentManager: #ZNY 性能耗时文档管理器类
@staticmethod
def build_index(): #ZNY 构建索引静态方法
from elasticsearch import Elasticsearch #ZNY 导入Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) #ZNY 创建客户端
res = client.indices.exists(index="performance") #ZNY 检查索引是否存在
if not res: #ZNY 如果索引不存在
ElapsedTimeDocument.init() #ZNY 初始化性能耗时文档索引
@staticmethod
def delete_index(): #ZNY 删除索引静态方法
from elasticsearch import Elasticsearch #ZNY 导入Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) #ZNY 创建客户端
es.indices.delete(index='performance', ignore=[400, 404]) #ZNY 删除性能索引,忽略特定错误
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip): #ZNY 创建性能记录静态方法
ElaspedTimeDocumentManager.build_index() #ZNY 确保索引存在
ua = UserAgent() #ZNY 创建用户代理对象
ua.browser = UserAgentBrowser() #ZNY 创建浏览器信息对象
ua.browser.Family = useragent.browser.family #ZNY 设置浏览器家族
ua.browser.Version = useragent.browser.version_string #ZNY 设置浏览器版本
ua.os = UserAgentOS() #ZNY 创建操作系统信息对象
ua.os.Family = useragent.os.family #ZNY 设置操作系统家族
ua.os.Version = useragent.os.version_string #ZNY 设置操作系统版本
ua.device = UserAgentDevice() #ZNY 创建设备信息对象
ua.device.Family = useragent.device.family #ZNY 设置设备家族
ua.device.Brand = useragent.device.brand #ZNY 设置设备品牌
ua.device.Model = useragent.device.model #ZNY 设置设备型号
ua.string = useragent.ua_string #ZNY 设置原始用户代理字符串
ua.is_bot = useragent.is_bot #ZNY 设置是否为机器人
doc = ElapsedTimeDocument( #ZNY 创建性能耗时文档
meta={ #ZNY 文档元数据
'id': int( #ZNY 使用时间戳作为文档ID
round(
time.time() *
1000))
},
url=url, #ZNY 设置URL
time_taken=time_taken, #ZNY 设置耗时
log_datetime=log_datetime, #ZNY 设置日志时间
useragent=ua, ip=ip) #ZNY 设置用户代理和IP
doc.save(pipeline="geoip") #ZNY 保存文档并使用geoip管道处理
class ArticleDocument(Document): #ZNY 定义文章文档类
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #ZNY 正文使用IK中文分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #ZNY 标题使用IK中文分词器
author = Object(properties={ #ZNY 作者对象
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), #ZNY 作者昵称
'id': Integer() #ZNY 作者ID
})
category = Object(properties={ #ZNY 分类对象
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), #ZNY 分类名称
'id': Integer() #ZNY 分类ID
})
tags = Object(properties={ #ZNY 标签对象
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), #ZNY 标签名称
'id': Integer() #ZNY 标签ID
})
pub_time = Date() #ZNY 发布时间
status = Text() #ZNY 文章状态
comment_status = Text() #ZNY 评论状态
type = Text() #ZNY 文章类型
views = Integer() #ZNY 浏览量
article_order = Integer() #ZNY 文章排序
class Index: #ZNY 定义索引配置
name = 'blog' #ZNY 索引名称
settings = { #ZNY 索引设置
"number_of_shards": 1, #ZNY 分片数量
"number_of_replicas": 0 #ZNY 副本数量
}
class Meta: #ZNY 元数据配置
doc_type = 'Article' #ZNY 文档类型
class ArticleDocumentManager(): #ZNY 文章文档管理器类
def __init__(self): #ZNY 初始化方法
self.create_index() #ZNY 创建索引
def create_index(self): #ZNY 创建索引方法
ArticleDocument.init() #ZNY 初始化文章文档索引
def delete_index(self): #ZNY 删除索引方法
from elasticsearch import Elasticsearch #ZNY 导入Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) #ZNY 创建客户端
es.indices.delete(index='blog', ignore=[400, 404]) #ZNY 删除博客索引,忽略特定错误
def convert_to_doc(self, articles): #ZNY 将文章转换为文档方法
return [ #ZNY 返回文档列表
ArticleDocument( #ZNY 创建文章文档
meta={
'id': article.id}, #ZNY 使用文章ID作为文档ID
body=article.body, #ZNY 设置正文
title=article.title, #ZNY 设置标题
author={ #ZNY 设置作者信息
'nickname': article.author.username, #ZNY 作者用户名
'id': article.author.id}, #ZNY 作者ID
category={ #ZNY 设置分类信息
'name': article.category.name, #ZNY 分类名称
'id': article.category.id}, #ZNY 分类ID
tags=[ #ZNY 设置标签列表
{
'name': t.name, #ZNY 标签名称
'id': t.id} for t in article.tags.all()], #ZNY 遍历所有标签
pub_time=article.pub_time, #ZNY 设置发布时间
status=article.status, #ZNY 设置文章状态
comment_status=article.comment_status, #ZNY 设置评论状态
type=article.type, #ZNY 设置文章类型
views=article.views, #ZNY 设置浏览量
article_order=article.article_order) for article in articles] #ZNY 设置文章排序
def rebuild(self, articles=None): #ZNY 重建索引方法
ArticleDocument.init() #ZNY 重新初始化索引
articles = articles if articles else Article.objects.all() #ZNY 获取所有文章或指定文章
docs = self.convert_to_doc(articles) #ZNY 转换为文档格式
for doc in docs: #ZNY 遍历所有文档
doc.save() #ZNY 保存文档到Elasticsearch
def update_docs(self, docs): #ZNY 更新文档方法
for doc in docs: #ZNY 遍历文档列表
doc.save() #ZNY 保存更新后的文档

@ -1,19 +0,0 @@
import logging #ZNY 导入日志模块
from django import forms #ZNY 导入Django表单模块
from haystack.forms import SearchForm #ZNY 导入Haystack搜索表单基类
logger = logging.getLogger(__name__) #ZNY 获取当前模块的日志记录器
class BlogSearchForm(SearchForm): #ZNY 定义博客搜索表单类继承自Haystack搜索表单
querydata = forms.CharField(required=True) #ZNY 定义查询数据字段,设置为必填
def search(self): #ZNY 重写搜索方法
datas = super(BlogSearchForm, self).search() #ZNY 调用父类的搜索方法获取数据
if not self.is_valid(): #ZNY 如果表单验证失败
return self.no_query_found() #ZNY 返回无查询结果
if self.cleaned_data['querydata']: #ZNY 如果查询数据存在且已清洗
logger.info(self.cleaned_data['querydata']) #ZNY 记录查询关键词到日志
return datas #ZNY 返回搜索结果数据

@ -1,18 +0,0 @@
from django.core.management.base import BaseCommand #ZNY 导入Django管理命令基类
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED #ZNY 导入Elasticsearch相关文档和管理器
# TODO 参数化 #ZNY 待办事项:将命令参数化
class Command(BaseCommand): #ZNY 定义构建搜索索引命令类继承BaseCommand
help = 'build search index' #ZNY 命令帮助信息
def handle(self, *args, **options): #ZNY 命令处理主方法
if ELASTICSEARCH_ENABLED: #ZNY 检查Elasticsearch是否启用
ElaspedTimeDocumentManager.build_index() #ZNY 构建性能耗时文档索引
manager = ElapsedTimeDocument() #ZNY 创建性能耗时文档管理器实例
manager.init() #ZNY 初始化性能耗时文档索引
manager = ArticleDocumentManager() #ZNY 创建文章文档管理器实例
manager.delete_index() #ZNY 删除现有文章索引
manager.rebuild() #ZNY 重新构建文章索引

@ -1,13 +0,0 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化 #ZNY 待办事项:将命令参数化
class Command(BaseCommand): #ZNY 定义构建搜索词命令类继承BaseCommand
help = 'build search words' #ZNY 命令帮助信息:构建搜索词
def handle(self, *args, **options): #ZNY 命令处理主方法
datas = set([t.name for t in Tag.objects.all()] + #ZNY 获取所有标签名称和分类名称,合并并去重
[t.name for t in Category.objects.all()])
print('\n'.join(datas)) #ZNY 将数据用换行符连接并打印输出

@ -1,11 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand): #ZNY 定义清除缓存命令类继承BaseCommand
help = 'clear the whole cache' #ZNY 命令帮助信息:清除整个缓存
def handle(self, *args, **options): #ZNY 命令处理主方法
cache.clear() #ZNY 调用Django缓存接口清除所有缓存
self.stdout.write(self.style.SUCCESS('Cleared cache\n')) #ZNY 输出成功信息到标准输出

@ -1,40 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand): #ZNY 定义创建测试数据命令类继承BaseCommand
help = 'create test datas' #ZNY 命令帮助信息:创建测试数据
def handle(self, *args, **options): #ZNY 命令处理主方法
user = get_user_model().objects.get_or_create( #ZNY 获取或创建测试用户
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] #ZNY 设置用户邮箱、用户名和加密密码
pcategory = Category.objects.get_or_create( #ZNY 获取或创建父分类
name='我是父类目', parent_category=None)[0] #ZNY 设置父分类名称,无父级分类
category = Category.objects.get_or_create( #ZNY 获取或创建子分类
name='子类目', parent_category=pcategory)[0] #ZNY 设置子分类名称,指定父级分类
category.save() #ZNY 保存子分类
basetag = Tag() #ZNY 创建基础标签实例
basetag.name = "标签" #ZNY 设置基础标签名称
basetag.save() #ZNY 保存基础标签
for i in range(1, 20): #ZNY 循环创建19篇文章
article = Article.objects.get_or_create( #ZNY 获取或创建文章
category=category, #ZNY 设置文章分类
title='nice title ' + str(i), #ZNY 设置带编号的文章标题
body='nice content ' + str(i), #ZNY 设置带编号的文章正文
author=user)[0] #ZNY 设置文章作者
tag = Tag() #ZNY 创建新标签实例
tag.name = "标签" + str(i) #ZNY 设置带编号的标签名称
tag.save() #ZNY 保存标签
article.tags.add(tag) #ZNY 将新标签添加到文章
article.tags.add(basetag) #ZNY 将基础标签添加到文章
article.save() #ZNY 保存文章
from djangoblog.utils import cache #ZNY 导入缓存工具
cache.clear() #ZNY 清除所有缓存
self.stdout.write(self.style.SUCCESS('created test datas \n')) #ZNY 输出成功信息到标准输出

@ -1,50 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain #ZNY 获取当前站点的域名
class Command(BaseCommand): #ZNY 定义通知百度URL命令类继承BaseCommand
help = 'notify baidu url' #ZNY 命令帮助信息通知百度URL
def add_arguments(self, parser): #ZNY 添加命令行参数方法
parser.add_argument( #ZNY 添加数据类型参数
'data_type', #ZNY 参数名称
type=str, #ZNY 参数类型为字符串
choices=[ #ZNY 参数可选值
'all', #ZNY 所有类型
'article', #ZNY 仅文章
'tag', #ZNY 仅标签
'category'], #ZNY 仅分类
help='article : all article,tag : all tag,category: all category,all: All of these') #ZNY 参数帮助信息
def get_full_url(self, path): #ZNY 获取完整URL方法
url = "https://{site}{path}".format(site=site, path=path) #ZNY 构建完整的HTTPS URL
return url #ZNY 返回完整URL
def handle(self, *args, **options): #ZNY 命令处理主方法
type = options['data_type'] #ZNY 从参数获取数据类型
self.stdout.write('start get %s' % type) #ZNY 输出开始获取数据的提示
urls = [] #ZNY 初始化URL列表
if type == 'article' or type == 'all': #ZNY 如果类型是文章或全部
for article in Article.objects.filter(status='p'): #ZNY 遍历所有已发布的文章
urls.append(article.get_full_url()) #ZNY 将文章完整URL添加到列表
if type == 'tag' or type == 'all': #ZNY 如果类型是标签或全部
for tag in Tag.objects.all(): #ZNY 遍历所有标签
url = tag.get_absolute_url() #ZNY 获取标签的相对URL
urls.append(self.get_full_url(url)) #ZNY 转换为完整URL并添加到列表
if type == 'category' or type == 'all': #ZNY 如果类型是分类或全部
for category in Category.objects.all(): #ZNY 遍历所有分类
url = category.get_absolute_url() #ZNY 获取分类的相对URL
urls.append(self.get_full_url(url)) #ZNY 转换为完整URL并添加到列表
self.stdout.write( #ZNY 输出开始通知的提示
self.style.SUCCESS( #ZNY 使用成功样式
'start notify %d urls' % #ZNY 显示要通知的URL数量
len(urls)))
SpiderNotify.baidu_notify(urls) #ZNY 调用百度通知功能推送URL
self.stdout.write(self.style.SUCCESS('finish notify')) #ZNY 输出完成通知的提示

@ -1,47 +0,0 @@
import requests
from django.core.management.base import BaseCommand
from django.templatetags.static import static
from djangoblog.utils import save_user_avatar
from oauth.models import OAuthUser
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand): #ZNY 定义同步用户头像命令类继承BaseCommand
help = 'sync user avatar' #ZNY 命令帮助信息:同步用户头像
def test_picture(self, url): #ZNY 测试图片URL是否可访问方法
try:
if requests.get(url, timeout=2).status_code == 200: #ZNY 发送HTTP请求测试图片URL设置2秒超时
return True #ZNY 如果返回200状态码返回True
except: #ZNY 捕获所有异常
pass #ZNY 发生异常时忽略
def handle(self, *args, **options): #ZNY 命令处理主方法
static_url = static("../") #ZNY 获取静态文件基础URL
users = OAuthUser.objects.all() #ZNY 获取所有OAuth用户
self.stdout.write(f'开始同步{len(users)}个用户头像') #ZNY 输出开始同步的提示,显示用户数量
for u in users: #ZNY 遍历所有用户
self.stdout.write(f'开始同步:{u.nickname}') #ZNY 输出开始同步当前用户的提示
url = u.picture #ZNY 获取用户当前头像URL
if url: #ZNY 如果头像URL存在
if url.startswith(static_url): #ZNY 如果头像URL是静态文件路径
if self.test_picture(url): #ZNY 测试静态图片是否可访问
continue #ZNY 如果可访问则跳过当前用户
else: #ZNY 如果静态图片不可访问
if u.metadata: #ZNY 如果用户有元数据
manage = get_manager_by_type(u.type) #ZNY 根据OAuth类型获取对应的管理器
url = manage.get_picture(u.metadata) #ZNY 从元数据中获取头像URL
url = save_user_avatar(url) #ZNY 保存用户头像并返回新的URL
else: #ZNY 如果没有元数据
url = static('blog/img/avatar.png') #ZNY 使用默认头像
else: #ZNY 如果头像URL不是静态文件路径
url = save_user_avatar(url) #ZNY 保存用户头像并返回新的URL
else: #ZNY 如果没有头像URL
url = static('blog/img/avatar.png') #ZNY 使用默认头像
if url: #ZNY 如果最终头像URL存在
self.stdout.write( #ZNY 输出同步完成提示
f'结束同步:{u.nickname}.url:{url}') #ZNY 显示用户名和最终头像URL
u.picture = url #ZNY 更新用户头像URL
u.save() #ZNY 保存用户信息
self.stdout.write('结束同步') #ZNY 输出同步结束提示

@ -1,42 +0,0 @@
import logging #ZNY 导入日志模块
import time #ZNY 导入时间模块
from ipware import get_client_ip #ZNY 导入获取客户端IP的工具
from user_agents import parse #ZNY 导入解析用户代理的工具
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager #ZNY 导入Elasticsearch配置和性能文档管理器
logger = logging.getLogger(__name__) #ZNY 获取当前模块的日志记录器
class OnlineMiddleware(object): #ZNY 定义在线中间件类
def __init__(self, get_response=None): #ZNY 初始化方法
self.get_response = get_response #ZNY 存储get_response函数
super().__init__() #ZNY 调用父类初始化
def __call__(self, request): #ZNY 使实例可调用,处理请求
''' page render time ''' #ZNY 页面渲染时间统计
start_time = time.time() #ZNY 记录请求开始时间
response = self.get_response(request) #ZNY 调用后续中间件和视图处理请求
http_user_agent = request.META.get('HTTP_USER_AGENT', '') #ZNY 获取用户代理字符串
ip, _ = get_client_ip(request) #ZNY 获取客户端IP地址
user_agent = parse(http_user_agent) #ZNY 解析用户代理信息
if not response.streaming: #ZNY 如果不是流式响应
try:
cast_time = time.time() - start_time #ZNY 计算总处理时间
if ELASTICSEARCH_ENABLED: #ZNY 如果Elasticsearch已启用
time_taken = round((cast_time) * 1000, 2) #ZNY 转换为毫秒并保留两位小数
url = request.path #ZNY 获取请求路径
from django.utils import timezone #ZNY 导入时区工具
ElaspedTimeDocumentManager.create( #ZNY 创建性能记录文档
url=url, #ZNY 请求URL
time_taken=time_taken, #ZNY 耗时
log_datetime=timezone.now(), #ZNY 当前时间
useragent=user_agent, #ZNY 用户代理信息
ip=ip) #ZNY IP地址
response.content = response.content.replace( #ZNY 在响应内容中替换占位符
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) #ZNY 将加载时间插入到指定位置
except Exception as e: #ZNY 捕获异常
logger.error("Error OnlineMiddleware: %s" % e) #ZNY 记录错误日志
return response #ZNY 返回响应对象

@ -1,198 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
# 标识这是初始迁移文件(首次创建模型时使用)
initial = True
# 迁移依赖依赖于Django的用户模型确保用户模型先创建
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 迁移操作列表:定义要创建的数据库模型
operations = [
# 创建网站配置模型(存储网站全局设置)
migrations.CreateModel(
name='BlogSettings',
fields=[
# 自增主键ID
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 网站名称默认空字符串最大长度200
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
# 网站描述默认空字符串最大长度1000
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
# 网站SEO描述用于搜索引擎优化
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键字(用于搜索引擎收录)
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
# 文章摘要长度默认300字
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
# 侧边栏显示的文章数量默认10篇
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
# 侧边栏显示的评论数量默认5条
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
# 文章页默认显示的评论数量默认5条
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
# 是否显示谷歌广告(默认不显示)
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 谷歌广告代码(可空,默认空字符串)
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
# 是否开启网站评论功能(默认开启)
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 网站备案号(可空)
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
# 网站统计代码如百度统计、Google Analytics
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案号(默认不显示)
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
# 公安备案号内容(可空)
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
# 在Admin后台显示的名称单数
'verbose_name': '网站配置',
# 在Admin后台显示的名称复数
'verbose_name_plural': '网站配置',
},
),
# 创建友情链接模型
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称(唯一,不可重复)
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址URL格式验证
('link', models.URLField(verbose_name='链接地址')),
# 排序序号(唯一,用于控制显示顺序)
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否显示该链接(默认显示)
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 显示位置(首页/列表页/文章页/全站/友情链接页)
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
# 创建时间(默认当前时间)
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间(默认当前时间)
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
# 按排序序号升序排列
'ordering': ['sequence'],
},
),
# 创建侧边栏模型
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 侧边栏标题
('name', models.CharField(max_length=100, verbose_name='标题')),
# 侧边栏内容支持HTML等富文本
('content', models.TextField(verbose_name='内容')),
# 排序序号(唯一,控制显示顺序)
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用该侧边栏(默认启用)
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
# 按排序序号升序排列
'ordering': ['sequence'],
},
),
# 创建标签模型
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 标签名称(唯一)
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
# URL别名用于生成友好URL默认'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
# 按标签名称升序排列
'ordering': ['name'],
},
),
# 创建分类模型(支持多级分类)
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 分类名称(唯一)
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
# URL别名
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 排序权重(越大越靠前)
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
# 父级分类(自关联,支持多级分类,可为空)
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
# 按权重降序排列(权重高的靠前)
'ordering': ['-index'],
},
),
# 创建文章模型(核心内容模型)
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 文章标题(唯一)
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 文章正文支持Markdown格式
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间(默认当前时间)
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
# 文章状态(草稿/发表)
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
# 评论状态(打开/关闭)
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
# 内容类型(文章/页面)
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量默认0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 排序序号(数字越大越靠前)
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示目录(默认不显示)
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
# 关联作者(外键关联用户模型,级联删除)
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 关联分类(外键关联分类模型,级联删除)
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
# 关联标签(多对多关系,一篇文章可多个标签,可为空)
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
# 排序规则:先按排序序号降序,再按发布时间降序
'ordering': ['-article_order', '-pub_time'],
# 按ID获取最新记录
'get_latest_by': 'id',
},
),
]

@ -1,22 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'), #ZNY 依赖于blog应用的初始迁移文件0001_initial
]
operations = [
migrations.AddField(
model_name='blogsettings', #ZNY 向BlogSettings模型添加字段
name='global_footer', #ZNY 字段名称为global_footer
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), #ZNY 公共尾部字段,用于存储全站公共底部内容
),
migrations.AddField(
model_name='blogsettings', #ZNY 向BlogSettings模型添加字段
name='global_header', #ZNY 字段名称为global_header
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), #ZNY 公共头部字段,用于存储全站公共头部内容
),
]

@ -1,17 +0,0 @@
# 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'), #ZNY 依赖于blog应用的第二个迁移文件0002
]
operations = [
migrations.AddField(
model_name='blogsettings', #ZNY 向BlogSettings模型添加新字段
name='comment_need_review', #ZNY 字段名称为comment_need_review
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), #ZNY 评论审核开关字段,控制评论是否需要审核才能显示
),
]

@ -1,27 +0,0 @@
# 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'), #ZNY 依赖于blog应用的第三个迁移文件0003
]
operations = [
migrations.RenameField(
model_name='blogsettings', #ZNY 指定要修改的模型为BlogSettings
old_name='analyticscode', #ZNY 原字段名称为analyticscode
new_name='analytics_code', #ZNY 新字段名称为analytics_code修改为更规范的命名
),
migrations.RenameField(
model_name='blogsettings', #ZNY 指定要修改的模型为BlogSettings
old_name='beiancode', #ZNY 原字段名称为beiancode
new_name='beian_code', #ZNY 新字段名称为beian_code修改为更规范的命名
),
migrations.RenameField(
model_name='blogsettings', #ZNY 指定要修改的模型为BlogSettings
old_name='sitename', #ZNY 原字段名称为sitename
new_name='site_name', #ZNY 新字段名称为site_name修改为更规范的命名
),
]

@ -1,300 +0,0 @@
# 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), #ZNY 依赖Django用户模型
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), #ZNY 依赖于blog应用的第四个迁移文件
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, #ZNY 修改Article模型的选项将中文改为英文
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, #ZNY 修改Category模型的选项将中文改为英文
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, #ZNY 修改Links模型的选项将中文改为英文
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, #ZNY 修改Sidebar模型的选项将中文改为英文
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, #ZNY 修改Tag模型的选项将中文改为英文
),
migrations.RemoveField(
model_name='article',
name='created_time', #ZNY 删除Article模型的created_time字段
),
migrations.RemoveField(
model_name='article',
name='last_mod_time', #ZNY 删除Article模型的last_mod_time字段
),
migrations.RemoveField(
model_name='category',
name='created_time', #ZNY 删除Category模型的created_time字段
),
migrations.RemoveField(
model_name='category',
name='last_mod_time', #ZNY 删除Category模型的last_mod_time字段
),
migrations.RemoveField(
model_name='links',
name='created_time', #ZNY 删除Links模型的created_time字段
),
migrations.RemoveField(
model_name='sidebar',
name='created_time', #ZNY 删除Sidebar模型的created_time字段
),
migrations.RemoveField(
model_name='tag',
name='created_time', #ZNY 删除Tag模型的created_time字段
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time', #ZNY 删除Tag模型的last_mod_time字段
),
migrations.AddField(
model_name='article',
name='creation_time', #ZNY 向Article模型添加creation_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), #ZNY 创建时间字段,英文标签
),
migrations.AddField(
model_name='article',
name='last_modify_time', #ZNY 向Article模型添加last_modify_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), #ZNY 最后修改时间字段,英文标签
),
migrations.AddField(
model_name='category',
name='creation_time', #ZNY 向Category模型添加creation_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), #ZNY 创建时间字段,英文标签
),
migrations.AddField(
model_name='category',
name='last_modify_time', #ZNY 向Category模型添加last_modify_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), #ZNY 最后修改时间字段,英文标签
),
migrations.AddField(
model_name='links',
name='creation_time', #ZNY 向Links模型添加creation_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), #ZNY 创建时间字段,英文标签
),
migrations.AddField(
model_name='sidebar',
name='creation_time', #ZNY 向Sidebar模型添加creation_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), #ZNY 创建时间字段,英文标签
),
migrations.AddField(
model_name='tag',
name='creation_time', #ZNY 向Tag模型添加creation_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), #ZNY 创建时间字段,英文标签
),
migrations.AddField(
model_name='tag',
name='last_modify_time', #ZNY 向Tag模型添加last_modify_time字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), #ZNY 最后修改时间字段,英文标签
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'), #ZNY 修改Article模型的article_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'), #ZNY 修改Article模型的author字段标签为英文
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'), #ZNY 修改Article模型的body字段标签为英文
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), #ZNY 修改Article模型的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'), #ZNY 修改Article模型的comment_status字段选项和标签为英文
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), #ZNY 修改Article模型的pub_time字段标签为英文
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'), #ZNY 修改Article模型的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'), #ZNY 修改Article模型的status字段选项和标签为英文
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), #ZNY 修改Article模型的tags字段标签为英文
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'), #ZNY 修改Article模型的title字段标签为英文
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), #ZNY 修改Article模型的type字段选项和标签为英文
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'), #ZNY 修改Article模型的views字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'), #ZNY 修改BlogSettings模型的article_comment_count字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'), #ZNY 修改BlogSettings模型的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'), #ZNY 修改BlogSettings模型的google_adsense_codes字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'), #ZNY 修改BlogSettings模型的open_site_comment字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'), #ZNY 修改BlogSettings模型的show_google_adsense字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'), #ZNY 修改BlogSettings模型的sidebar_article_count字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'), #ZNY 修改BlogSettings模型的sidebar_comment_count字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'), #ZNY 修改BlogSettings模型的site_description字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), #ZNY 修改BlogSettings模型的site_keywords字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'), #ZNY 修改BlogSettings模型的site_name字段标签为英文
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), #ZNY 修改BlogSettings模型的site_seo_description字段标签为英文
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'), #ZNY 修改Category模型的index字段标签为英文
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'), #ZNY 修改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'), #ZNY 修改Category模型的parent_category字段标签为英文
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'), #ZNY 修改Links模型的is_enable字段标签为英文
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), #ZNY 修改Links模型的last_mod_time字段标签为英文
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'), #ZNY 修改Links模型的link字段标签为英文
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'), #ZNY 修改Links模型的name字段标签为英文
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'), #ZNY 修改Links模型的sequence字段标签为英文
),
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'), #ZNY 修改Links模型的show_type字段选项和标签为英文
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'), #ZNY 修改Sidebar模型的content字段标签为英文
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'), #ZNY 修改Sidebar模型的is_enable字段标签为英文
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), #ZNY 修改Sidebar模型的last_mod_time字段标签为英文
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'), #ZNY 修改Sidebar模型的name字段标签为英文
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'), #ZNY 修改Sidebar模型的sequence字段标签为英文
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), #ZNY 修改Tag模型的name字段标签为英文
),
]

@ -1,17 +0,0 @@
# 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'), #ZNY 依赖于blog应用的第五个迁移文件0005
]
operations = [
migrations.AlterModelOptions(
name='blogsettings', #ZNY 修改BlogSettings模型的选项配置
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, #ZNY 将BlogSettings模型的显示名称从中文改为英文
),
]

@ -1,23 +0,0 @@
# Generated by Django 5.2.4 on 2025-11-20 20:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
]
operations = [
migrations.AddField(
model_name='article',
name='dislikes',
field=models.PositiveIntegerField(default=0, verbose_name='不喜欢数'),
),
migrations.AddField(
model_name='article',
name='likes',
field=models.PositiveIntegerField(default=0, verbose_name='点赞数'),
),
]

@ -1,408 +0,0 @@
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
from django.contrib.auth import get_user_model
from django.db.models import JSONField
from django.contrib.auth.models import User
logger = logging.getLogger(__name__) #ZNY 获取当前模块的日志记录器
class LinkShowType(models.TextChoices): #ZNY 定义链接显示类型枚举类
I = ('i', _('index')) #ZNY 首页显示
L = ('l', _('list')) #ZNY 列表页显示
P = ('p', _('post')) #ZNY 文章页面显示
A = ('a', _('all')) #ZNY 全站显示
S = ('s', _('slide')) #ZNY 友情链接页面显示
class BaseModel(models.Model): #ZNY 定义基础模型抽象类
id = models.AutoField(primary_key=True) #ZNY 自增主键字段
creation_time = models.DateTimeField(_('creation time'), default=now) #ZNY 创建时间字段
last_modify_time = models.DateTimeField(_('modify time'), default=now) #ZNY 最后修改时间字段
def save(self, *args, **kwargs): #ZNY 重写保存方法
is_update_views = isinstance( #ZNY 检查是否是更新文章浏览量的操作
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views: #ZNY 如果是更新浏览量
Article.objects.filter(pk=self.pk).update(views=self.views) #ZNY 直接更新浏览量字段
else: #ZNY 如果不是更新浏览量
if 'slug' in self.__dict__: #ZNY 如果模型有slug字段
slug = getattr( #ZNY 获取标题或名称作为slug源
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug)) #ZNY 生成并设置slug字段
super().save(*args, **kwargs) #ZNY 调用父类保存方法
def get_full_url(self): #ZNY 获取完整URL方法
site = get_current_site().domain #ZNY 获取当前站点域名
url = "https://{site}{path}".format(site=site, #ZNY 构建完整URL
path=self.get_absolute_url())
return url #ZNY 返回完整URL
class Meta:
abstract = True #ZNY 设置为抽象基类,不创建数据库表
@abstractmethod #ZNY 抽象方法,子类必须实现
def get_absolute_url(self): #ZNY 获取绝对URL方法
pass #ZNY 由子类具体实现
class Article(BaseModel): #ZNY 定义文章模型,继承基础模型
"""文章""" #ZNY 文章模型文档字符串
#ZNY 文章状态选项
STATUS_CHOICES = (
('d', _('Draft')), #ZNY 草稿状态
('p', _('Published')), #ZNY 发布状态
)
COMMENT_STATUS = ( #ZNY 评论状态选项
('o', _('Open')), #ZNY 开放评论
('c', _('Close')), #ZNY 关闭评论
)
TYPE = ( #ZNY 文章类型选项
('a', _('Article')), #ZNY 普通文章
('p', _('Page')), #ZNY 页面类型
)
# --------------- 原来 #ZNY 的字段 ---------------
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(_('publish time'), blank=False, null=False, default=now)
status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p')
comment_status = models.CharField(_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, on_delete=models.CASCADE)
article_order = models.IntegerField(_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey('Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# --------------- #Zxy 新增字段 ---------------
likes = models.PositiveIntegerField(default=0, verbose_name=_("点赞数"))
dislikes = models.PositiveIntegerField(default=0, verbose_name=_("不喜欢数"))
# --------------- 原来 #ZNY 的方法全部保留 ---------------
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def next_article(self):
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
def prev_article(self):
return Article.objects.filter(id__lt=self.id, status='p').first()
# --------------- #Zxy 新增方法 ---------------
def vote(self, user, like=True):
user_id = str(user.id)
if user_id in self.vote_record:
return False
self.vote_record[user_id] = 'like' if like else 'dislike'
if like:
self.likes += 1
else:
self.dislikes += 1
self.save(update_fields=['likes', 'dislikes', 'vote_record'])
return True
class Meta:
ordering = ['-article_order', '-pub_time'] #ZNY 默认按排序和发布时间降序排列
verbose_name = _('article') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
get_latest_by = 'id' #ZNY 获取最新记录的依据字段
def get_absolute_url(self): #ZNY 实现获取绝对URL方法
return reverse('blog:detailbyid', kwargs={ #ZNY 生成文章详情页URL
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10) #ZNY 缓存装饰器缓存10小时
def get_category_tree(self): #ZNY 获取分类树方法
tree = self.category.get_category_tree() #ZNY 获取分类的层级树
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) #ZNY 转换为名称和URL的元组列表
return names #ZNY 返回分类树列表
def save(self, *args, **kwargs): #ZNY 重写保存方法
super().save(*args, **kwargs) #ZNY 调用父类保存方法
def viewed(self): #ZNY 增加浏览量方法
self.views += 1 #ZNY 浏览量加1
self.save(update_fields=['views']) #ZNY 只更新浏览量字段
def comment_list(self): #ZNY 获取评论列表方法
cache_key = 'article_comments_{id}'.format(id=self.id) #ZNY 生成评论缓存键
value = cache.get(cache_key) #ZNY 尝试从缓存获取评论
if value: #ZNY 如果缓存存在
logger.info('get article comments:{id}'.format(id=self.id)) #ZNY 记录获取缓存日志
return value #ZNY 返回缓存评论
else: #ZNY 如果缓存不存在
comments = self.comment_set.filter(is_enable=True).order_by('-id') #ZNY 从数据库获取启用的评论
cache.set(cache_key, comments, 60 * 100) #ZNY 设置缓存有效期100分钟
logger.info('set article comments:{id}'.format(id=self.id)) #ZNY 记录设置缓存日志
return comments #ZNY 返回评论列表
def get_admin_url(self): #ZNY 获取管理后台URL方法
info = (self._meta.app_label, self._meta.model_name) #ZNY 获取应用和模型名称
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) #ZNY 生成管理后台编辑URL
@cache_decorator(expiration=60 * 100) #ZNY 缓存装饰器缓存100分钟
def next_article(self): #ZNY 获取下一篇文章方法
# 下一篇
return Article.objects.filter( #ZNY 查询ID大于当前文章且已发布的文章
id__gt=self.id, status='p').order_by('id').first() #ZNY 按ID升序取第一篇
@cache_decorator(expiration=60 * 100) #ZNY 缓存装饰器缓存100分钟
def prev_article(self): #ZNY 获取上一篇文章方法
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first() #ZNY 查询ID小于当前文章且已发布的文章
def get_first_image_url(self): #ZNY 获取文章首张图片URL方法
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) #ZNY 使用正则匹配Markdown图片语法
if match: #ZNY 如果匹配到图片
return match.group(1) #ZNY 返回图片URL
return "" #ZNY 没有图片返回空字符串
class Category(BaseModel): #ZNY 定义分类模型,继承基础模型
"""文章分类""" #ZNY 分类模型文档字符串
name = models.CharField(_('category name'), max_length=30, unique=True) #ZNY 分类名称,唯一
parent_category = models.ForeignKey( #ZNY 父级分类自关联字段
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE) #ZNY 支持多级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) #ZNY URL友好的分类名
index = models.IntegerField(default=0, verbose_name=_('index')) #ZNY 排序索引,越大越靠前
class Meta:
ordering = ['-index'] #ZNY 按索引降序排列
verbose_name = _('category') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
def get_absolute_url(self): #ZNY 实现获取绝对URL方法
return reverse( #ZNY 生成分类详情页URL
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self): #ZNY 字符串表示方法
return self.name #ZNY 返回分类名称
@cache_decorator(60 * 60 * 10) #ZNY 缓存装饰器缓存10小时
def get_category_tree(self): #ZNY 获取分类树方法
"""
递归获得分类目录的父级
:return:
"""
categorys = [] #ZNY 初始化分类列表
def parse(category): #ZNY 递归解析函数
categorys.append(category) #ZNY 添加当前分类到列表
if category.parent_category: #ZNY 如果存在父级分类
parse(category.parent_category) #ZNY 递归解析父级分类
parse(self) #ZNY 从当前分类开始解析
return categorys #ZNY 返回分类树列表
@cache_decorator(60 * 60 * 10) #ZNY 缓存装饰器缓存10小时
def get_sub_categorys(self): #ZNY 获取所有子分类方法
"""
获得当前分类目录所有子集
:return:
"""
categorys = [] #ZNY 初始化子分类列表
all_categorys = Category.objects.all() #ZNY 获取所有分类
def parse(category): #ZNY 递归解析函数
if category not in categorys: #ZNY 如果分类不在列表中
categorys.append(category) #ZNY 添加当前分类
childs = all_categorys.filter(parent_category=category) #ZNY 获取直接子分类
for child in childs: #ZNY 遍历子分类
if category not in categorys: #ZNY 如果子分类不在列表中
categorys.append(child) #ZNY 添加子分类
parse(child) #ZNY 递归解析子分类的子分类
parse(self) #ZNY 从当前分类开始解析
return categorys #ZNY 返回所有子分类列表
class Tag(BaseModel): #ZNY 定义标签模型,继承基础模型
"""文章标签""" #ZNY 标签模型文档字符串
name = models.CharField(_('tag name'), max_length=30, unique=True) #ZNY 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) #ZNY URL友好的标签名
def __str__(self): #ZNY 字符串表示方法
return self.name #ZNY 返回标签名称
def get_absolute_url(self): #ZNY 实现获取绝对URL方法
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) #ZNY 生成标签详情页URL
@cache_decorator(60 * 60 * 10) #ZNY 缓存装饰器缓存10小时
def get_article_count(self): #ZNY 获取标签下文章数量方法
return Article.objects.filter(tags__name=self.name).distinct().count() #ZNY 统计使用该标签的文章数量
class Meta:
ordering = ['name'] #ZNY 按名称升序排列
verbose_name = _('tag') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
class Links(models.Model): #ZNY 定义友情链接模型
"""友情链接""" #ZNY 友情链接模型文档字符串
name = models.CharField(_('link name'), max_length=30, unique=True) #ZNY 链接名称,唯一
link = models.URLField(_('link')) #ZNY 链接地址
sequence = models.IntegerField(_('order'), unique=True) #ZNY 排序序号,唯一
is_enable = models.BooleanField( #ZNY 是否启用显示字段
_('is show'), default=True, blank=False, null=False) #ZNY 默认启用
show_type = models.CharField( #ZNY 显示类型字段
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I) #ZNY 默认在首页显示
creation_time = models.DateTimeField(_('creation time'), default=now) #ZNY 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #ZNY 最后修改时间
class Meta:
ordering = ['sequence'] #ZNY 按排序序号升序排列
verbose_name = _('link') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
def __str__(self): #ZNY 字符串表示方法
return self.name #ZNY 返回链接名称
class SideBar(models.Model): #ZNY 定义侧边栏模型
"""侧边栏,可以展示一些html内容""" #ZNY 侧边栏模型文档字符串
name = models.CharField(_('title'), max_length=100) #ZNY 侧边栏标题
content = models.TextField(_('content')) #ZNY 侧边栏内容
sequence = models.IntegerField(_('order'), unique=True) #ZNY 排序序号,唯一
is_enable = models.BooleanField(_('is enable'), default=True) #ZNY 是否启用,默认启用
creation_time = models.DateTimeField(_('creation time'), default=now) #ZNY 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #ZNY 最后修改时间
class Meta:
ordering = ['sequence'] #ZNY 按排序序号升序排列
verbose_name = _('sidebar') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
def __str__(self): #ZNY 字符串表示方法
return self.name #ZNY 返回侧边栏名称
class BlogSettings(models.Model): #ZNY 定义博客设置模型
"""blog的配置""" #ZNY 博客设置模型文档字符串
site_name = models.CharField( #ZNY 网站名称字段
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField( #ZNY 网站描述字段
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField( #ZNY 网站SEO描述字段
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField( #ZNY 网站关键词字段
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300) #ZNY 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) #ZNY 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) #ZNY 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) #ZNY 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) #ZNY 是否显示谷歌广告
google_adsense_codes = models.TextField( #ZNY 谷歌广告代码字段
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True) #ZNY 是否开启全站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') #ZNY 全局头部内容
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') #ZNY 全局尾部内容
beian_code = models.CharField( #ZNY ICP备案号字段
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField( #ZNY 网站统计代码字段
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField( #ZNY 是否显示公安备案号字段
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField( #ZNY 公安备案号字段
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField( #ZNY 评论是否需要审核字段
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration') #ZNY 单数显示名称
verbose_name_plural = verbose_name #ZNY 复数显示名称
def __str__(self): #ZNY 字符串表示方法
return self.site_name #ZNY 返回网站名称
def clean(self): #ZNY 数据清洗验证方法
if BlogSettings.objects.exclude(id=self.id).count(): #ZNY 检查是否已存在其他配置
raise ValidationError(_('There can only be one configuration')) #ZNY 只能有一个配置,抛出验证错误
def save(self, *args, **kwargs): #ZNY 重写保存方法
super().save(*args, **kwargs) #ZNY 调用父类保存方法
from djangoblog.utils import cache #ZNY 导入缓存工具
cache.clear() #ZNY 保存配置后清除所有缓存

@ -1,14 +0,0 @@
from haystack import indexes #ZNY 导入Haystack搜索索引模块
from blog.models import Article #ZNY 导入文章模型
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): #ZNY 定义文章搜索索引类,继承搜索索引和可索引接口
text = indexes.CharField(document=True, use_template=True) #ZNY 定义主搜索字段,使用模板构建索引内容
def get_model(self): #ZNY 获取索引对应的模型方法
return Article #ZNY 返回文章模型类
def index_queryset(self, using=None): #ZNY 定义索引查询集方法
return self.get_model().objects.filter(status='p') #ZNY 只索引已发布状态的文章

@ -1,344 +0,0 @@
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__) #ZNY 获取当前模块的日志记录器
register = template.Library() #ZNY 创建Django模板标签库实例
@register.simple_tag(takes_context=True) #ZNY 注册简单模板标签,接收模板上下文
def head_meta(context): #ZNY 生成头部meta信息的模板标签
return mark_safe(hooks.apply_filters('head_meta', '', context)) #ZNY 应用过滤器并返回安全的HTML字符串
@register.simple_tag #ZNY 注册简单模板标签
def timeformat(data): #ZNY 时间格式化模板标签
try:
return data.strftime(settings.TIME_FORMAT) #ZNY 按照设置的时间格式格式化时间
except Exception as e:
logger.error(e) #ZNY 记录错误日志
return "" #ZNY 出错时返回空字符串
@register.simple_tag #ZNY 注册简单模板标签
def datetimeformat(data): #ZNY 日期时间格式化模板标签
try:
return data.strftime(settings.DATE_TIME_FORMAT) #ZNY 按照设置的日期时间格式格式化
except Exception as e:
logger.error(e) #ZNY 记录错误日志
return "" #ZNY 出错时返回空字符串
@register.filter() #ZNY 注册模板过滤器
@stringfilter #ZNY 确保输入被转换为字符串
def custom_markdown(content): #ZNY 自定义Markdown转换过滤器
return mark_safe(CommonMarkdown.get_markdown(content)) #ZNY 将Markdown内容转换为HTML并标记为安全
@register.simple_tag #ZNY 注册简单模板标签
def get_markdown_toc(content): #ZNY 获取Markdown目录模板标签
from djangoblog.utils import CommonMarkdown #ZNY 导入Markdown工具类
body, toc = CommonMarkdown.get_markdown_with_toc(content) #ZNY 获取带目录的Markdown内容
return mark_safe(toc) #ZNY 返回目录HTML并标记为安全
@register.filter() #ZNY 注册模板过滤器
@stringfilter #ZNY 确保输入被转换为字符串
def comment_markdown(content): #ZNY 评论Markdown转换过滤器
content = CommonMarkdown.get_markdown(content) #ZNY 将Markdown转换为HTML
return mark_safe(sanitize_html(content)) #ZNY 对HTML进行安全过滤后返回
@register.filter(is_safe=True) #ZNY 注册安全模板过滤器
@stringfilter #ZNY 确保输入被转换为字符串
def truncatechars_content(content): #ZNY 截断文章内容过滤器
"""
获得文章内容的摘要
:param content:
:return:
"""
from django.template.defaultfilters import truncatechars_html #ZNY 导入HTML截断函数
from djangoblog.utils import get_blog_setting #ZNY 导入博客设置获取函数
blogsetting = get_blog_setting() #ZNY 获取博客设置
return truncatechars_html(content, blogsetting.article_sub_length) #ZNY 按设置的文章摘要长度截断HTML内容
@register.filter(is_safe=True) #ZNY 注册安全模板过滤器
@stringfilter #ZNY 确保输入被转换为字符串
def truncate(content): #ZNY 简单截断过滤器
from django.utils.html import strip_tags #ZNY 导入HTML标签去除函数
return strip_tags(content)[:150] #ZNY 去除HTML标签后截取前150个字符
@register.inclusion_tag('blog/tags/breadcrumb.html') #ZNY 注册包含标签,指定模板
def load_breadcrumb(article): #ZNY 加载面包屑导航标签
"""
获得文章面包屑
:param article:
:return:
"""
names = article.get_category_tree() #ZNY 获取文章分类树
from djangoblog.utils import get_blog_setting #ZNY 导入博客设置获取函数
blogsetting = get_blog_setting() #ZNY 获取博客设置
site = get_current_site().domain #ZNY 获取当前站点域名
names.append((blogsetting.site_name, '/')) #ZNY 添加首页链接
names = names[::-1] #ZNY 反转列表顺序
return {
'names': names, #ZNY 面包屑名称和URL列表
'title': article.title, #ZNY 文章标题
'count': len(names) + 1 #ZNY 面包屑数量
}
@register.inclusion_tag('blog/tags/article_tag_list.html') #ZNY 注册包含标签,指定模板
def load_articletags(article): #ZNY 加载文章标签列表标签
"""
文章标签
:param article:
:return:
"""
tags = article.tags.all() #ZNY 获取文章的所有标签
tags_list = [] #ZNY 初始化标签列表
for tag in tags: #ZNY 遍历每个标签
url = tag.get_absolute_url() #ZNY 获取标签URL
count = tag.get_article_count() #ZNY 获取标签下文章数量
tags_list.append(( #ZNY 添加标签信息到列表
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) #ZNY URL、数量、标签对象、随机颜色
))
return {
'article_tags_list': tags_list #ZNY 返回标签列表
}
@register.inclusion_tag('blog/tags/sidebar.html') #ZNY 注册包含标签,指定模板
def load_sidebar(user, linktype): #ZNY 加载侧边栏标签
"""
加载侧边栏
:return:
"""
value = cache.get("sidebar" + linktype) #ZNY 尝试从缓存获取侧边栏数据
if value: #ZNY 如果缓存存在
value['user'] = user #ZNY 添加用户信息
return value #ZNY 返回缓存数据
else: #ZNY 如果缓存不存在
logger.info('load sidebar') #ZNY 记录加载侧边栏日志
from djangoblog.utils import get_blog_setting #ZNY 导入博客设置获取函数
blogsetting = get_blog_setting() #ZNY 获取博客设置
recent_articles = Article.objects.filter( #ZNY 获取最近文章
status='p')[:blogsetting.sidebar_article_count] #ZNY 只取已发布文章,按设置数量限制
sidebar_categorys = Category.objects.all() #ZNY 获取所有分类
extra_sidebars = SideBar.objects.filter( #ZNY 获取额外侧边栏
is_enable=True).order_by('sequence') #ZNY 只取启用的,按顺序排序
most_read_articles = Article.objects.filter(status='p').order_by( #ZNY 获取最多阅读文章
'-views')[:blogsetting.sidebar_article_count] #ZNY 按浏览量降序排列,按设置数量限制
dates = Article.objects.datetimes('creation_time', 'month', order='DESC') #ZNY 获取文章按月归档日期
links = Links.objects.filter(is_enable=True).filter( #ZNY 获取友情链接
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) #ZNY 按显示类型过滤
commment_list = Comment.objects.filter(is_enable=True).order_by( #ZNY 获取最新评论
'-id')[:blogsetting.sidebar_comment_count] #ZNY 按ID降序排列按设置数量限制
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5 #ZNY 字体大小增量
tags = Tag.objects.all() #ZNY 获取所有标签
sidebar_tags = None #ZNY 初始化侧边栏标签
if tags and len(tags) > 0: #ZNY 如果有标签
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] #ZNY 生成标签和文章数量列表
count = sum([t[1] for t in s]) #ZNY 计算总文章数
dd = 1 if (count == 0 or not len(tags)) else count / len(tags) #ZNY 计算平均值
import random #ZNY 导入随机模块
sidebar_tags = list( #ZNY 生成侧边栏标签列表
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) #ZNY 计算每个标签的字体大小
random.shuffle(sidebar_tags) #ZNY 随机打乱标签顺序
value = { #ZNY 构建侧边栏数据字典
'recent_articles': recent_articles, #ZNY 最近文章
'sidebar_categorys': sidebar_categorys, #ZNY 分类列表
'most_read_articles': most_read_articles, #ZNY 最多阅读文章
'article_dates': dates, #ZNY 文章归档日期
'sidebar_comments': commment_list, #ZNY 侧边栏评论
'sidabar_links': links, #ZNY 侧边栏链接
'show_google_adsense': blogsetting.show_google_adsense, #ZNY 是否显示谷歌广告
'google_adsense_codes': blogsetting.google_adsense_codes, #ZNY 谷歌广告代码
'open_site_comment': blogsetting.open_site_comment, #ZNY 是否开启全站评论
'show_gongan_code': blogsetting.show_gongan_code, #ZNY 是否显示公安备案
'sidebar_tags': sidebar_tags, #ZNY 侧边栏标签云
'extra_sidebars': extra_sidebars #ZNY 额外侧边栏
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) #ZNY 将侧边栏数据存入缓存有效期3小时
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) #ZNY 记录缓存设置日志
value['user'] = user #ZNY 添加用户信息
return value #ZNY 返回侧边栏数据
@register.inclusion_tag('blog/tags/article_meta_info.html') #ZNY 注册包含标签,指定模板
def load_article_metas(article, user): #ZNY 加载文章meta信息标签
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article, #ZNY 文章对象
'user': user #ZNY 用户对象
}
@register.inclusion_tag('blog/tags/article_pagination.html') #ZNY 注册包含标签,指定模板
def load_pagination_info(page_obj, page_type, tag_name): #ZNY 加载分页信息标签
previous_url = '' #ZNY 初始化上一页URL
next_url = '' #ZNY 初始化下一页URL
if page_type == '': #ZNY 如果是首页分页
if page_obj.has_next(): #ZNY 如果有下一页
next_number = page_obj.next_page_number() #ZNY 获取下一页页码
next_url = reverse('blog:index_page', kwargs={'page': next_number}) #ZNY 生成下一页URL
if page_obj.has_previous(): #ZNY 如果有上一页
previous_number = page_obj.previous_page_number() #ZNY 获取上一页页码
previous_url = reverse( #ZNY 生成上一页URL
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档': #ZNY 如果是标签归档分页
tag = get_object_or_404(Tag, name=tag_name) #ZNY 获取标签对象
if page_obj.has_next(): #ZNY 如果有下一页
next_number = page_obj.next_page_number() #ZNY 获取下一页页码
next_url = reverse( #ZNY 生成标签下一页URL
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous(): #ZNY 如果有上一页
previous_number = page_obj.previous_page_number() #ZNY 获取上一页页码
previous_url = reverse( #ZNY 生成标签上一页URL
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档': #ZNY 如果是作者归档分页
if page_obj.has_next(): #ZNY 如果有下一页
next_number = page_obj.next_page_number() #ZNY 获取下一页页码
next_url = reverse( #ZNY 生成作者下一页URL
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous(): #ZNY 如果有上一页
previous_number = page_obj.previous_page_number() #ZNY 获取上一页页码
previous_url = reverse( #ZNY 生成作者上一页URL
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档': #ZNY 如果是分类归档分页
category = get_object_or_404(Category, name=tag_name) #ZNY 获取分类对象
if page_obj.has_next(): #ZNY 如果有下一页
next_number = page_obj.next_page_number() #ZNY 获取下一页页码
next_url = reverse( #ZNY 生成分类下一页URL
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous(): #ZNY 如果有上一页
previous_number = page_obj.previous_page_number() #ZNY 获取上一页页码
previous_url = reverse( #ZNY 生成分类上一页URL
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url, #ZNY 上一页URL
'next_url': next_url, #ZNY 下一页URL
'page_obj': page_obj #ZNY 分页对象
}
@register.inclusion_tag('blog/tags/article_info.html') #ZNY 注册包含标签,指定模板
def load_article_detail(article, isindex, user): #ZNY 加载文章详情标签
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting #ZNY 导入博客设置获取函数
blogsetting = get_blog_setting() #ZNY 获取博客设置
return {
'article': article, #ZNY 文章对象
'isindex': isindex, #ZNY 是否是列表页
'user': user, #ZNY 用户对象
'open_site_comment': blogsetting.open_site_comment, #ZNY 是否开启全站评论
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter #ZNY 注册模板过滤器
def gravatar_url(email, size=40): #ZNY 获取Gravatar头像URL过滤器
"""获得gravatar头像"""
cachekey = 'gravatat/' + email #ZNY 生成缓存键
url = cache.get(cachekey) #ZNY 尝试从缓存获取头像URL
if url: #ZNY 如果缓存存在
return url #ZNY 返回缓存URL
else: #ZNY 如果缓存不存在
usermodels = OAuthUser.objects.filter(email=email) #ZNY 查询OAuth用户
if usermodels: #ZNY 如果找到用户
o = list(filter(lambda x: x.picture is not None, usermodels)) #ZNY 过滤有头像的用户
if o: #ZNY 如果有头像的用户存在
return o[0].picture #ZNY 返回用户自定义头像
email = email.encode('utf-8') #ZNY 编码邮箱
default = static('blog/img/avatar.png') #ZNY 设置默认头像路径
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5( #ZNY 生成Gravatar URL
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) #ZNY 使用MD5哈希和URL参数
cache.set(cachekey, url, 60 * 60 * 10) #ZNY 将URL存入缓存有效期10小时
logger.info('set gravatar cache.key:{key}'.format(key=cachekey)) #ZNY 记录缓存设置日志
return url #ZNY 返回头像URL
@register.filter #ZNY 注册模板过滤器
def gravatar(email, size=40): #ZNY 获取Gravatar头像HTML过滤器
"""获得gravatar头像"""
url = gravatar_url(email, size) #ZNY 获取头像URL
return mark_safe( #ZNY 返回安全的HTML图像标签
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag #ZNY 注册简单模板标签
def query(qs, **kwargs): #ZNY 查询集过滤模板标签
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs) #ZNY 应用过滤条件并返回查询集
@register.filter #ZNY 注册模板过滤器
def addstr(arg1, arg2): #ZNY 字符串连接过滤器
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2) #ZNY 将两个参数转换为字符串后连接

@ -1,233 +0,0 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase): #ZNY 定义文章测试类继承Django测试用例
def setUp(self): #ZNY 测试初始化方法
self.client = Client() #ZNY 创建测试客户端
self.factory = RequestFactory() #ZNY 创建请求工厂
def test_validate_article(self): #ZNY 测试文章验证方法
site = get_current_site().domain #ZNY 获取当前站点域名
user = BlogUser.objects.get_or_create( #ZNY 获取或创建测试用户
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy") #ZNY 设置用户密码
user.is_staff = True #ZNY 设置为管理员
user.is_superuser = True #ZNY 设置为超级用户
user.save() #ZNY 保存用户
response = self.client.get(user.get_absolute_url()) #ZNY 访问用户详情页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
response = self.client.get('/admin/servermanager/emailsendlog/') #ZNY 访问邮件发送日志管理页
response = self.client.get('admin/admin/logentry/') #ZNY 访问日志条目管理页
s = SideBar() #ZNY 创建侧边栏实例
s.sequence = 1 #ZNY 设置排序序号
s.name = 'test' #ZNY 设置侧边栏名称
s.content = 'test content' #ZNY 设置侧边栏内容
s.is_enable = True #ZNY 启用侧边栏
s.save() #ZNY 保存侧边栏
category = Category() #ZNY 创建分类实例
category.name = "category" #ZNY 设置分类名称
category.creation_time = timezone.now() #ZNY 设置创建时间
category.last_mod_time = timezone.now() #ZNY 设置修改时间
category.save() #ZNY 保存分类
tag = Tag() #ZNY 创建标签实例
tag.name = "nicetag" #ZNY 设置标签名称
tag.save() #ZNY 保存标签
article = Article() #ZNY 创建文章实例
article.title = "nicetitle" #ZNY 设置文章标题
article.body = "nicecontent" #ZNY 设置文章正文
article.author = user #ZNY 设置文章作者
article.category = category #ZNY 设置文章分类
article.type = 'a' #ZNY 设置文章类型为普通文章
article.status = 'p' #ZNY 设置文章状态为发布
article.save() #ZNY 保存文章
self.assertEqual(0, article.tags.count()) #ZNY 断言初始标签数量为0
article.tags.add(tag) #ZNY 添加标签到文章
article.save() #ZNY 保存文章
self.assertEqual(1, article.tags.count()) #ZNY 断言标签数量为1
for i in range(20): #ZNY 循环创建20篇文章
article = Article() #ZNY 创建文章实例
article.title = "nicetitle" + str(i) #ZNY 设置带编号的文章标题
article.body = "nicetitle" + str(i) #ZNY 设置带编号的文章正文
article.author = user #ZNY 设置文章作者
article.category = category #ZNY 设置文章分类
article.type = 'a' #ZNY 设置文章类型为普通文章
article.status = 'p' #ZNY 设置文章状态为发布
article.save() #ZNY 保存文章
article.tags.add(tag) #ZNY 添加标签到文章
article.save() #ZNY 保存文章
from blog.documents import ELASTICSEARCH_ENABLED #ZNY 导入Elasticsearch启用标志
if ELASTICSEARCH_ENABLED: #ZNY 如果Elasticsearch已启用
call_command("build_index") #ZNY 调用构建索引命令
response = self.client.get('/search', {'q': 'nicetitle'}) #ZNY 搜索文章
self.assertEqual(response.status_code, 200) #ZNY 断言搜索响应状态码为200
response = self.client.get(article.get_absolute_url()) #ZNY 访问文章详情页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
from djangoblog.spider_notify import SpiderNotify #ZNY 导入蜘蛛通知模块
SpiderNotify.notify(article.get_absolute_url()) #ZNY 通知蜘蛛文章URL
response = self.client.get(tag.get_absolute_url()) #ZNY 访问标签详情页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
response = self.client.get(category.get_absolute_url()) #ZNY 访问分类详情页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
response = self.client.get('/search', {'q': 'django'}) #ZNY 搜索django相关内容
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
s = load_articletags(article) #ZNY 加载文章标签
self.assertIsNotNone(s) #ZNY 断言标签不为空
self.client.login(username='liangliangyy', password='liangliangyy') #ZNY 登录用户
response = self.client.get(reverse('blog:archives')) #ZNY 访问文章归档页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
p = Paginator(Article.objects.all(), settings.PAGINATE_BY) #ZNY 创建文章分页器
self.check_pagination(p, '', '') #ZNY 检查首页分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) #ZNY 创建标签文章分页器
self.check_pagination(p, '分类标签归档', tag.slug) #ZNY 检查标签分页
p = Paginator( #ZNY 创建作者文章分页器
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy') #ZNY 检查作者分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) #ZNY 创建分类文章分页器
self.check_pagination(p, '分类目录归档', category.slug) #ZNY 检查分类分页
f = BlogSearchForm() #ZNY 创建搜索表单实例
f.search() #ZNY 执行搜索
# self.client.login(username='liangliangyy', password='liangliangyy') #ZNY 注释掉的登录代码
from djangoblog.spider_notify import SpiderNotify #ZNY 导入蜘蛛通知模块
SpiderNotify.baidu_notify([article.get_full_url()]) #ZNY 通知百度文章完整URL
from blog.templatetags.blog_tags import gravatar_url, gravatar #ZNY 导入Gravatar相关函数
u = gravatar_url('liangliangyy@gmail.com') #ZNY 获取Gravatar头像URL
u = gravatar('liangliangyy@gmail.com') #ZNY 获取Gravatar头像HTML
link = Links( #ZNY 创建友情链接实例
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save() #ZNY 保存链接
response = self.client.get('/links.html') #ZNY 访问友情链接页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
response = self.client.get('/feed/') #ZNY 访问RSS订阅
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
response = self.client.get('/sitemap.xml') #ZNY 访问站点地图
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
self.client.get("/admin/blog/article/1/delete/") #ZNY 访问文章删除页
self.client.get('/admin/servermanager/emailsendlog/') #ZNY 访问邮件发送日志管理页
self.client.get('/admin/admin/logentry/') #ZNY 访问日志条目管理页
self.client.get('/admin/admin/logentry/1/change/') #ZNY 访问日志条目编辑页
def check_pagination(self, p, type, value): #ZNY 检查分页方法
for page in range(1, p.num_pages + 1): #ZNY 遍历所有分页
s = load_pagination_info(p.page(page), type, value) #ZNY 加载分页信息
self.assertIsNotNone(s) #ZNY 断言分页信息不为空
if s['previous_url']: #ZNY 如果存在上一页URL
response = self.client.get(s['previous_url']) #ZNY 访问上一页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
if s['next_url']: #ZNY 如果存在下一页URL
response = self.client.get(s['next_url']) #ZNY 访问下一页
self.assertEqual(response.status_code, 200) #ZNY 断言响应状态码为200
def test_image(self): #ZNY 测试图片相关功能
import requests #ZNY 导入requests库
rsp = requests.get( #ZNY 下载图片
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') #ZNY 构建图片保存路径
with open(imagepath, 'wb') as file: #ZNY 保存图片到本地
file.write(rsp.content)
rsp = self.client.post('/upload') #ZNY 测试未授权上传
self.assertEqual(rsp.status_code, 403) #ZNY 断言返回403禁止访问
sign = get_sha256(get_sha256(settings.SECRET_KEY)) #ZNY 生成上传签名
with open(imagepath, 'rb') as file: #ZNY 打开图片文件
imgfile = SimpleUploadedFile( #ZNY 创建上传文件对象
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile} #ZNY 构建表单数据
rsp = self.client.post( #ZNY 执行上传请求
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200) #ZNY 断言上传成功
os.remove(imagepath) #ZNY 删除临时图片文件
from djangoblog.utils import save_user_avatar, send_email #ZNY 导入工具函数
send_email(['qq@qq.com'], 'testTitle', 'testContent') #ZNY 测试发送邮件
save_user_avatar( #ZNY 测试保存用户头像
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self): #ZNY 测试错误页面
rsp = self.client.get('/eee') #ZNY 访问不存在的页面
self.assertEqual(rsp.status_code, 404) #ZNY 断言返回404错误
def test_commands(self): #ZNY 测试管理命令
user = BlogUser.objects.get_or_create( #ZNY 获取或创建测试用户
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy") #ZNY 设置用户密码
user.is_staff = True #ZNY 设置为管理员
user.is_superuser = True #ZNY 设置为超级用户
user.save() #ZNY 保存用户
c = OAuthConfig() #ZNY 创建OAuth配置实例
c.type = 'qq' #ZNY 设置OAuth类型为QQ
c.appkey = 'appkey' #ZNY 设置应用密钥
c.appsecret = 'appsecret' #ZNY 设置应用密钥
c.save() #ZNY 保存配置
u = OAuthUser() #ZNY 创建OAuth用户实例
u.type = 'qq' #ZNY 设置OAuth类型为QQ
u.openid = 'openid' #ZNY 设置OpenID
u.user = user #ZNY 关联用户
u.picture = static("/blog/img/avatar.png") #ZNY 设置头像路径
u.metadata = ''' #ZNY 设置元数据
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save() #ZNY 保存OAuth用户
u = OAuthUser() #ZNY 创建另一个OAuth用户实例
u.type = 'qq' #ZNY 设置OAuth类型为QQ
u.openid = 'openid1' #ZNY 设置不同的OpenID
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' #ZNY 设置头像URL
u.metadata = ''' #ZNY 设置元数据
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save() #ZNY 保存OAuth用户
from blog.documents import ELASTICSEARCH_ENABLED #ZNY 导入Elasticsearch启用标志
if ELASTICSEARCH_ENABLED: #ZNY 如果Elasticsearch已启用
call_command("build_index") #ZNY 调用构建索引命令
call_command("ping_baidu", "all") #ZNY 调用百度推送命令
call_command("create_testdata") #ZNY 调用创建测试数据命令
call_command("clear_cache") #ZNY 调用清除缓存命令
call_command("sync_user_avatar") #ZNY 调用同步用户头像命令
call_command("build_search_words") #ZNY 调用构建搜索词命令

@ -1,66 +0,0 @@
from django.urls import path #ZNY 导入Django URL路径模块
from django.views.decorators.cache import cache_page #ZNY 导入缓存页面装饰器
from django.urls import path
from . import views #ZNY 导入当前应用的视图模块
app_name = "blog" #ZNY 定义应用命名空间为blog
urlpatterns = [ #ZNY 定义URL模式列表
path( #ZNY 首页路由
r'',
views.IndexView.as_view(), #ZNY 使用类视图处理首页
name='index'), #ZNY URL名称为index
path( #ZNY 首页分页路由
r'page/<int:page>/', #ZNY 带页码参数的URL
views.IndexView.as_view(), #ZNY 使用相同的类视图处理分页
name='index_page'), #ZNY URL名称为index_page
path( #ZNY 文章详情页路由
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', #ZNY 包含年月日和文章ID的URL
views.ArticleDetailView.as_view(), #ZNY 使用文章详情类视图
name='detailbyid'), #ZNY URL名称为detailbyid
path( #ZNY 分类详情页路由
r'category/<slug:category_name>.html', #ZNY 包含分类名称的URL
views.CategoryDetailView.as_view(), #ZNY 使用分类详情类视图
name='category_detail'), #ZNY URL名称为category_detail
path( #ZNY 分类分页路由
r'category/<slug:category_name>/<int:page>.html', #ZNY 包含分类名称和页码的URL
views.CategoryDetailView.as_view(), #ZNY 使用相同的类视图处理分页
name='category_detail_page'), #ZNY URL名称为category_detail_page
path( #ZNY 作者详情页路由
r'author/<author_name>.html', #ZNY 包含作者名称的URL
views.AuthorDetailView.as_view(), #ZNY 使用作者详情类视图
name='author_detail'), #ZNY URL名称为author_detail
path( #ZNY 作者分页路由
r'author/<author_name>/<int:page>.html', #ZNY 包含作者名称和页码的URL
views.AuthorDetailView.as_view(), #ZNY 使用相同的类视图处理分页
name='author_detail_page'), #ZNY URL名称为author_detail_page
path( #ZNY 标签详情页路由
r'tag/<slug:tag_name>.html', #ZNY 包含标签名称的URL
views.TagDetailView.as_view(), #ZNY 使用标签详情类视图
name='tag_detail'), #ZNY URL名称为tag_detail
path( #ZNY 标签分页路由
r'tag/<slug:tag_name>/<int:page>.html', #ZNY 包含标签名称和页码的URL
views.TagDetailView.as_view(), #ZNY 使用相同的类视图处理分页
name='tag_detail_page'), #ZNY URL名称为tag_detail_page
path( #ZNY 文章归档页路由
'archives.html',
cache_page( #ZNY 使用缓存装饰器缓存页面
60 * 60)( #ZNY 缓存1小时
views.ArchivesView.as_view()), #ZNY 使用归档视图
name='archives'), #ZNY URL名称为archives
path( #ZNY 友情链接页路由
'links.html',
views.LinkListView.as_view(), #ZNY 使用链接列表视图
name='links'), #ZNY URL名称为links
path( #ZNY 文件上传路由
r'upload',
views.fileupload, #ZNY 使用函数视图处理文件上传
name='upload'), #ZNY URL名称为upload
path( #ZNY 清理缓存路由
r'clean',
views.clean_cache_view, #ZNY 使用函数视图清理缓存
name='clean'), #ZNY URL名称为clean
# Zxy: 新增点赞和不喜欢 API 路由
path('article/<int:article_id>/like/', views.like_article, name='like_article'),
path('article/<int:article_id>/dislike/', views.dislike_article, name='dislike_article'),
]

@ -1,408 +0,0 @@
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from .models import Article, Category
logger = logging.getLogger(__name__)
class ArticleListView(ListView): #ZNY 文章列表视图基类继承Django通用列表视图
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html' #ZNY 指定使用的模板文件
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list' #ZNY 模板中使用的上下文变量名
# 页面类型,分类目录或标签列表等
page_type = '' #ZNY 页面类型标识
paginate_by = settings.PAGINATE_BY #ZNY 每页显示的文章数量
page_kwarg = 'page' #ZNY 分页参数名称
link_type = LinkShowType.L #ZNY 链接显示类型,默认为列表页
def get_view_cache_key(self): #ZNY 获取视图缓存键方法
return self.request.get['pages'] #ZNY 从请求中获取页面参数作为缓存键
@property
def page_number(self): #ZNY 页码属性,计算当前页码
page_kwarg = self.page_kwarg #ZNY 获取分页参数名
page = self.kwargs.get( #ZNY 从URL参数、GET参数或默认值获取页码
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page #ZNY 返回当前页码
def get_queryset_cache_key(self): #ZNY 获取查询集缓存键方法
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError() #ZNY 要求子类必须实现此方法
def get_queryset_data(self): #ZNY 获取查询集数据方法
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError() #ZNY 要求子类必须实现此方法
def get_queryset_from_cache(self, cache_key): #ZNY 从缓存获取查询集方法
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key) #ZNY 尝试从缓存获取数据
if value: #ZNY 如果缓存存在
logger.info('get view cache.key:{key}'.format(key=cache_key)) #ZNY 记录缓存命中日志
return value #ZNY 返回缓存数据
else: #ZNY 如果缓存不存在
article_list = self.get_queryset_data() #ZNY 从数据库获取数据
cache.set(cache_key, article_list) #ZNY 将数据存入缓存
logger.info('set view cache.key:{key}'.format(key=cache_key)) #ZNY 记录缓存设置日志
return article_list #ZNY 返回数据
def get_queryset(self): #ZNY 重写获取查询集方法
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key() #ZNY 获取缓存键
value = self.get_queryset_from_cache(key) #ZNY 从缓存获取数据
return value #ZNY 返回查询集
def get_context_data(self, **kwargs): #ZNY 获取上下文数据方法
kwargs['linktype'] = self.link_type #ZNY 添加链接类型到上下文
return super(ArticleListView, self).get_context_data(**kwargs) #ZNY 调用父类方法
class IndexView(ArticleListView): #ZNY 首页视图,继承文章列表视图
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I #ZNY 设置链接显示类型为首页
def get_queryset_data(self): #ZNY 实现获取查询集数据方法
article_list = Article.objects.filter(type='a', status='p') #ZNY 获取所有已发布的普通文章
return article_list #ZNY 返回文章列表
def get_queryset_cache_key(self): #ZNY 实现获取查询集缓存键方法
cache_key = 'index_{page}'.format(page=self.page_number) #ZNY 生成首页缓存键,包含页码
return cache_key #ZNY 返回缓存键
class ArticleDetailView(DetailView): #ZNY 文章详情视图继承Django通用详情视图
'''
文章详情页面
'''
template_name = 'blog/article_detail.html' #ZNY 指定详情页模板
model = Article #ZNY 指定模型类
pk_url_kwarg = 'article_id' #ZNY URL中的主键参数名
context_object_name = "article" #ZNY 模板中使用的上下文变量名
def get_context_data(self, **kwargs): #ZNY 重写获取上下文数据方法
comment_form = CommentForm() #ZNY 创建评论表单实例
article_comments = self.object.comment_list() #ZNY 获取文章评论列表
parent_comments = article_comments.filter(parent_comment=None) #ZNY 获取顶级评论(无父评论)
blog_setting = get_blog_setting() #ZNY 获取博客设置
paginator = Paginator(parent_comments, blog_setting.article_comment_count) #ZNY 创建评论分页器
page = self.request.GET.get('comment_page', '1') #ZNY 获取评论页码默认为1
if not page.isnumeric(): #ZNY 如果页码不是数字
page = 1 #ZNY 设置为第一页
else: #ZNY 如果是数字
page = int(page) #ZNY 转换为整数
if page < 1: #ZNY 如果页码小于1
page = 1 #ZNY 设置为第一页
if page > paginator.num_pages: #ZNY 如果页码大于总页数
page = paginator.num_pages #ZNY 设置为最后一页
p_comments = paginator.page(page) #ZNY 获取指定页的评论
next_page = p_comments.next_page_number() if p_comments.has_next() else None #ZNY 计算下一页页码
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None #ZNY 计算上一页页码
if next_page: #ZNY 如果存在下一页
kwargs[ #ZNY 设置下一页评论URL
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page: #ZNY 如果存在上一页
kwargs[ #ZNY 设置上一页评论URL
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
kwargs['form'] = comment_form #ZNY 添加评论表单到上下文
kwargs['article_comments'] = article_comments #ZNY 添加所有评论到上下文
kwargs['p_comments'] = p_comments #ZNY 添加分页评论到上下文
kwargs['comment_count'] = len( #ZNY 添加评论总数到上下文
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article #ZNY 添加下一篇文章到上下文
kwargs['prev_article'] = self.object.prev_article #ZNY 添加上一篇文章到上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs) #ZNY 调用父类方法获取基础上下文
article = self.object #ZNY 获取当前文章对象
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request) #ZNY 执行文章内容获取后的动作钩子
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, #ZNY 应用文章内容过滤器
request=self.request)
return context #ZNY 返回完整的上下文数据
class CategoryDetailView(ArticleListView): #ZNY 分类详情视图,继承文章列表视图
'''
分类目录列表
'''
page_type = "分类目录归档" #ZNY 设置页面类型
def get_queryset_data(self): #ZNY 实现获取查询集数据方法
slug = self.kwargs['category_name'] #ZNY 从URL参数获取分类slug
category = get_object_or_404(Category, slug=slug) #ZNY 获取分类对象不存在则404
categoryname = category.name #ZNY 获取分类名称
self.categoryname = categoryname #ZNY 保存分类名称到实例变量
categorynames = list( #ZNY 获取所有子分类名称列表
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter( #ZNY 获取该分类及其子分类下的所有已发布文章
category__name__in=categorynames, status='p')
return article_list #ZNY 返回文章列表
def get_queryset_cache_key(self): #ZNY 实现获取查询集缓存键方法
slug = self.kwargs['category_name'] #ZNY 从URL参数获取分类slug
category = get_object_or_404(Category, slug=slug) #ZNY 获取分类对象
categoryname = category.name #ZNY 获取分类名称
self.categoryname = categoryname #ZNY 保存分类名称到实例变量
cache_key = 'category_list_{categoryname}_{page}'.format( #ZNY 生成分类缓存键
categoryname=categoryname, page=self.page_number)
return cache_key #ZNY 返回缓存键
def get_context_data(self, **kwargs): #ZNY 重写获取上下文数据方法
categoryname = self.categoryname #ZNY 获取分类名称
try:
categoryname = categoryname.split('/')[-1] #ZNY 尝试获取分类路径的最后一部分
except BaseException:
pass #ZNY 如果出错则忽略
kwargs['page_type'] = CategoryDetailView.page_type #ZNY 添加页面类型到上下文
kwargs['tag_name'] = categoryname #ZNY 添加分类名称到上下文
return super(CategoryDetailView, self).get_context_data(**kwargs) #ZNY 调用父类方法
class AuthorDetailView(ArticleListView): #ZNY 作者详情视图,继承文章列表视图
'''
作者详情页
'''
page_type = '作者文章归档' #ZNY 设置页面类型
def get_queryset_cache_key(self): #ZNY 实现获取查询集缓存键方法
from uuslug import slugify #ZNY 导入slugify函数
author_name = slugify(self.kwargs['author_name']) #ZNY 对作者名称进行slugify处理
cache_key = 'author_{author_name}_{page}'.format( #ZNY 生成作者缓存键
author_name=author_name, page=self.page_number)
return cache_key #ZNY 返回缓存键
def get_queryset_data(self): #ZNY 实现获取查询集数据方法
author_name = self.kwargs['author_name'] #ZNY 从URL参数获取作者名称
article_list = Article.objects.filter( #ZNY 获取该作者的所有已发布普通文章
author__username=author_name, type='a', status='p')
return article_list #ZNY 返回文章列表
def get_context_data(self, **kwargs): #ZNY 重写获取上下文数据方法
author_name = self.kwargs['author_name'] #ZNY 获取作者名称
kwargs['page_type'] = AuthorDetailView.page_type #ZNY 添加页面类型到上下文
kwargs['tag_name'] = author_name #ZNY 添加作者名称到上下文
return super(AuthorDetailView, self).get_context_data(**kwargs) #ZNY 调用父类方法
class TagDetailView(ArticleListView): #ZNY 标签详情视图,继承文章列表视图
'''
标签列表页面
'''
page_type = '分类标签归档' #ZNY 设置页面类型
def get_queryset_data(self): #ZNY 实现获取查询集数据方法
slug = self.kwargs['tag_name'] #ZNY 从URL参数获取标签slug
tag = get_object_or_404(Tag, slug=slug) #ZNY 获取标签对象不存在则404
tag_name = tag.name #ZNY 获取标签名称
self.name = tag_name #ZNY 保存标签名称到实例变量
article_list = Article.objects.filter( #ZNY 获取该标签下的所有已发布普通文章
tags__name=tag_name, type='a', status='p')
return article_list #ZNY 返回文章列表
def get_queryset_cache_key(self): #ZNY 实现获取查询集缓存键方法
slug = self.kwargs['tag_name'] #ZNY 从URL参数获取标签slug
tag = get_object_or_404(Tag, slug=slug) #ZNY 获取标签对象
tag_name = tag.name #ZNY 获取标签名称
self.name = tag_name #ZNY 保存标签名称到实例变量
cache_key = 'tag_{tag_name}_{page}'.format( #ZNY 生成标签缓存键
tag_name=tag_name, page=self.page_number)
return cache_key #ZNY 返回缓存键
def get_context_data(self, **kwargs): #ZNY 重写获取上下文数据方法
# tag_name = self.kwargs['tag_name'] #ZNY 注释掉的获取标签名称方式
tag_name = self.name #ZNY 从实例变量获取标签名称
kwargs['page_type'] = TagDetailView.page_type #ZNY 添加页面类型到上下文
kwargs['tag_name'] = tag_name #ZNY 添加标签名称到上下文
return super(TagDetailView, self).get_context_data(**kwargs) #ZNY 调用父类方法
class ArchivesView(ArticleListView): #ZNY 文章归档视图,继承文章列表视图
'''
文章归档页面
'''
page_type = '文章归档' #ZNY 设置页面类型
paginate_by = None #ZNY 不分页
page_kwarg = None #ZNY 无分页参数
template_name = 'blog/article_archives.html' #ZNY 指定归档页面模板
def get_queryset_data(self): #ZNY 实现获取查询集数据方法
return Article.objects.filter(status='p').all() #ZNY 获取所有已发布文章
def get_queryset_cache_key(self): #ZNY 实现获取查询集缓存键方法
cache_key = 'archives' #ZNY 归档页面使用固定缓存键
return cache_key #ZNY 返回缓存键
class LinkListView(ListView): #ZNY 友情链接列表视图继承Django通用列表视图
model = Links #ZNY 指定模型类
template_name = 'blog/links_list.html' #ZNY 指定模板文件
def get_queryset(self): #ZNY 重写获取查询集方法
return Links.objects.filter(is_enable=True) #ZNY 只返回启用的友情链接
class EsSearchView(SearchView): #ZNY Elasticsearch搜索视图继承Haystack搜索视图
def get_context(self): #ZNY 重写获取上下文方法
paginator, page = self.build_page() #ZNY 构建分页
context = { #ZNY 构建上下文字典
"query": self.query, #ZNY 搜索查询词
"form": self.form, #ZNY 搜索表单
"page": page, #ZNY 当前页
"paginator": paginator, #ZNY 分页器
"suggestion": None, #ZNY 搜索建议初始为None
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling: #ZNY 如果支持拼写建议
context["suggestion"] = self.results.query.get_spelling_suggestion() #ZNY 获取拼写建议
context.update(self.extra_context()) #ZNY 更新额外上下文
return context #ZNY 返回完整上下文
@csrf_exempt #ZNY 免除CSRF保护
def fileupload(request): #ZNY 文件上传视图函数
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST': #ZNY 只处理POST请求
sign = request.GET.get('sign', None) #ZNY 从GET参数获取签名
if not sign: #ZNY 如果没有签名
return HttpResponseForbidden() #ZNY 返回403禁止访问
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): #ZNY 验证签名是否正确
return HttpResponseForbidden() #ZNY 签名错误返回403
response = [] #ZNY 初始化响应列表
for filename in request.FILES: #ZNY 遍历所有上传的文件
timestr = timezone.now().strftime('%Y/%m/%d') #ZNY 生成时间目录字符串
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] #ZNY 图片扩展名列表
fname = u''.join(str(filename)) #ZNY 获取文件名
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 #ZNY 判断是否为图片文件
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) #ZNY 构建保存目录
if not os.path.exists(base_dir): #ZNY 如果目录不存在
os.makedirs(base_dir) #ZNY 创建目录
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) #ZNY 生成保存路径使用UUID作为文件名
if not savepath.startswith(base_dir): #ZNY 安全检查,确保路径在指定目录内
return HttpResponse("only for post") #ZNY 路径非法返回错误
with open(savepath, 'wb+') as wfile: #ZNY 打开文件进行写入
for chunk in request.FILES[filename].chunks(): #ZNY 分块写入文件
wfile.write(chunk)
if isimage: #ZNY 如果是图片文件
from PIL import Image #ZNY 导入PIL库
image = Image.open(savepath) #ZNY 打开图片
image.save(savepath, quality=20, optimize=True) #ZNY 压缩图片并保存
url = static(savepath) #ZNY 生成静态文件URL
response.append(url) #ZNY 将URL添加到响应列表
return HttpResponse(response) #ZNY 返回URL列表
else: #ZNY 非POST请求
return HttpResponse("only for post") #ZNY 返回只支持POST请求
def page_not_found_view( #ZNY 404页面未找到视图
request,
exception,
template_name='blog/error_page.html'):
if exception: #ZNY 如果有异常信息
logger.error(exception) #ZNY 记录错误日志
url = request.get_full_path() #ZNY 获取请求的完整路径
return render(request, #ZNY 渲染错误页面
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'}, #ZNY 错误消息和状态码
status=404) #ZNY 返回404状态码
def server_error_view(request, template_name='blog/error_page.html'): #ZNY 500服务器错误视图
return render(request, #ZNY 渲染错误页面
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'}, #ZNY 错误消息和状态码
status=500) #ZNY 返回500状态码
def permission_denied_view( #ZNY 403权限拒绝视图
request,
exception,
template_name='blog/error_page.html'):
if exception: #ZNY 如果有异常信息
logger.error(exception) #ZNY 记录错误日志
return render( #ZNY 渲染错误页面
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403) #ZNY 错误消息和状态码返回403状态码
def clean_cache_view(request): #ZNY 清理缓存视图
cache.clear() #ZNY 清除所有缓存
return HttpResponse('ok') #ZNY 返回成功响应
# Zxy: 新增点赞视图
@login_required
@require_POST
def like_article(request, article_id):
article = get_object_or_404(Article, id=article_id)
article.likes += 1
article.save()
return JsonResponse({
'ok': True,
'likes': article.likes,
'dislikes': article.dislikes
})
# Zxy: 新增不喜欢视图
@login_required
@require_POST
def dislike_article(request, article_id):
article = get_object_or_404(Article, id=article_id)
article.dislikes += 1
article.save()
return JsonResponse({
'ok': True,
'likes': article.likes,
'dislikes': article.dislikes
})

@ -1,96 +0,0 @@
#zyl
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# 批量禁用评论的 admin 动作
def disable_commentstatus(modeladmin, request, queryset):
"""
将选中的评论批量更新为禁用状态is_enable=False
:param modeladmin: 当前 ModelAdmin 实例
:param request: HTTP 请求对象
:param queryset: 选中的评论查询集
"""
queryset.update(is_enable=False)
# 批量启用评论的 admin 动作
def enable_commentstatus(modeladmin, request, queryset):
"""
将选中的评论批量更新为启用状态is_enable=True
:param modeladmin: 当前 ModelAdmin 实例
:param request: HTTP 请求对象
:param queryset: 选中的评论查询集
"""
queryset.update(is_enable=True)
# 设置 admin 动作在后台显示的友好名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# Comment 模型的后台管理配置类
class CommentAdmin(admin.ModelAdmin):
# 列表页每页显示 20 条记录
list_per_page = 20
# 列表页显示的字段(按顺序)
list_display = (
'id', # 评论 ID
'body', # 评论内容
'link_to_userinfo', # 自定义方法:链接到用户信息
'link_to_article', # 自定义方法:链接到文章
'is_enable', # 是否启用
'creation_time' # 创建时间
)
# 可点击跳转到详情页的字段
list_display_links = ('id', 'body', 'is_enable')
# 右侧过滤栏,按 is_enable 字段过滤
list_filter = ('is_enable',)
# 在编辑/创建页面排除的字段(这些字段会自动处理)
exclude = ('creation_time', 'last_modify_time')
# 注册批量操作动作
actions = [disable_commentstatus, enable_commentstatus]
# 自定义方法:生成指向用户详情页的链接
def link_to_userinfo(self, obj):
"""
构建指向评论作者后台详情页的 HTML 链接
:param obj: 当前评论实例
:return: 包含作者昵称或邮箱的 HTML 超链接
"""
# 获取作者模型的 app 名称和模型名称,用于构建 admin URL
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 反向解析作者详情页的 admin URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回 HTML 超链接,优先显示昵称,无昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# 自定义方法:生成指向文章详情页的链接
def link_to_article(self, obj):
"""
构建指向评论所属文章后台详情页的 HTML 链接
:param obj: 当前评论实例
:return: 包含文章标题的 HTML 超链接
"""
# 获取文章模型的 app 名称和模型名称,用于构建 admin URL
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 反向解析文章详情页的 admin URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回 HTML 超链接,显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 为自定义方法设置后台显示的友好名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,46 +0,0 @@
#zyl:
from django.apps import AppConfig
# 导入 Django 的应用配置基类 AppConfig
# 每个 Django 应用都可以有一个配置类来定义应用的行为和元数据
class CommentsConfig(AppConfig):
"""
Comments 评论应用的配置类
继承自 AppConfig用于配置 comments 应用的各项属性
Django 项目初始化时加载
"""
# 应用的名称,对应应用目录名
# 必须是完整的 Python 导入路径
# 在 settings.py 的 INSTALLED_APPS 中注册时使用此名称
name = 'comments'
# 可选配置示例(当前使用的是默认值,未显式定义):
# verbose_name = '评论系统' # 应用在 Django Admin 中显示的中文名称
# # 如果未设置,默认使用 name 的首字母大写形式
# default_auto_field = 'django.db.models.BigAutoField' # 模型主键的默认类型
# # Django 3.2+ 默认为 BigAutoField
# label = 'comments' # 应用的简短标签,用于内部标识
# # 如果未设置,默认为 name 的最后一部分
# path = '/path/to/comments' # 应用的路径
# # 通常自动从 name 推导,一般无需手动设置
# 高级配置方法(当前未使用):
# def ready(self):
# """
# 应用就绪时调用的钩子方法
# 常用于:
# - 导入信号处理器signals
# - 执行应用初始化操作
# - 注册检查checks
# """
# import comments.signals # 示例:导入信号

@ -1,80 +0,0 @@
#zyl
# comments/forms.py
# Django 表单定义文件:用于处理评论的创建和验证
from django import forms
from django.forms import ModelForm
from django.core.exceptions import ValidationError
from .models import Comment
class CommentForm(ModelForm):
"""
评论表单类
用于处理用户提交的评论内容包括主评论和回复评论
功能特性
- 验证评论内容的长度和格式
- 处理嵌套评论回复功能
- Comment 模型直接绑定
"""
# 隐藏输入字段:用于存储父评论的 ID当用户回复某个评论时
# required=False 表示这是可选字段(主评论没有父评论)
# 在模板中需要手动渲染此字段,或通过 JavaScript 动态赋值
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, # 使用隐藏输入框,不显示给用户
required=False # 允许为空(顶级评论无需父评论)
)
class Meta:
"""
表单元数据配置
"""
model = Comment # 指定关联的模型为 Comment
fields = ['body'] # 只包含 body 字段(评论内容)
# 其他字段(如 author、article将在视图中自动赋值
def clean_body(self):
"""
自定义验证方法验证评论内容body 字段
Django 表单会自动调用以 clean_<fieldname> 命名的方法来验证特定字段
验证规则
1. 去除首尾空白字符
2. 最小长度限制5个字符
3. 最大长度限制300个字符与模型定义保持一致
返回:
str: 验证通过并清理后的评论内容
抛出:
ValidationError: 当验证失败时抛出异常错误信息将显示在表单中
"""
# 从已清理的数据中获取 body 内容,并去除首尾空白
body = self.cleaned_data.get('body').strip()
# 定义长度限制常量
min_length = 5
max_length = 300 # 必须与 Comment 模型中 body 字段的 max_length 保持一致
# 验证:检查最小长度
if len(body) < min_length:
raise ValidationError(f"评论内容不能少于{min_length}个字符")
# 验证:检查最大长度
if len(body) > max_length:
raise ValidationError(f"评论内容不能超过{max_length}个字符")
# 返回清理后的数据Django 要求 clean_<field> 方法必须返回处理后的值)
return body
# 使用示例(在视图中):
# form = CommentForm(request.POST)
# if form.is_valid():
# comment = form.save(commit=False)
# comment.author = request.user # 自动填充作者
# comment.article = article # 自动填充文章
# comment.save()

@ -1,68 +0,0 @@
#zyl
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
"""
创建 Comment 评论模型的数据库迁移类
"""
# 标记这是该应用的首次迁移
initial = True
# 定义此迁移依赖的其他迁移
dependencies = [
# 依赖 blog 应用的初始迁移(因为 Comment 外键关联 Article
('blog', '0001_initial'),
# 依赖用户认证系统的可替换用户模型(因为 Comment 外键关联用户)
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义迁移操作列表
operations = [
# 创建 Comment 模型
migrations.CreateModel(
name='Comment', # 模型名称
fields=[
# 主键 IDBigAutoField 支持更大的数值范围
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 评论正文内容,最大长度 300 字符的文本字段
('body', models.TextField(max_length=300, verbose_name='正文')),
# 评论创建时间,默认值为当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 评论最后修改时间,默认值为当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 是否显示该评论,布尔类型,默认为 True显示
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 外键关联:评论所属的文章
# on_delete=models.CASCADE当文章被删除时级联删除该评论
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 外键关联:评论的作者(用户)
# on_delete=models.CASCADE当用户被删除时级联删除该用户的所有评论
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 自关联外键:上级评论(用于实现评论的嵌套/回复功能)
# blank=True, null=True允许为空顶级评论没有上级评论
# on_delete=models.CASCADE当上级评论被删除时级联删除该回复
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
# 模型元数据配置
options={
'verbose_name': '评论', # 单数形式的模型名称(用于后台显示)
'verbose_name_plural': '评论', # 复数形式的模型名称(用于后台显示)
'ordering': ['-id'], # 默认按 ID 降序排列(最新评论在前)
'get_latest_by': 'id', # 指定获取最新记录的字段
},
),
]

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

Loading…
Cancel
Save