Compare commits

..

20 Commits

Binary file not shown.

Binary file not shown.

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

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

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

@ -0,0 +1,18 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 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 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -0,0 +1,47 @@
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

@ -0,0 +1,136 @@
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

@ -0,0 +1,43 @@
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}}

@ -0,0 +1,39 @@
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 }}

@ -0,0 +1,80 @@
# 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/

@ -0,0 +1,15 @@
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"]

@ -0,0 +1,20 @@
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.

@ -0,0 +1,158 @@
# 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)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,72 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
#lht: 创建用户表单用于在Django管理后台创建新用户
#lht: 密码输入字段
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#lht: 确认密码输入字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
#lht: 指定关联的模型和字段
model = BlogUser
fields = ('email',)
def clean_password2(self):
#lht: 验证两次密码输入是否一致
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):
#lht: 保存用户并加密密码
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
#lht: 设置用户来源为管理后台
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
#lht: 修改用户表单用于在Django管理后台编辑现有用户
class Meta:
#lht: 指定关联的模型、字段和字段类
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
#lht: 初始化表单
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
#lht: 用户管理界面配置自定义Django管理后台的用户管理界面
#lht: 指定修改用户和创建用户使用的表单
form = BlogUserChangeForm
add_form = BlogUserCreationForm
#lht: 定义在列表页面显示的字段
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
#lht: 定义在列表页面中可点击跳转到编辑页面的字段
list_display_links = ('id', 'username')
#lht: 定义默认排序方式
ordering = ('-id',)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
#lht:指定应用的名称Django会根据这个名称找到对应的应用目录
name = 'accounts'

@ -0,0 +1,136 @@
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):
#lht: 登录表单继承Django内置认证表单
def __init__(self, *args, **kwargs):
#lht: 调用父类构造函数
super(LoginForm, self).__init__(*args, **kwargs)
#lht: 自定义用户名和密码字段的显示样式
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):
#lht: 用户注册表单继承Django内置用户创建表单
def __init__(self, *args, **kwargs):
#lht: 调用父类构造函数
super(RegisterForm, self).__init__(*args, **kwargs)
#lht: 自定义各字段的显示样式
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):
#lht: 验证邮箱唯一性
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
#lht: 指定模型和字段
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
#lht: 忘记密码表单
#lht: 新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
#lht: 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
#lht: 邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
#lht: 验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
#lht: 验证两次输入的密码是否一致
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"))
#lht: 验证密码强度
password_validation.validate_password(password2)
return password2
def clean_email(self):
#lht: 验证邮箱是否存在
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
#lht: todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#lht: 验证验证码是否正确
code = self.cleaned_data.get("code")
#lht: 调用工具函数验证验证码
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
#lht: 忘记密码时获取验证码的表单
#lht: 邮箱字段
email = forms.EmailField(
label=_('Email'),
)

@ -0,0 +1,70 @@
# 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):
#lht: 标记这是一个初始迁移文件
initial = True
#lht: 定义依赖关系该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
#lht: 创建BlogUser模型的操作
migrations.CreateModel(
name='BlogUser',
fields=[
#lht: 主键字段自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#lht: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')),
#lht: 上次登录时间字段
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
#lht: 超级用户状态字段,拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
#lht: 用户名字段,具有唯一性约束和验证器
('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')),
#lht: 名字字段
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
#lht: 姓氏字段
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
#lht: 邮箱地址字段
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
#lht: 员工状态字段,决定是否可以登录管理站点
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
#lht: 活跃状态字段,决定用户账户是否有效
('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')),
#lht: 加入日期字段
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
#lht: 昵称字段,博客用户的额外信息
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
#lht: 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#lht: 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#lht: 创建来源字段,标记用户通过何种方式创建
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
#lht: 用户组关联字段,多对多关系
('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')),
#lht: 用户权限字段,多对多关系
('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')),
],
#lht: 模型选项配置
options={
'verbose_name': '用户', #lht: 单数名称
'verbose_name_plural': '用户', #lht: 复数名称
'ordering': ['-id'], #lht: 默认排序方式按ID降序
'get_latest_by': 'id', #lht: 获取最新记录的依据字段
},
#lht: 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,70 @@
# 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):
#lht: 标记这是一个初始迁移文件
initial = True
#lht: 定义依赖关系该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
#lht: 创建BlogUser模型的操作
migrations.CreateModel(
name='BlogUser',
fields=[
#lht: 主键字段自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#lht: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')),
#lht: 上次登录时间字段
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
#lht: 超级用户状态字段,拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
#lht: 用户名字段,具有唯一性约束和验证器
('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')),
#lht: 名字字段
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
#lht: 姓氏字段
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
#lht: 邮箱地址字段
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
#lht: 员工状态字段,决定是否可以登录管理站点
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
#lht: 活跃状态字段,决定用户账户是否有效
('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')),
#lht: 加入日期字段
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
#lht: 昵称字段,博客用户的额外信息
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
#lht: 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#lht: 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#lht: 创建来源字段,标记用户通过何种方式创建
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
#lht: 用户组关联字段,多对多关系
('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')),
#lht: 用户权限字段,多对多关系
('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')),
],
#lht: 模型选项配置
options={
'verbose_name': '用户', #lht: 单数名称
'verbose_name_plural': '用户', #lht: 复数名称
'ordering': ['-id'], #lht: 默认排序方式按ID降序
'get_latest_by': 'id', #lht: 获取最新记录的依据字段
},
#lht: 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,43 @@
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
#lht: Create your models here.
class BlogUser(AbstractUser):
#lht: 用户昵称字段
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#lht: 用户创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#lht: 用户最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#lht: 用户来源标识(如通过注册、后台创建等)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#lht: 返回用户个人页面的URL
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
#lht: 字符串表示,返回用户邮箱
return self.email
def get_full_url(self):
#lht: 获取用户页面的完整URL
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
#lht: 模型元数据配置
ordering = ['-id'] #lht: 默认按ID倒序排列
verbose_name = _('user') #lht: 单数名称
verbose_name_plural = verbose_name #lht: 复数名称
get_latest_by = 'id' #lht: 获取最新记录的字段

@ -0,0 +1,275 @@
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
#lht: Create your tests here.
class AccountTest(TestCase):
#lht: """
#lht: 账户功能测试类
#lht: 继承Django的TestCase用于测试账户相关的各种功能
#lht: """
def setUp(self):
#lht: """
#lht: 测试前的准备工作
#lht: 每个测试方法执行前都会调用此方法
#lht: """
self.client = Client() #lht: 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() #lht: 创建请求工厂,用于创建请求对象
#lht: 创建一个测试用户,用于后续的测试
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" #lht: 设置测试用的新密码
def test_validate_account(self):
#lht: """
#lht: 测试账户验证功能
#lht: 包括超级用户创建、登录验证、管理员权限等
#lht: """
site = get_current_site().domain
#lht: 创建超级用户用于测试
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
#lht: 测试用户登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #lht: 验证登录成功
response = self.client.get('/admin/') #lht: 访问管理后台
self.assertEqual(response.status_code, 200) #lht: 验证访问成功
#lht: 创建分类和文章用于测试
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
#lht: 测试能否正常访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#lht: """
#lht: 测试用户注册流程
#lht: 包括注册、邮箱验证、登录、权限设置等完整流程
#lht: """
#lht: 验证目标邮箱尚未注册
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lht: 模拟用户注册请求
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#lht: 验证用户已成功创建
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lht: 获取新创建的用户并验证邮箱链接
user = BlogUser.objects.filter(email='user123@user.com')[0]
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)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
#lht: 使用新用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
#lht: 设置用户为超级用户和员工,以便访问管理功能
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
#lht: 创建分类和文章
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()
#lht: 验证能够访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#lht: 测试用户登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
#lht: 登出后应无法访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
#lht: 测试使用错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
#lht: 登录失败后仍无法访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
#lht: """
#lht: 测试邮箱验证码验证功能
#lht: """
to_email = "admin@admin.com"
code = generate_code() #lht: 生成验证码
utils.set_code(to_email, code) #lht: 设置验证码
utils.send_verify_email(to_email, code) #lht: 发送验证码(模拟)
#lht: 验证正确的验证码能通过验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
#lht: 验证错误的验证码不能通过验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#lht: """
#lht: 测试忘记密码时成功获取验证码
#lht: """
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
#lht: 验证请求成功且返回"ok"
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#lht: """
#lht: 测试忘记密码时获取验证码失败的情况
#lht: """
#lht: 测试没有提供邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
#lht: 测试提供无效邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#lht: """
#lht: 测试成功重置密码的完整流程
#lht: """
code = generate_code()
utils.set_code(self.blog_user.email, code) #lht: 设置验证码
#lht: 准备重置密码的数据
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
#lht: 发送重置密码请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) #lht: 重定向表示成功
#lht: 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() #lht: type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#lht: """
#lht: 测试为不存在的用户重置密码的情况
#lht: """
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", #lht: 不存在的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lht: 应该返回200状态码而不是重定向因为验证失败
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#lht: """
#lht: 测试使用错误验证码重置密码的情况
#lht: """
code = generate_code()
utils.set_code(self.blog_user.email, code)
#lht: 使用错误的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", #lht: 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lht: 应该返回200状态码而不是重定向因为验证失败
self.assertEqual(resp.status_code, 200)

@ -0,0 +1,35 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts" #lht: 应用命名空间
urlpatterns = [
#lht: 登录URL
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
#lht: 注册URL
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
#lht: 登出URL
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
#lht: 账户操作结果页面
path(r'account/result.html',
views.account_result,
name='result'),
#lht: 忘记密码页面
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
#lht: 获取忘记密码验证码
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -0,0 +1,28 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
#lht: """
#lht: 允许使用用户名或邮箱登录
#lht: """
def authenticate(self, request, username=None, password=None, **kwargs):
#lht: 根据输入内容判断是邮箱还是用户名
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
#lht: 根据用户名获取用户对象
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -0,0 +1,49 @@
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
_code_ttl = timedelta(minutes=5) #lht: 验证码有效期5分钟
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
#lht: """发送重设密码验证码
#lht: Args:
#lht: to_mail: 接受邮箱
#lht: subject: 邮件主题
#lht: code: 验证码
#lht: """
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
#lht: """验证code是否有效
#lht: Args:
#lht: email: 请求邮箱
#lht: code: 验证码
#lht: Return:
#lht: 如果有错误就返回错误str
#lht: Node:
#lht: 这里的错误处理不太合理应该采用raise抛出
#lht: 否测调用方也需要对error进行处理
#lht: """
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
#lht: """设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
#lht: """获取code"""
return cache.get(email)

@ -0,0 +1,218 @@
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__)
#lht: Create your views here.
class RegisterView(FormView):
#lht: 用户注册视图
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#lht: 处理请求分发添加CSRF保护装饰器
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
if form.is_valid():
user = form.save(False)
user.is_active = False #lht: 新注册用户默认不激活
user.source = 'Register' #lht: 标记来源为注册
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#lht: 构造验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
#lht: 用户登出视图
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#lht: 处理请求分发,添加不缓存装饰器
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
#lht: 处理GET请求执行登出操作
logout(request)
delete_sidebar_cache() #lht: 清除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
#lht: 用户登录视图
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 #lht: 登录会话保持时间(一个月)
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#lht: 处理请求分发添加敏感参数保护、CSRF保护和不缓存装饰器
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#lht: 获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
#lht: 获取登录成功后的跳转URL
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
#lht: 账户操作结果页面
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
#lht: 忘记密码视图
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#lht: 发送忘记密码验证码视图
def post(self, request: HttpRequest):
#lht: 处理POST请求发送验证码邮件
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")

@ -0,0 +1,64 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()

@ -0,0 +1,172 @@
#zf导入Django表单模块
from django import forms
#zf导入Django管理后台模块
from django.contrib import admin
#zf导入获取用户模型的函数
from django.contrib.auth import get_user_model
#zf导入URL反向解析函数
from django.urls import reverse
#zf导入HTML格式化函数
from django.utils.html import format_html
#zf导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
#zf导入博客应用的Article模型
from .models import Article
#zf定义文章表单类继承自ModelForm
class ArticleForm(forms.ModelForm):
#zf被注释掉的代码使用AdminPagedownWidget作为body字段的widget
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
#zf包含所有字段
fields = '__all__'
#zf定义批量发布文章的操作函数
def makr_article_publish(modeladmin, request, queryset):
#zf将选中文章的状态更新为'p'(已发布)
queryset.update(status='p')
#zf定义批量将文章设为草稿的操作函数
def draft_article(modeladmin, request, queryset):
#zf将选中文章的状态更新为'd'(草稿)
queryset.update(status='d')
#zf定义批量关闭文章评论的操作函数
def close_article_commentstatus(modeladmin, request, queryset):
#zf将选中文章的评论状态更新为'c'(关闭)
queryset.update(comment_status='c')
#zf定义批量开启文章评论的操作函数
def open_article_commentstatus(modeladmin, request, queryset):
#zf将选中文章的评论状态更新为'o'(开启)
queryset.update(comment_status='o')
#zf为操作函数设置描述信息用于在管理后台显示支持国际化
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
#zf定义文章管理类继承自ModelAdmin
class ArticlelAdmin(admin.ModelAdmin):
#zf每页显示20条记录
list_per_page = 20
#zf设置可搜索的字段为body和title
search_fields = ('body', 'title')
#zf使用自定义的ArticleForm
form = ArticleForm
#zf设置在列表页显示的字段
list_display = (
'id',
'title',
'author',
#zf自定义的分类链接字段
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
#zf设置哪些字段可以作为链接点击进入编辑页
list_display_links = ('id', 'title')
#zf设置右侧的过滤器字段
list_filter = ('status', 'type', 'category')
#zf对tags字段使用水平过滤器
filter_horizontal = ('tags',)
#zf在表单中排除这些字段由系统自动管理
exclude = ('creation_time', 'last_modify_time')
#zf启用"在站点上查看"功能
view_on_site = True
#zf注册自定义的管理操作
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
#zf自定义字段显示分类链接
def link_to_category(self, obj):
#zf获取分类模型的app_label和model_name
info = (obj.category._meta.app_label, obj.category._meta.model_name)
#zf生成分类编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
#zf返回HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
#zf设置字段显示名称
link_to_category.short_description = _('category')
#zf自定义表单获取方法
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
#zf限制作者字段只能选择超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
#zf保存模型的方法
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
#zf获取在站点上查看的URL
def get_view_on_site_url(self, obj=None):
if obj:
#zf获取文章的完整URL
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
#zf获取当前站点域名
site = get_current_site().domain
return site
#zf定义标签管理类
class TagAdmin(admin.ModelAdmin):
#zf排除这些字段由系统自动管理
exclude = ('slug', 'last_mod_time', 'creation_time')
#zf定义分类管理类
class CategoryAdmin(admin.ModelAdmin):
#zf设置在列表页显示的字段
list_display = ('name', 'parent_category', 'index')
#zf排除这些字段由系统自动管理
exclude = ('slug', 'last_mod_time', 'creation_time')
#zf定义链接管理类
class LinksAdmin(admin.ModelAdmin):
#zf排除这些字段由系统自动管理
exclude = ('last_mod_time', 'creation_time')
#zf定义侧边栏管理类
class SideBarAdmin(admin.ModelAdmin):
#zf设置在列表页显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
#zf排除这些字段由系统自动管理
exclude = ('last_mod_time', 'creation_time')
#zf定义博客设置管理类
class BlogSettingsAdmin(admin.ModelAdmin):
#zf没有特殊配置
pass
#zf注册模型到管理后台
#zf注意在你提供的代码中模型注册部分被省略了通常应该包含如下代码
#zfadmin.site.register(Article, ArticlelAdmin)
#zfadmin.site.register(Tag, TagAdmin)
#zfadmin.site.register(Category, CategoryAdmin)
#zfadmin.site.register(Links, LinksAdmin)
#zfadmin.site.register(SideBar, SideBarAdmin)
#zfadmin.site.register(BlogSettings, BlogSettingsAdmin)

@ -0,0 +1,9 @@
#zf导入Django的应用配置基类
from django.apps import AppConfig
#zf定义博客应用的配置类继承自AppConfig
class BlogConfig(AppConfig):
#zf设置应用的名称为'blog'
#zf这个名称需要与Django项目中应用的目录名称保持一致
name = 'blog'

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

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

@ -0,0 +1,34 @@
# 导入日志模块,用于记录日志信息
import logging
# 导入Django表单模块
from django import forms
# 从haystack.forms导入SearchForm用于实现搜索功能
from haystack.forms import SearchForm
# 创建日志记录器
logger = logging.getLogger(__name__)
# 定义博客搜索表单类继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# 定义搜索查询字段,设置为必填项
querydata = forms.CharField(required=True)
# 重写search方法实现自定义搜索逻辑
def search(self):
# 调用父类的search方法获取搜索结果
datas = super(BlogSearchForm, self).search()
# 检查表单数据是否有效
if not self.is_valid():
# 如果无效,返回无查询结果
return self.no_query_found()
# 如果查询数据存在
if self.cleaned_data['querydata']:
# 记录查询关键字到日志中
logger.info(self.cleaned_data['querydata'])
# 返回搜索结果
return datas

@ -0,0 +1,25 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.documents模块导入需要用到的文档类和管理器以及Elasticsearch启用状态常量
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_index 时会显示
help = 'build search index'
def handle(self, *args, **options):
# 检查是否启用了Elasticsearch功能
if ELASTICSEARCH_ENABLED:
# 构建时间文档索引
ElaspedTimeDocumentManager.build_index()
# 创建ElapsedTimeDocument实例并初始化
manager = ElapsedTimeDocument()
manager.init()
# 创建ArticleDocumentManager实例删除现有索引后重新构建
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,18 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入Tag和Category模型
from blog.models import Tag, Category
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_search_words 时会显示
help = 'build search words'
def handle(self, *args, **options):
# 收集所有标签(Tag)和分类(Category)的名称并用set去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
# 将所有名称用换行符连接并打印输出
print('\n'.join(datas))

@ -0,0 +1,16 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从djangoblog.utils模块导入缓存工具
from djangoblog.utils import cache
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help clear_cache 时会显示
help = 'clear the whole cache'
def handle(self, *args, **options):
# 清除整个缓存
cache.clear()
# 使用标准输出打印成功信息,显示"缓存已清除"的消息
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -0,0 +1,61 @@
# 导入获取用户模型的函数用于操作Django内置的用户认证系统
from django.contrib.auth import get_user_model
# 导入密码加密函数,用于安全地存储用户密码
from django.contrib.auth.hashers import make_password
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help create_testdata 时会显示
help = 'create test datas'
def handle(self, *args, **options):
# 获取或创建一个测试用户邮箱为test@test.com用户名为"测试用户",密码经过加密处理
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 获取或创建一个父级分类,名称为"我是父类目",无上级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# 获取或创建一个子分类,名称为"子类目"父级分类为上面创建的pcategory
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# 保存分类虽然get_or_create已经保存过这里再次显式调用save
category.save()
# 创建一个基础标签对象,名称为"标签"
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 循环创建19篇文章序号从1到19
for i in range(1, 20):
# 获取或创建文章,设置分类、标题、内容和作者
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# 创建一个新的标签,名称为"标签"+序号
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 给文章添加两个标签:新创建的标签和基础标签
article.tags.add(tag)
article.tags.add(basetag)
article.save()
# 导入缓存工具并清除缓存,确保新创建的数据能立即生效
from djangoblog.utils import cache
cache.clear()
# 使用标准输出打印成功信息,显示"测试数据已创建"的消息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,77 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从djangoblog.spider_notify模块导入SpiderNotify类用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
# 从djangoblog.utils模块导入get_current_site函数用于获取当前站点信息
from djangoblog.utils import get_current_site
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
# 获取当前站点的域名
site = get_current_site().domain
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help ping_baidu 时会显示
help = 'notify baidu url'
# 添加命令行参数
def add_arguments(self, parser):
# 添加data_type参数指定要通知的数据类型
parser.add_argument(
'data_type',
type=str,
# 限制参数值只能是以下几种选项
choices=[
'all',
'article',
'tag',
'category'],
# 参数帮助说明
help='article : all article,tag : all tag,category: all category,all: All of these')
# 构造完整URL的方法
def get_full_url(self, path):
# 使用站点域名和路径拼接成完整HTTPS URL
url = "https://{site}{path}".format(site=site, path=path)
return url
# 命令主处理逻辑
def handle(self, *args, **options):
# 获取传入的数据类型参数
type = options['data_type']
# 输出开始处理的信息
self.stdout.write('start get %s' % type)
# 初始化URL列表
urls = []
# 如果是指定文章或全部则获取所有已发布文章的URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'): # status='p'表示已发布(published)
urls.append(article.get_full_url())
# 如果是指定标签或全部则获取所有标签页面的URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url() # 获取标签的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 如果是指定分类或全部则获取所有分类页面的URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url() # 获取分类的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 输出即将通知的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用百度通知接口推送所有URL
SpiderNotify.baidu_notify(urls)
# 输出完成通知的信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,84 @@
# 导入requests库用于发送HTTP请求测试图片链接有效性
import requests
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 导入static函数用于获取静态文件的URL
from django.templatetags.static import static
# 从djangoblog.utils模块导入save_user_avatar函数用于保存用户头像
from djangoblog.utils import save_user_avatar
# 从oauth.models模块导入OAuthUser模型用于操作OAuth用户
from oauth.models import OAuthUser
# 从oauth.oauthmanager模块导入get_manager_by_type函数用于获取对应类型的OAuth管理器
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help sync_user_avatar 时会显示
help = 'sync user avatar'
# 测试图片URL是否有效的方法
def test_picture(self, url):
try:
# 发送GET请求测试URL设置2秒超时
if requests.get(url, timeout=2).status_code == 200:
# 如果返回状态码为200说明图片有效返回True
return True
except:
# 捕获异常如网络错误或超时直接pass
pass
# 命令主处理逻辑
def handle(self, *args, **options):
# 获取静态文件根路径的URL
static_url = static("../")
# 获取所有OAuth用户
users = OAuthUser.objects.all()
# 输出开始同步的用户数量信息
self.stdout.write(f'开始同步{len(users)}个用户头像')
# 遍历所有用户进行头像同步
for u in users:
# 输出正在同步的用户昵称
self.stdout.write(f'开始同步:{u.nickname}')
# 获取用户当前头像URL
url = u.picture
# 如果用户有头像URL
if url:
# 如果头像URL以静态URL开头说明是本地静态文件
if url.startswith(static_url):
# 测试图片是否有效
if self.test_picture(url):
# 如果有效则跳过该用户,继续下一个
continue
else:
# 如果无效但用户有元数据
if u.metadata:
# 根据用户类型获取对应的OAuth管理器
manage = get_manager_by_type(u.type)
# 通过管理器和元数据获取新的头像URL
url = manage.get_picture(u.metadata)
# 保存用户头像到本地
url = save_user_avatar(url)
else:
# 如果没有元数据,则使用默认头像
url = static('blog/img/avatar.png')
else:
# 如果不是本地静态文件,则保存用户头像到本地
url = save_user_avatar(url)
else:
# 如果用户没有头像,则使用默认头像
url = static('blog/img/avatar.png')
# 如果获取到了有效的头像URL
if url:
# 输出同步完成的信息和新的头像URL
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 更新用户的头像URL并保存
u.picture = url
u.save()
# 输出同步结束信息
self.stdout.write('结束同步')

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

@ -0,0 +1,219 @@
# 由Django 4.1.7在2023年3月2日生成的初始数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
# 标记这是一个初始迁移文件
initial = True
# 定义依赖关系,依赖于用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义具体的操作
operations = [
# 创建BlogSettings模型用于存储网站配置信息
migrations.CreateModel(
name='BlogSettings',
fields=[
# 主键字段,自动增长的大整数
('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描述文本字段最大长度1000默认为空字符串
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键字文本字段最大长度1000默认为空字符串
('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='文章页面默认显示评论数目')),
# 是否显示谷歌广告布尔类型默认False
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 广告内容文本字段最大长度2000可为空默认为空字符串
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
# 是否打开网站评论功能布尔类型默认True
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 备案号字符字段最大长度2000可为空默认为空字符串
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
# 网站统计代码文本字段最大长度1000默认为空字符串
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案号布尔类型默认False
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
# 公安备案号文本字段最大长度2000可为空默认为空字符串
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
# 模型选项设置
options={
'verbose_name': '网站配置', # 单数形式的可读名称
'verbose_name_plural': '网站配置', # 复数形式的可读名称
},
),
# 创建Links模型用于存储友情链接信息
migrations.CreateModel(
name='Links',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址URL字段
('link', models.URLField(verbose_name='链接地址')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否显示布尔类型默认True
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 显示类型字符字段最大长度1可选值包括首页、列表页、文章页面、全站、友情链接页面默认为首页
('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'], # 默认排序按sequence字段升序排列
},
),
# 创建SideBar模型用于存储侧边栏信息
migrations.CreateModel(
name='SideBar',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 标题最大长度100
('name', models.CharField(max_length=100, verbose_name='标题')),
# 内容,文本字段
('content', models.TextField(verbose_name='内容')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用布尔类型默认True
('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'], # 默认排序按sequence字段升序排列
},
),
# 创建Tag模型用于存储文章标签
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='修改时间')),
# 标签名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
# 模型选项设置
options={
'verbose_name': '标签', # 单数形式的可读名称
'verbose_name_plural': '标签', # 复数形式的可读名称
'ordering': ['name'], # 默认排序按name字段升序排列
},
),
# 创建Category模型用于存储文章分类
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='修改时间')),
# 分类名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 权重排序整数类型默认0数值越大越靠前
('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'], # 默认排序按index字段降序排列
},
),
# 创建Article模型用于存储文章信息
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='修改时间')),
# 标题最大长度200唯一约束
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 正文使用MDTextField类型Markdown编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间,日期时间字段,默认为当前时间
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
# 文章状态字符字段最大长度1可选值为草稿和发表默认为发表
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
# 评论状态字符字段最大长度1可选值为打开和关闭默认为打开
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
# 类型字符字段最大长度1可选值为文章和页面默认为文章
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量正整数类型默认0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 文章排序整数类型默认0数值越大越靠前
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示toc目录布尔类型默认False
('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模型级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
# 标签集合多对多关系关联到Tag模型可为空
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
# 模型选项设置
options={
'verbose_name': '文章', # 单数形式的可读名称
'verbose_name_plural': '文章', # 复数形式的可读名称
'ordering': ['-article_order', '-pub_time'], # 默认排序先按article_order降序再按pub_time降序
'get_latest_by': 'id', # latest()方法使用的默认字段
},
),
]

@ -0,0 +1,28 @@
# 由Django 4.1.7在2023年3月29日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0001_initial
dependencies = [
('blog', '0001_initial'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段global_footer公共尾部
migrations.AddField(
model_name='blogsettings', # 目标模型名称
name='global_footer', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共尾部"
),
# 向BlogSettings模型添加新字段global_header公共头部
migrations.AddField(
model_name='blogsettings', # 目标模型名称
name='global_header', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共头部"
),
]

@ -0,0 +1,21 @@
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0002_blogsettings_global_footer_and_more
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段comment_need_review评论是否需要审核
migrations.AddField(
model_name='blogsettings', # 目标模型名称
name='comment_need_review', # 新增字段名
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # 字段定义布尔字段默认值为False显示名为"评论是否需要审核"
),
]

@ -0,0 +1,35 @@
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0003_blogsettings_comment_need_review
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# 定义具体的操作
operations = [
# 重命名BlogSettings模型中的字段analyticscode -> analytics_code
migrations.RenameField(
model_name='blogsettings', # 目标模型名称
old_name='analyticscode', # 原字段名
new_name='analytics_code', # 新字段名
),
# 重命名BlogSettings模型中的字段beiancode -> beian_code
migrations.RenameField(
model_name='blogsettings', # 目标模型名称
old_name='beiancode', # 原字段名
new_name='beian_code', # 新字段名
),
# 重命名BlogSettings模型中的字段sitename -> site_name
migrations.RenameField(
model_name='blogsettings', # 目标模型名称
old_name='sitename', # 原字段名
new_name='site_name', # 新字段名
),
]

@ -0,0 +1,352 @@
# 由Django 4.2.5在2023年9月6日生成的数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
# 定义依赖关系,依赖于用户模型和上一个迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
# 定义具体的操作
operations = [
# 修改Article模型的选项设置
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# 修改Category模型的选项设置
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# 修改Links模型的选项设置
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# 修改SideBar模型的选项设置
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# 修改Tag模型的选项设置
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 移除Article模型中的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
# 移除Article模型中的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# 移除Category模型中的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
# 移除Category模型中的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# 移除Links模型中的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# 移除SideBar模型中的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# 移除Tag模型中的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
# 移除Tag模型中的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# 为Article模型添加creation_time字段创建时间
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Article模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Category模型添加creation_time字段创建时间
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Category模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Links模型添加creation_time字段创建时间
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为SideBar模型添加creation_time字段创建时间
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加creation_time字段创建时间
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型的article_order字段显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
# 修改Article模型的author字段显示名称
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'),
),
# 修改Article模型的body字段显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
# 修改Article模型的category字段显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
# 修改Article模型的comment_status字段选项和显示名称
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'),
),
# 修改Article模型的pub_time字段显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
# 修改Article模型的show_toc字段显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
# 修改Article模型的status字段选项和显示名称
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
# 修改Article模型的tags字段显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
# 修改Article模型的title字段显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
# 修改Article模型的type字段选项和显示名称
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
# 修改Article模型的views字段显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的多个字段显示名称英文化
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的多个字段显示名称
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型的多个字段显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# 修改SideBar模型的多个字段显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的name字段显示名称
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -0,0 +1,20 @@
# 由Django 4.2.7在2024年1月26日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0005_alter_article_options_alter_category_options_and_more
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# 定义具体的操作
operations = [
# 修改BlogSettings模型的选项设置
migrations.AlterModelOptions(
name='blogsettings', # 目标模型名称
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, # 新的模型选项:将单数和复数形式的显示名称都改为"Website configuration"
),
]

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

@ -0,0 +1,21 @@
#zf:从haystack导入索引相关模块
from haystack import indexes
#zf:从blog.models导入Article模型
from blog.models import Article
#zf:定义文章索引类继承自SearchIndex和Indexable
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#zf:定义文本字段,作为文档字段,使用模板来确定索引内容
text = indexes.CharField(document=True, use_template=True)
#zf:获取模型类的方法
def get_model(self):
#zf:返回Article模型
return Article
#zf:定义索引查询集,确定哪些数据会被索引
def index_queryset(self, using=None):
#zf:返回所有状态为已发布('p')的文章
return self.get_model().objects.filter(status='p')

@ -0,0 +1,200 @@
#zf导入所需的Python标准库
#zf用于生成MD5哈希值
import hashlib
#zf用于日志记录
import logging
#zf用于随机选择
import random
#zf用于URL编码
import urllib
#zf导入Django相关模块
#zfDjango模板系统
from django import template
#zfDjango配置
from django.conf import settings
#zfDjango查询对象
from django.db.models import Q
#zf获取对象或返回404
from django.shortcuts import get_object_or_404
#zf字符串过滤器装饰器
from django.template.defaultfilters import stringfilter
#zf静态文件处理
from django.templatetags.static import static
#zfURL反向解析
from django.urls import reverse
#zf标记安全字符串
from django.utils.safestring import mark_safe
#zf导入项目相关模块
#zf博客模型
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
#zf评论模型
from comments.models import Comment
#zfMarkdown处理和HTML清理
from djangoblog.utils import CommonMarkdown, sanitize_html
#zf缓存工具
from djangoblog.utils import cache
#zf获取当前站点
from djangoblog.utils import get_current_site
#zfOAuth用户模型
from oauth.models import OAuthUser
#zf插件管理
from djangoblog.plugin_manage import hooks
#zf创建日志记录器
logger = logging.getLogger(__name__)
#zf注册模板标签库
register = template.Library()
#zf定义head_meta简单标签用于在模板中插入头部元信息
@register.simple_tag(takes_context=True)
def head_meta(context):
#zf应用插件过滤器返回安全的HTML字符串
return mark_safe(hooks.apply_filters('head_meta', '', context))
#zf定义timeformat简单标签用于格式化时间
@register.simple_tag
def timeformat(data):
try:
#zf使用settings中定义的时间格式格式化数据
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义datetimeformat简单标签用于格式化日期时间
@register.simple_tag
def datetimeformat(data):
try:
#zf使用settings中定义的日期时间格式格式化数据
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义custom_markdown过滤器用于将Markdown内容转换为HTML
@register.filter()
@stringfilter
def custom_markdown(content):
#zf使用CommonMarkdown工具转换Markdown并标记为安全HTML
return mark_safe(CommonMarkdown.get_markdown(content))
#zf定义get_markdown_toc简单标签用于获取Markdown内容的目录
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
#zf获取Markdown内容和目录
body, toc = CommonMarkdown.get_markdown_with_toc(content)
#zf标记目录为安全HTML并返回
return mark_safe(toc)
#zf定义comment_markdown过滤器用于处理评论中的Markdown内容
@register.filter()
@stringfilter
def comment_markdown(content):
#zf转换Markdown内容
content = CommonMarkdown.get_markdown(content)
#zf清理HTML并标记为安全
return mark_safe(sanitize_html(content))
#zf定义truncatechars_content过滤器用于截取文章内容摘要
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content: 文章内容
:return: 截取后的摘要
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
#zf获取博客设置
blogsetting = get_blog_setting()
#zf根据设置的文章摘要长度截取内容
return truncatechars_html(content, blogsetting.article_sub_length)
#zf定义truncate过滤器用于截取内容前150个字符
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
#zf去除HTML标签并截取前150个字符
return strip_tags(content)[:150]
#zf定义load_breadcrumb包含标签用于加载文章面包屑导航
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article: 文章对象
:return: 面包屑数据
"""
#zf获取文章的分类树
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
#zf获取博客设置和当前站点
blogsetting = get_blog_setting()
site = get_current_site().domain
#zf添加站点名称到面包屑
names.append((blogsetting.site_name, '/'))
#zf反转面包屑顺序
names = names[::-1]
return {
'names': names,
'title': article.title,
'count': len(names) + 1
}
#zf定义load_articletags包含标签用于加载文章标签列表
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article: 文章对象
:return: 标签列表数据
"""
#zf获取文章的所有标签
tags = article.tags.all()
tags_list = []
for tag in tags:
#zf获取标签URL和文章数量
url = tag.get_absolute_url()
count = tag.get_article_count()
#zf添加标签信息到列表随机选择Bootstrap颜色
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list
}
#zf定义load_sidebar包含标签用于加载侧边栏内容
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return: 侧边栏数据
"""
#zf尝试从缓存获取侧边栏数据
value = cache.get("sidebar" + linktype)
if value:
#zf如果缓存存在添加用户信息并返回
value['user'] = user
return value
else

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

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

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

@ -0,0 +1,247 @@
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
if isinstance(instance, LogEntry):
return
is_update_views = update_fields == {'views'}
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()

@ -1,56 +1,56 @@
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 _
#zr 禁用评论状态的管理动作
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zr 启用评论状态的管理动作
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zr 设置动作的描述信息
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
#zr 评论管理后台配置类
class CommentAdmin(admin.ModelAdmin):
#zr 设置每页显示数量
list_per_page = 20
#zr 设置列表页显示的字段
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
#zr 设置可点击链接的字段
list_display_links = ('id', 'body', 'is_enable')
#zr 设置过滤器字段
list_filter = ('is_enable',)
#zr 设置排除的表单字段
exclude = ('creation_time', 'last_modify_time')
#zr 设置可用的批量动作
actions = [disable_commentstatus, enable_commentstatus]
#zr 生成用户信息链接的方法
def link_to_userinfo(self, obj):
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))
#zr 生成文章链接的方法
def link_to_article(self, obj):
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))
#zr 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
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 _
#zr 禁用评论状态的管理动作
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zr 启用评论状态的管理动作
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zr 设置动作的描述信息
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
#zr 评论管理后台配置类
class CommentAdmin(admin.ModelAdmin):
#zr 设置每页显示数量
list_per_page = 20
#zr 设置列表页显示的字段
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
#zr 设置可点击链接的字段
list_display_links = ('id', 'body', 'is_enable')
#zr 设置过滤器字段
list_filter = ('is_enable',)
#zr 设置排除的表单字段
exclude = ('creation_time', 'last_modify_time')
#zr 设置可用的批量动作
actions = [disable_commentstatus, enable_commentstatus]
#zr 生成用户信息链接的方法
def link_to_userinfo(self, obj):
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))
#zr 生成文章链接的方法
def link_to_article(self, obj):
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))
#zr 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,6 +1,6 @@
from django.apps import AppConfig
#zr 评论应用配置类
class CommentsConfig(AppConfig):
#zr 定义应用名称
from django.apps import AppConfig
#zr 评论应用配置类
class CommentsConfig(AppConfig):
#zr 定义应用名称
name = 'comments'

@ -1,16 +1,16 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
#zr 评论表单类
class CommentForm(ModelForm):
#zr 父评论ID字段隐藏输入且非必需
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
#zr 指定关联的模型
model = Comment
#zr 指定表单包含的字段
from django import forms
from django.forms import ModelForm
from .models import Comment
#zr 评论表单类
class CommentForm(ModelForm):
#zr 父评论ID字段隐藏输入且非必需
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
#zr 指定关联的模型
model = Comment
#zr 指定表单包含的字段
fields = ['body']

@ -1,52 +1,52 @@
#zr 初始数据库迁移文件:创建评论表结构
# 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
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 初始迁移
initial = True
#zr 依赖关系
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#zr 迁移操作
operations = [
#zr 创建评论表
migrations.CreateModel(
name='Comment',
fields=[
#zr 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#zr 评论正文字段
('body', models.TextField(max_length=300, verbose_name='正文')),
#zr 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#zr 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#zr 是否显示字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#zr 文章外键关联
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#zr 作者外键关联
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#zr 父评论自关联
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#zr 模型元选项
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]
#zr 初始数据库迁移文件:创建评论表结构
# 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
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 初始迁移
initial = True
#zr 依赖关系
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#zr 迁移操作
operations = [
#zr 创建评论表
migrations.CreateModel(
name='Comment',
fields=[
#zr 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#zr 评论正文字段
('body', models.TextField(max_length=300, verbose_name='正文')),
#zr 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#zr 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#zr 是否显示字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#zr 文章外键关联
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#zr 作者外键关联
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#zr 父评论自关联
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#zr 模型元选项
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]

@ -1,23 +1,23 @@
#zr 数据库迁移文件:修改评论是否显示字段的默认值
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
('comments', '0001_initial'),
]
#zr 迁移操作
operations = [
#zr 修改comment模型的is_enable字段
migrations.AlterField(
model_name='comment',
name='is_enable',
#zr 将默认值改为False并更新显示名称
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]
#zr 数据库迁移文件:修改评论是否显示字段的默认值
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
('comments', '0001_initial'),
]
#zr 迁移操作
operations = [
#zr 修改comment模型的is_enable字段
migrations.AlterField(
model_name='comment',
name='is_enable',
#zr 将默认值改为False并更新显示名称
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -1,73 +1,73 @@
#zr 数据库迁移文件:更新评论模型字段和选项
# 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
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#zr 迁移操作列表
operations = [
#zr 更新评论模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#zr 移除旧的创建时间字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#zr 移除旧的最后修改时间字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#zr 添加新的创建时间字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#zr 添加新的最后修改时间字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#zr 更新文章外键字段配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#zr 更新作者外键字段配置
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'),
),
#zr 更新是否启用字段配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#zr 更新父评论外键字段配置
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'),
),
]
#zr 数据库迁移文件:更新评论模型字段和选项
# 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
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#zr 迁移操作列表
operations = [
#zr 更新评论模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#zr 移除旧的创建时间字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#zr 移除旧的最后修改时间字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#zr 添加新的创建时间字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#zr 添加新的最后修改时间字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#zr 更新文章外键字段配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#zr 更新作者外键字段配置
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'),
),
#zr 更新是否启用字段配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#zr 更新父评论外键字段配置
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,48 +1,48 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
#zr 评论数据模型
class Comment(models.Model):
#zr 评论正文最大长度300字符
body = models.TextField('正文', max_length=300)
#zr 评论创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#zr 评论最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zr 评论作者,关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
#zr 关联的文章
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
#zr 父级评论,支持评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#zr 评论是否启用显示
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
#zr 按ID降序排列
ordering = ['-id']
#zr 设置单数和复数显示名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zr 指定最新记录的依据字段
get_latest_by = 'id'
def __str__(self):
#zr 返回评论正文作为字符串表示
return self.body
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
#zr 评论数据模型
class Comment(models.Model):
#zr 评论正文最大长度300字符
body = models.TextField('正文', max_length=300)
#zr 评论创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#zr 评论最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zr 评论作者,关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
#zr 关联的文章
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
#zr 父级评论,支持评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#zr 评论是否启用显示
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
#zr 按ID降序排列
ordering = ['-id']
#zr 设置单数和复数显示名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zr 指定最新记录的依据字段
get_latest_by = 'id'
def __str__(self):
#zr 返回评论正文作为字符串表示
return self.body

@ -1,37 +1,37 @@
from django import template
#zr 注册模板标签库
register = template.Library()
#zr 解析评论树的模板标签
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
#zr 递归解析子评论的内部函数
def parse(c):
#zr 获取当前评论的直接子评论
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
#zr 将子评论添加到结果列表
datas.append(child)
#zr 递归解析子评论的子评论
parse(child)
#zr 从传入的评论开始解析
parse(comment)
return datas
#zr 显示评论项的包含标签
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
#zr 根据是否为子评论设置不同的深度
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
}
from django import template
#zr 注册模板标签库
register = template.Library()
#zr 解析评论树的模板标签
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
#zr 递归解析子评论的内部函数
def parse(c):
#zr 获取当前评论的直接子评论
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
#zr 将子评论添加到结果列表
datas.append(child)
#zr 递归解析子评论的子评论
parse(child)
#zr 从传入的评论开始解析
parse(comment)
return datas
#zr 显示评论项的包含标签
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
#zr 根据是否为子评论设置不同的深度
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
}

@ -1,131 +1,131 @@
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
# zr 评论模块测试类
class CommentsTest(TransactionTestCase):
# zr 测试初始化设置
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# zr 导入并设置博客配置,开启评论审核
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
# zr 创建测试用的超级用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# zr 更新文章评论状态为启用
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
# zr 测试评论验证功能
def test_validate_comment(self):
# zr 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# zr 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# zr 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# zr 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# zr 测试提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302)
# zr 验证评论初始状态为未显示(需要审核)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# zr 更新评论状态后验证评论显示
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
# zr 测试提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
# zr 验证第二条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# zr 获取父评论ID用于回复测试
parent_comment_id = article.comment_list()[0].id
# zr 测试回复评论包含Markdown格式内容
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
# zr 验证回复评论成功
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
# zr 测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
# zr 测试评论项显示功能
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# zr 测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
# zr 测试发送评论邮件功能
from comments.utils import send_comment_email
send_comment_email(comment)
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
# zr 评论模块测试类
class CommentsTest(TransactionTestCase):
# zr 测试初始化设置
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# zr 导入并设置博客配置,开启评论审核
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
# zr 创建测试用的超级用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# zr 更新文章评论状态为启用
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
# zr 测试评论验证功能
def test_validate_comment(self):
# zr 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# zr 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# zr 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# zr 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# zr 测试提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302)
# zr 验证评论初始状态为未显示(需要审核)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# zr 更新评论状态后验证评论显示
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
# zr 测试提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
# zr 验证第二条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# zr 获取父评论ID用于回复测试
parent_comment_id = article.comment_list()[0].id
# zr 测试回复评论包含Markdown格式内容
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
# zr 验证回复评论成功
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
# zr 测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
# zr 测试评论项显示功能
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# zr 测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
# zr 测试发送评论邮件功能
from comments.utils import send_comment_email
send_comment_email(comment)

@ -1,14 +1,14 @@
from django.urls import path
from . import views
#zr 定义评论应用的命名空间
app_name = "comments"
#zr 评论模块URL路由配置
urlpatterns = [
#zr 文章评论提交路由
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]
from django.urls import path
from . import views
#zr 定义评论应用的命名空间
app_name = "comments"
#zr 评论模块URL路由配置
urlpatterns = [
#zr 文章评论提交路由
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -1,50 +1,50 @@
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# zr 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# zr 发送评论邮件功能
def send_comment_email(comment):
# zr 获取当前站点域名
site = get_current_site().domain
# zr 设置邮件主题
subject = _('Thanks for your comment')
# zr 构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# zr 构建给评论作者的邮件内容
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}
# zr 获取评论作者邮箱并发送邮件
tomail = comment.author.email
send_email([tomail], subject, html_content)
# zr 如果是回复评论,同时发送邮件给被回复的评论作者
try:
if comment.parent_comment:
# zr 构建回复通知邮件内容
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}
# zr 获取被回复评论作者的邮箱并发送通知
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
# zr 记录邮件发送异常
logger.error(e)
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# zr 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# zr 发送评论邮件功能
def send_comment_email(comment):
# zr 获取当前站点域名
site = get_current_site().domain
# zr 设置邮件主题
subject = _('Thanks for your comment')
# zr 构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# zr 构建给评论作者的邮件内容
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}
# zr 获取评论作者邮箱并发送邮件
tomail = comment.author.email
send_email([tomail], subject, html_content)
# zr 如果是回复评论,同时发送邮件给被回复的评论作者
try:
if comment.parent_comment:
# zr 构建回复通知邮件内容
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}
# zr 获取被回复评论作者的邮箱并发送通知
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
# zr 记录邮件发送异常
logger.error(e)

@ -1,81 +1,81 @@
# zr 评论视图模块
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
# zr 评论提交视图类
class CommentPostView(FormView):
# zr 使用评论表单类
form_class = CommentForm
# zr 指定模板名称
template_name = 'blog/article_detail.html'
# zr 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# zr 处理GET请求重定向到文章详情页
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")
# zr 处理表单验证失败的情况
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
})
# zr 处理表单验证成功的情况
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# zr 获取当前用户信息
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
# zr 获取文章信息
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# zr 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# zr 创建评论对象但不立即保存到数据库
comment = form.save(False)
comment.article = article
# zr 获取博客设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
# zr 处理回复评论的情况
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# zr 保存评论到数据库
comment.save(True)
# zr 重定向到文章页面并定位到新评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
# zr 评论视图模块
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
# zr 评论提交视图类
class CommentPostView(FormView):
# zr 使用评论表单类
form_class = CommentForm
# zr 指定模板名称
template_name = 'blog/article_detail.html'
# zr 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# zr 处理GET请求重定向到文章详情页
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")
# zr 处理表单验证失败的情况
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
})
# zr 处理表单验证成功的情况
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# zr 获取当前用户信息
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
# zr 获取文章信息
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# zr 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# zr 创建评论对象但不立即保存到数据库
comment = form.save(False)
comment.article = article
# zr 获取博客设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
# zr 处理回复评论的情况
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# zr 保存评论到数据库
comment.save(True)
# zr 重定向到文章页面并定位到新评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -0,0 +1,48 @@
version: '3'
services:
es:
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
container_name: es
restart: always
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- 9200:9200
volumes:
- ./bin/datas/es/:/usr/share/elasticsearch/data/
kibana:
image: kibana:8.6.1
restart: always
container_name: kibana
ports:
- 5601:5601
environment:
- ELASTICSEARCH_HOSTS=http://es:9200
djangoblog:
build: .
restart: always
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
volumes:
- ./collectedstatic:/code/djangoblog/collectedstatic
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_MEMCACHED_LOCATION=memcached:11211
- DJANGO_ELASTICSEARCH_HOST=es:9200
links:
- db
- memcached
depends_on:
- db
container_name: djangoblog

@ -0,0 +1,60 @@
version: '3'
services:
db:
image: mysql:latest
restart: always
environment:
- MYSQL_DATABASE=djangoblog
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
ports:
- 3306:3306
volumes:
- ./bin/datas/mysql/:/var/lib/mysql
depends_on:
- redis
container_name: db
djangoblog:
build:
context: ../../
restart: always
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
volumes:
- ./collectedstatic:/code/djangoblog/collectedstatic
- ./logs:/code/djangoblog/logs
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_REDIS_URL=redis:6379
links:
- db
- redis
depends_on:
- db
container_name: djangoblog
nginx:
restart: always
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./bin/nginx.conf:/etc/nginx/nginx.conf
- ./collectedstatic:/code/djangoblog/collectedstatic
links:
- djangoblog:djangoblog
container_name: nginx
redis:
restart: always
image: redis:latest
container_name: redis
ports:
- "6379:6379"

@ -0,0 +1,31 @@
#!/usr/bin/env bash
NAME="djangoblog"
DJANGODIR=/code/djangoblog
USER=root
GROUP=root
NUM_WORKERS=1
DJANGO_WSGI_MODULE=djangoblog.wsgi
echo "Starting $NAME as `whoami`"
cd $DJANGODIR
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
python manage.py makemigrations && \
python manage.py migrate && \
python manage.py collectstatic --noinput && \
python manage.py compress --force && \
python manage.py build_index && \
python manage.py compilemessages || exit 1
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind 0.0.0.0:8000 \
--log-level=debug \
--log-file=- \
--worker-class gevent \
--threads 4

@ -0,0 +1,119 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: web-nginx-config
namespace: djangoblog
data:
nginx.conf: |
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
# Include server configurations
include /etc/nginx/conf.d/*.conf;
}
djangoblog.conf: |
server {
server_name lylinux.net;
root /code/djangoblog/collectedstatic/;
listen 80;
keepalive_timeout 70;
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
root /resource/djangopub;
expires 1d;
access_log off;
error_log off;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
server {
server_name www.lylinux.net;
listen 80;
return 301 https://lylinux.net$request_uri;
}
resource.lylinux.net.conf: |
server {
index index.html index.htm;
server_name resource.lylinux.net;
root /resource/;
location /djangoblog/ {
alias /code/djangoblog/collectedstatic/;
}
access_log off;
error_log off;
include lylinux/resource.conf;
}
lylinux.resource.conf: |
expires max;
access_log off;
log_not_found off;
add_header Pragma public;
add_header Cache-Control "public";
add_header "Access-Control-Allow-Origin" "*";
---
apiVersion: v1
kind: ConfigMap
metadata:
name: djangoblog-env
namespace: djangoblog
data:
DJANGO_MYSQL_DATABASE: djangoblog
DJANGO_MYSQL_USER: db_user
DJANGO_MYSQL_PASSWORD: db_password
DJANGO_MYSQL_HOST: db_host
DJANGO_MYSQL_PORT: db_port
DJANGO_REDIS_URL: "redis:6379"
DJANGO_DEBUG: "False"
MYSQL_ROOT_PASSWORD: db_password
MYSQL_DATABASE: djangoblog
MYSQL_PASSWORD: db_password
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

@ -0,0 +1,274 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: djangoblog
namespace: djangoblog
labels:
app: djangoblog
spec:
replicas: 3
selector:
matchLabels:
app: djangoblog
template:
metadata:
labels:
app: djangoblog
spec:
containers:
- name: djangoblog
image: liangliangyy/djangoblog:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
- name: resource
mountPath: /resource
volumes:
- name: djangoblog
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource
persistentVolumeClaim:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: djangoblog
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 200m
memory: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: djangoblog
labels:
app: db
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mysql:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
exec:
command:
- mysqladmin
- ping
- "-h"
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec:
command:
- mysqladmin
- ping
- "-h"
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
volumes:
- name: db-data
persistentVolumeClaim:
claimName: db-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: djangoblog
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: djangoblog.conf
- name: nginx-config
mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
subPath: resource.lylinux.net.conf
- name: nginx-config
mountPath: /etc/nginx/lylinux/resource.conf
subPath: lylinux.resource.conf
- name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic
- name: resource-pvc
mountPath: /resource
volumes:
- name: nginx-config
configMap:
name: web-nginx-config
- name: djangoblog-pvc
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource-pvc
persistentVolumeClaim:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
namespace: djangoblog
labels:
app: elasticsearch
spec:
replicas: 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
imagePullPolicy: IfNotPresent
env:
- name: discovery.type
value: single-node
- name: ES_JAVA_OPTS
value: "-Xms256m -Xmx256m"
- name: xpack.security.enabled
value: "false"
- name: xpack.monitoring.templates.enabled
value: "false"
ports:
- containerPort: 9200
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
readinessProbe:
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
livenessProbe:
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc

@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
namespace: djangoblog
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80

@ -0,0 +1,94 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-db
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-db
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-djangoblog
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-djangoblog
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-resource
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/resource/
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-elasticsearch
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-elasticsearch
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master

@ -0,0 +1,60 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-db
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: djangoblog-pvc
namespace: djangoblog
spec:
volumeName: local-pv-djangoblog
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-pvc
namespace: djangoblog
spec:
volumeName: local-pv-resource
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elasticsearch-pvc
namespace: djangoblog
spec:
volumeName: local-pv-elasticsearch
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi

@ -0,0 +1,80 @@
apiVersion: v1
kind: Service
metadata:
name: djangoblog
namespace: djangoblog
labels:
app: djangoblog
spec:
selector:
app: djangoblog
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: djangoblog
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: djangoblog
labels:
app: redis
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: djangoblog
labels:
app: db
spec:
selector:
app: db
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: djangoblog
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
ports:
- protocol: TCP
port: 9200
targetPort: 9200
type: ClusterIP

@ -0,0 +1,10 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate

@ -0,0 +1,50 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
server {
root /code/djangoblog/collectedstatic/;
listen 80;
keepalive_timeout 70;
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
}

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,64 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()

@ -0,0 +1,247 @@
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
if isinstance(instance, LogEntry):
return
is_update_views = update_fields == {'views'}
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()

@ -0,0 +1,183 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
def author_name(self):
return get_user_model().objects.first().nickname
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
return item.get_absolute_url()
def item_guid(self, item):
return

@ -0,0 +1,91 @@
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
list_filter = [
'content_type'
]
search_fields = [
'object_repr',
'change_message'
]
list_display_links = [
'action_time',
'get_change_message',
]
list_display = [
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
pass
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

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

Loading…
Cancel
Save