提交sxc代码注释

sxc_branch
p6lh3f7qt 3 months ago
parent e84d247636
commit aef3fa193a

@ -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,59 @@
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):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
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):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

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

@ -0,0 +1,117 @@
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):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
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):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)

@ -0,0 +1,49 @@
# 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):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, 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='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,46 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -0,0 +1,35 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -0,0 +1,207 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
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()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
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',
})
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
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)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
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):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # 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):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -0,0 +1,28 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

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

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

@ -0,0 +1,204 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
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)
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):
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
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):
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)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
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):
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):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
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):
def post(self, request: HttpRequest):
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,137 @@
#sxc 该模块是 Django 博客系统的后台管理配置文件核心功能是为博客核心模型Article、Tag、Category 等)注册 Django Admin 管理界面,并通过自定义 Admin 类、表单和批量操作,实现对博客内容的可视化管理,包括文章发布 / 草稿切换、评论状态控制、分类标签管理等功能
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
#sxc 自定义批量操作函数:将选中的文章状态批量更新为 “发布”status='p'
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
#sxc 自定义批量操作函数:将选中的文章状态批量更新为 “草稿”status='d'
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
#sxc 自定义批量操作函数:将选中的文章评论状态批量更新为 “关闭”comment_status='c'
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
#sxc 自定义批量操作函数:将选中的文章评论状态批量更新为 “打开”comment_status='o'
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
#sxc 为批量操作函数设置后台显示名称支持国际化_() 函数)
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')
class ArticlelAdmin(admin.ModelAdmin):
#sxc 文章模型的 Admin 管理类,自定义后台文章列表和编辑页的显示与功能
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
#sxc 列表页显示的字段涵盖文章核心信息ID、标题、作者、分类、创建时间、浏览量、状态、类型、排序
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
#sxc 列表页支持的批量操作,关联前面定义的 4 个批量函数
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
#sxc 自定义列表字段:将文章所属分类转为可点击链接,点击跳转到分类的编辑页
#sxc 获取分类模型的 APP 标签和模型名,用于生成 Admin 后台的 URL
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
#sxc 生成 HTML 链接format_html 确保内容安全渲染(避免 XSS
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
#sxc 重写表单获取方法:限制 “作者” 字段的可选范围为超级用户
#sxc 调用父类方法获取默认表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
#sxc 筛选作者下拉框选项,仅显示超级用户(避免普通用户被选为作者)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
#sxc 重写模型保存方法,可扩展自定义保存逻辑(如自动记录操作日志)
#sxc 此处保留默认保存逻辑,如需扩展可在 super 前后添加代码
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
if obj:
#sxc 若有文章对象,调用文章模型的 get_full_url 方法生成前台链接
url = obj.get_full_url()
return url
else:
#sxc 若无对象(如列表页),返回当前站点的域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
#sxc 标签模型的 Admin 管理类
#sxc 编辑页隐藏自动生成的字段slugURL 友好标识)、创建时间、最后修改时间
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
#sxc 列表页显示:分类名、父分类、权重排序
#sxc 编辑页隐藏自动生成的字段slug、创建时间、最后修改时间
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
#sxc 友情链接模型的 Admin 管理类
#sxc 编辑页隐藏自动生成的字段:创建时间、最后修改时间
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
#sxc 侧边栏模型的 Admin 管理类
#sxc 列表页显示:侧边栏标题、内容、是否启用、排序序号
list_display = ('name', 'content', 'is_enable', 'sequence')
#sxc 编辑页隐藏自动生成的字段:创建时间、最后修改时间
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
#sxc 网站配置模型的 Admin 管理类
#sxc 未添加额外配置,使用 Django Admin 默认的显示和操作逻辑(适合字段较少的配置类)
pass

@ -0,0 +1,8 @@
#sxc 该模块是 Django 博客应用的配置类模块用于定义博客应用blog的基本配置信息使 Django 能够识别并正确加载该应用
from django.apps import AppConfig
class BlogConfig(AppConfig):
#sxc 博客应用的配置类,继承自 Django 的 AppConfig
#sxc 定义应用的名称为 'blog'Django 通过该名称识别并管理该应用
name = 'blog'

@ -0,0 +1,51 @@
#sxc 该模块是 Django 博客系统的上下文处理器模块,用于为所有模板提供全局可用的 SEO 相关配置和通用数据,避免在每个视图中重复传递这些数据,提升模板复用性和开发效率
#sxc 集成缓存机制减少数据库查询,包含网站基本设置、导航数据、评论配置等关键信息
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
#sxc 上下文处理器函数:向所有模板提供全局 SEO 及网站配置数据
#sxc 使用缓存减少数据库访问,提升性能,缓存键为'seo_processor'
key = 'seo_processor'
value = cache.get(key)
if value:
return value #sxc 缓存命中:直接返回缓存的配置数据
else:
logger.info('set processor cache.')
setting = get_blog_setting()
#sxc 组装需要传递给模板的全局数据字典
value = {
'SITE_NAME': setting.site_name, #sxc 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,#sxc 是否显示谷歌广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,#sxc 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description,#sxc 网站 SEO 描述(用于搜索引擎)
'SITE_DESCRIPTION': setting.site_description,#sxc 网站描述
'SITE_KEYWORDS': setting.site_keywords,#sxc 网站关键字(用于 SEO
#sxc 网站基础 URL如https://example.com/
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,#sxc 文章摘要长度
'nav_category_list': Category.objects.all(),#sxc 导航栏分类列表
#sxc 导航栏页面列表(仅包含已发布的页面类型文章)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,#sxc 是否开启网站评论功能
'BEIAN_CODE': setting.beian_code,#sxc 网站备案号
'ANALYTICS_CODE': setting.analytics_code,#sxc 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,#sxc 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code,#sxc 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, #sxc 当前年份(用于页脚版权信息)
"GLOBAL_HEADER": setting.global_header,#sxc 公共头部 HTML 代码
"GLOBAL_FOOTER": setting.global_footer,#sxc 公共尾部 HTML 代码
"COMMENT_NEED_REVIEW": setting.comment_need_review,#sxc 评论是否需要审核
}
#sxc 设置缓存,有效期 10 小时606010 秒)
cache.set(key, value, 60 * 60 * 10)
return value

@ -0,0 +1,240 @@
#sxc 该模块是 Django 博客系统的 Elasticsearch 集成配置模块,核心功能是定义 Elasticsearch 文档模型(性能日志、文章)、实现文档的创建 / 更新 / 删除等管理操作,同时配置 IP 地理信息解析管道,为博客提供高性能的全文搜索和访问性能监控能力
#sxc 依赖 elasticsearch-dsl 库,支持条件启用(通过 settings.ELASTICSEARCH_DSL 配置),包含文档模型定义、索引管理、数据同步三大核心模块
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
#sxc 判断项目是否配置 Elasticsearch控制后续功能是否启用
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
#sxc 建立与 Elasticsearch 的连接,使用项目配置的地址
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
#sxc 检查是否存在 "geoip" 管道(用于解析 IP 地理信息)
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
#sxc 若不存在则创建 "geoip" 管道,定义 IP 解析逻辑(从 ip 字段提取地理信息)
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
#sxc 内部文档类:定义 IP 地理信息的字段结构,用于嵌套在性能日志文档中
continent_name = Keyword()#sxc 洲名
country_iso_code = Keyword()#sxc 国家 ISO 代码(如 CN、US
country_name = Keyword()#sxc 国家名称
location = GeoPoint()#sxc 地理位置坐标(经纬度)
class UserAgentBrowser(InnerDoc):
#sxc 内部文档类:定义用户代理中的浏览器信息结构
Family = Keyword()#sxc 浏览器家族(如 Chrome、Safari
Version = Keyword() #sxc 浏览器版本
class UserAgentOS(UserAgentBrowser):
#sxc 内部文档类:定义用户代理中的操作系统信息结构(继承浏览器类字段)
pass
class UserAgentDevice(InnerDoc):
#sxc 内部文档类:定义用户代理中的设备信息结构
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
#sxc 内部文档类定义完整的用户代理信息结构嵌套浏览器、OS、设备信息
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document):
#sxc Elasticsearch 文档类:定义访问性能日志模型,用于记录页面加载耗时等信息
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
#sxc 定义索引配置:索引名为 "performance",分片 1 个、副本 0 个(单节点环境优化)
name = 'performance'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager:
#sxc 性能日志文档管理器:提供索引创建、删除、日志写入等静态方法
@staticmethod
def build_index():
#sxc 静态方法:创建 "performance" 索引(若不存在)
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
#sxc 静态方法:删除 "performance" 索引(忽略 400/404 错误:索引不存在或删除失败)
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
#sxc 静态方法:创建一条性能日志文档,并应用 geoip 管道解析 IP 地理信息
ElaspedTimeDocumentManager.build_index()
#sxc 组装用户代理信息(从传入的 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
#sxc 创建性能日志文档ID 用时间戳(毫秒级)确保唯一,关联 geoip 管道
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
#sxc Elasticsearch 文档类:定义博客文章模型,用于全文搜索
#sxc 正文和标题使用 IK 分词器ik_max_word细粒度分词ik_smart粗粒度搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#sxc 作者信息(嵌套 Object包含昵称和 ID昵称支持分词
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 分类信息(嵌套 Object包含名称和 ID名称支持分词
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 标签信息(嵌套 Object 列表:包含名称和 ID名称支持分词
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 文章其他字段:发布时间、状态、评论状态、类型、浏览量、排序
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
#sxc 定义索引配置:索引名为 "blog",分片 1 个、副本 0 个(单节点环境优化)
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
#sxc 文章文档管理器:提供索引创建、删除、数据同步(重建 / 更新)等实例方法
class ArticleDocumentManager():
def __init__(self):
self.create_index()
def create_index(self):
#sxc 实例方法:初始化 "blog" 索引(根据 ArticleDocument 定义创建)
ArticleDocument.init()
def delete_index(self):
#sxc 实例方法:删除 "blog" 索引(忽略 400/404 错误)
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
#sxc 实例方法:将 Django Article 模型对象列表转换为 Elasticsearch ArticleDocument 列表
return [
ArticleDocument(
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
#sxc 实例方法:重建文章索引(全量同步或指定文章同步)
ArticleDocument.init()
#sxc 若未指定文章列表,则同步所有 Article 模型数据
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()
def update_docs(self, docs):
for doc in docs:
doc.save()

@ -0,0 +1,25 @@
#sxc 该模块是 Django 博客系统的搜索表单模块,基于 Haystack 搜索框架扩展自定义搜索表单,用于接收和验证用户的搜索请求,同时记录搜索关键词日志,确保搜索功能的合法性和可追溯性
import logging
from django import forms
from haystack.forms import SearchForm #sxc 导入 Haystack 框架的基础搜索表单类
logger = logging.getLogger(__name__) #sxc 初始化日志记录器,用于记录搜索相关日志
class BlogSearchForm(SearchForm):
#sxc 自定义博客搜索表单类,继承自 Haystack 的 SearchForm扩展搜索功能
#sxc 定义搜索输入字段querydata必填用于接收用户输入的搜索关键词
querydata = forms.CharField(required=True)
def search(self):
#sxc 重写父类 search 方法:执行搜索逻辑,包含表单验证和日志记录
#sxc 调用父类 search 方法,获取初始搜索结果集
datas = super(BlogSearchForm, self).search()
#sxc 表单验证:若表单数据无效,返回 “无搜索结果” 的默认响应
if not self.is_valid():
return self.no_query_found()
#sxc 日志记录:若存在有效搜索关键词,记录关键词到日志(便于分析用户搜索行为)
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
#sxc 返回搜索结果集(若验证通过且有关键词,返回匹配结果;否则返回空结果)
return datas

@ -0,0 +1,24 @@
#sxc 该模块定义了 Django 自定义管理命令,用于在 Elasticsearch 中构建和重建博客系统的搜索索引,支持文章数据和耗时统计数据的索引管理,仅在 Elasticsearch 启用时执行操作。
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
class Command(BaseCommand):
#sxc 定义命令的帮助信息使用python manage.py help build_index可查看
help = 'build search index'
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:检查 Elasticsearch 是否启用,若启用则执行索引构建操作
if ELASTICSEARCH_ENABLED:
#sxc 构建耗时统计文档的索引(如文章访问耗时等数据)
ElaspedTimeDocumentManager.build_index()
#sxc 初始化耗时统计文档的索引结构
manager = ElapsedTimeDocument()
manager.init() #sxc 创建索引映射mapping
#sxc 处理文章文档的索引:先删除旧索引,再重建新索引
manager = ArticleDocumentManager()
manager.delete_index() #sxc 删除已存在的文章索引(避免数据冲突)
manager.rebuild() #sxc 从数据库同步文章数据并重建索引

@ -0,0 +1,20 @@
#sxc 该模块定义了 Django 自定义管理命令用于提取博客系统中所有标签Tag和分类Category的名称去重后输出为搜索词列表可用于搜索提示、自动补全等功能的数据源。
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
#sxc 定义命令的帮助信息通过python manage.py help build_search_words可查看
help = 'build search words' #sxc 命令功能描述:生成搜索词列表
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:提取标签和分类名称,去重后输出
#sxc 1. 提取所有标签名称并转换为列表
#sxc 2. 提取所有分类名称并转换为列表
#sxc 3. 合并两个列表并通过 set 去重,得到唯一的搜索词集合
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
#sxc 按行输出所有搜索词(每行一个词),便于后续处理或导入
print('\n'.join(datas))

@ -0,0 +1,15 @@
#sxc 该模块定义了 Django 自定义管理命令,用于清空系统缓存,通过调用缓存工具类的清除方法,快速清理所有缓存数据,适用于缓存数据过期或异常时的手动运维操作。
from django.core.management.base import BaseCommand
from djangoblog.utils import cache #sxc 导入系统缓存工具类
class Command(BaseCommand):
#sxc 命令帮助信息执行python manage.py help clear_cache时显示
help = 'clear the whole cache' #sxc 命令功能描述:清空所有缓存
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:调用缓存工具类的 clear 方法清空所有缓存
cache.clear()
#sxc 输出操作成功的提示信息(绿色成功标识)
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -0,0 +1,50 @@
#sxc 该模块定义了 Django 自定义管理命令,用于快速生成博客系统所需的测试数据,包括测试用户、分类、标签及文章,适用于开发调试和功能验证场景,生成数据后会自动清理缓存确保数据生效。
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand):
#sxc 命令帮助信息执行python manage.py help create_testdata时显示
help = 'create test datas' #sxc 命令功能描述:生成测试数据
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:创建测试用户、分类、标签和文章数据
#sxc 创建或获取测试用户邮箱test@test.com用户名测试用户密码test!q@w#eTYU
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
#sxc 创建或获取父分类(名称:我是父类目,无父分类)
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
#sxc 创建或获取子分类(名称:子类目,父分类为 pcategory
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save() #sxc 保存子分类
#sxc 创建基础标签(名称:标签)
basetag = Tag()
basetag.name = "标签"
basetag.save()
#sxc 循环创建 19 篇测试文章(标题和内容带序号区分)
for i in range(1, 20):
#sxc 创建或获取取文章(关联子分类、测试用户,标题和内容动态生成)
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i), #sxc 文章标题nice title 1~19
body='nice content ' + str(i), #sxc 文章内容nice content 1~19
author=user)[0]
#sxc 为每篇文章创建专属标签(名称:标签 1~19
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
#sxc 为文章关联专属标签和基础标签
article.tags.add(tag)
article.tags.add(basetag)
article.save()
#sxc 清理系统缓存,确保新生成的测试数据能被及时加载
from djangoblog.utils import cache
cache.clear()
#sxc 输出操作成功的提示信息(绿色成功标识)
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,59 @@
#sxc 该模块定义了 Django 自定义管理命令,用于向百度搜索引擎推送博客系统的 URL支持文章、标签、分类及全量 URL助力搜索引擎快速收录内容提升博客内容的检索曝光率。
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
#sxc 获取当前站点的域名如example.com用于拼接完整 URL
site = get_current_site().domain
class Command(BaseCommand):
#sxc 命令帮助信息执行python manage.py help ping_baidu时显示
help = 'notify baidu url' #sxc 命令功能描述:向百度推送 URL
def add_arguments(self, parser):
#sxc 为命令添加参数,指定需推送的 URL 类型,限制可选值避免非法输入
parser.add_argument(
'data_type', #sxc 参数名:数据类型(需推送的 URL 类别)
type=str,
choices=[
'all', #sxc 选项 1推送所有类型 URL文章 + 标签 + 分类)
'article',#sxc 选项 2仅推送文章 URL
'tag',#sxc 选项 3仅推送标签 URL
'category'], #sxc 选项 4仅推送分类 URL
help='article : all article,tag : all tag,category: all category,all: All of these') #sxc 参数说明
def get_full_url(self, path):
#sxc 拼接完整 URL将相对路径如 /article/1/)与站点域名组合为绝对 URL
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:根据参数筛选 URL → 拼接完整 URL → 推送百度
type = options['data_type'] #sxc 获取用户指定的 URL 推送类型
self.stdout.write('start get %s' % type) #sxc 输出日志:开始获取指定类型的 URL
urls = [] #sxc 存储待推送的完整 URL 列表
#sxc 1. 筛选文章 URL仅状态为 “已发布p” 的文章)
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())#sxc 文章模型自带完整 URL 方法,直接获取
#sxc 2. 筛选标签 URL通过相对路径拼接完整 URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url() #sxc 获取标签的相对路径(如 /tag/python/
urls.append(self.get_full_url(url)) #sxc 拼接为完整 URL 并添加到列表
#sxc 3. 筛选分类 URL通过相对路径拼接完整 URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url() #sxc 获取分类的相对路径(如 /category/tech/
urls.append(self.get_full_url(url)) #sxc 拼接为完整 URL 并添加到列表
#sxc 输出日志:展示待推送的 URL 数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
#sxc 调用爬虫通知工具类,向百度推送筛选后的 URL 列表
SpiderNotify.baidu_notify(urls)
#sxc 输出成功日志:推送完成
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,58 @@
#sxc 该模块定义了 Django 自定义管理命令用于同步第三方登录用户OAuthUser的头像图片。通过检测头像 URL 有效性,自动更新失效头像,支持从第三方平台重新获取或使用默认头像,确保用户头像展示正常。
import requests
from django.core.management.base import BaseCommand
from django.templatetags.static import static
from djangoblog.utils import save_user_avatar
from oauth.models import OAuthUser
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
#sxc 命令帮助信息执行python manage.py help sync_user_avatar时显示
help = 'sync user avatar' #sxc 命令功能描述:同步用户头像
def test_picture(self, url):
#sxc 检测头像 URL 是否有效(状态码 200
try:
#sxc 发送 GET 请求检测 URL超时时间 2 秒
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass #sxc 捕获请求异常(如超时、连接失败等),视为无效
def handle(self, *args, **options):
#sxc 命令核心执行逻辑:遍历所有第三方用户,检测并同步头像
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
#sxc 判断头像是否为本地静态文件
if url.startswith(static_url):
#sxc 检测本地头像 URL 有效性
if self.test_picture(url):
continue
else:
if u.metadata:
#sxc 获取对应平台的 OAuth 管理器(如 QQ、GitHub 等)
manage = get_manager_by_type(u.type)
#sxc 从元数据中重新获取头像 URL
url = manage.get_picture(u.metadata)
#sxc 下载并保存头像,返回本地存储 URL
url = save_user_avatar(url)
else: #sxc 无元数据时使用默认头像
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
#sxc 更新用户头像并保存
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')#sxc 输出同步结果
u.picture = url
u.save() #sxc 保存用户头像更新
self.stdout.write('结束同步') #sxc 输出同步完成日志

@ -0,0 +1,53 @@
#sxc 该模块是 Django 博客系统的自定义中间件,核心功能是监控页面渲染耗时、收集访问用户的 IP 和设备信息,并在启用 Elasticsearch 时将这些性能数据写入日志索引,同时支持在页面中替换渲染耗时占位符,实现性能监控和数据统计
import logging
import time
from ipware import get_client_ip#sxc 导入 IP 获取工具,用于获取访问者真实 IP
from user_agents import parse #sxc 导入用户代理解析工具,用于识别设备 / 浏览器信息
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager #sxc 导入 Elasticsearch 相关配置和管理器
logger = logging.getLogger(__name__) #sxc 初始化日志记录器,用于记录中间件运行中的错误
class OnlineMiddleware(object):
#sxc 自定义中间件类:用于监控页面性能、收集访问数据
def __init__(self, get_response=None):
#sxc 中间件初始化方法:接收 Django 的 get_response 回调函数,用于传递请求
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
#sxc 中间件核心方法:处理请求和响应,计算页面渲染耗时并收集数据,记录请求开始时间(用于计算渲染耗时)
start_time = time.time()
#sxc 传递请求到下一个中间件或视图,获取响应对象
response = self.get_response(request)
#sxc 提取请求中的用户代理字符串(用于解析设备信息)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
#sxc 获取访问者的 IP 地址ipware 工具自动处理代理 IP 场景)
ip, _ = get_client_ip(request)
#sxc 非流式响应(如 HTML 页面)才处理性能数据(流式响应如文件下载无需处理)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
#sxc 计算页面渲染总耗时(当前时间 - 请求开始时间)
cast_time = time.time() - start_time
#sxc 若启用 Elasticsearch将性能数据写入 "performance" 索引
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
#sxc 调用管理器创建性能日志文档,包含 URL、耗时、时间、设备、IP 信息
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
#sxc 替换页面中的 "<!!LOAD_TIMES!!>" 占位符为实际渲染耗时(保留前 5 位字符)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
#sxc 捕获处理过程中的异常,记录错误日志(避免中间件报错影响页面正常响应)
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
#sxc 返回处理后的响应对象
return response

@ -0,0 +1,188 @@
#sxc 该模块是 Django 的数据库迁移文件,用于定义博客系统的数据模型结构及迁移操作
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
#sxc 标记这是初始迁移
initial = True
#sxc 依赖项,依赖于 Django 的用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#sxc 迁移操作列表,定义了所有模型的创建操作
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
#sxc 自增主键 ID
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 网站名称字段,默认空字符串,最大长度 200
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
#sxc 网站描述字段,默认空字符串,最大长度 1000
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
#sxc 网站 SEO 描述字段,用于搜索引擎优化
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
#sxc 网站关键字字段,用于搜索引擎优化
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
#sxc 文章摘要长度设置,默认 300
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
#sxc 侧边栏文章数目设置,默认 10
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
#sxc 侧边栏评论数目设置,默认 5
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
#sxc 文章页面默认显示评论数目,默认 5
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
#sxc 是否显示谷歌广告的布尔值,默认不显示
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
#sxc 谷歌广告代码内容,可为空
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
#sxc 是否打开网站评论功能,默认打开
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
#sxc 网站备案号,可为空
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
#sxc 网站统计代码
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
#sxc 是否显示公安备案号,默认不显示
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
#sxc 公安备案号内容,可为空
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
},
),
#sxc 创建友情链接模型
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 链接名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
#sxc 链接地址URL 类型
('link', models.URLField(verbose_name='链接地址')),
#sxc 排序序号,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
#sxc 是否显示,默认显示
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#sxc 显示类型,可选首页、列表页、文章页面、全站、友情链接页面
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
#sxc 创建时间,默认当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#sxc 最后修改时间,默认当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
},
),
#sxc 创建侧边栏模型
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 侧边栏标题
('name', models.CharField(max_length=100, verbose_name='标题')),
#sxc 侧边栏内容
('content', models.TextField(verbose_name='内容')),
#sxc 排序序号,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
#sxc 是否启用,默认启用
('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'],
},
),
#sxc 创建标签模型
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='修改时间')),
#sxc 标签名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
#sxc URL 友好的标识符,默认 'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
},
),
#sxc 创建分类模型
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='修改时间')),
#sxc 分类名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
#sxc 权重排序,数字越大越靠前,默认 0
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
#sxc 父级分类,自引用外键,可为空
('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'],
},
),
#sxc 创建文章模型
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='修改时间')),
#sxc 文章标题,唯一
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
#sxc 文章正文,使用 markdown 编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
#sxc 发布时间,默认当前时间
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
#sxc 文章状态,可选草稿或发表,默认发表
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
#sxc 评论状态,可选打开或关闭,默认打开
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
#sxc 类型,可选文章或页面,默认文章
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
#sxc 浏览量,正整数,默认 0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
#sxc 排序,数字越大越靠前,默认 0
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
#sxc 是否显示 toc 目录,默认不显示
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
#sxc 外键关联到用户模型,作者
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#sxc 外键关联到分类模型
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
#sxc 多对多关联到标签模型
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
#sxc 先按排序降序,再按发布时间降序
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,28 @@
#sxc 该模块是 Django 的数据库迁移文件,用于对博客系统的网站配置模型进行扩展,添加公共头部和公共尾部字段
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于 blog 应用的初始迁移 (0001_initial)
dependencies = [
('blog', '0001_initial'),
]
#sxc 迁移操作列表,定义了为 BlogSettings 模型添加字段的操作
operations = [
#sxc 为 BlogSettings 模型添加公共尾部字段
migrations.AddField(
model_name='blogsettings',#sxc 为 BlogSettings 模型添加公共尾部字段
name='global_footer',#sxc 新增字段名称
#sxc 字段类型为文本字段可为空默认值为空字符串verbose_name 为 "公共尾部"
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
#sxc 为 BlogSettings 模型添加公共头部字段
migrations.AddField(
model_name='blogsettings',#sxc 要修改的模型名称
name='global_header', #sxc 新增字段名称
#sxc 字段类型为文本字段可为空默认值为空字符串verbose_name 为 "公共头部"
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,21 @@
#sxc 该模块是 Django 的数据库迁移文件,用于在博客系统的网站配置模型中添加评论审核审核设置字段
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于 blog 应用的上一次迁移 (0002_blogsettings_global_footer_and_more)
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
#sxc 迁移操作列表,定义了为 BlogSettings 模型添加评论审核字段的操作
operations = [
#sxc 为 BlogSettingsSettings 模型添加评论是否需要审核的字段
migrations.AddField(
model_name='blogsettings', #sxc 要修改的模型名称
name='comment_need_review',#sxc 新增字段名称
#sxc 字段类型为布尔值,默认值为 False (不需要审核)verbose_name 为 "评论是否需要审核"
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,32 @@
#sxc 该模块是 Django 的数据库迁移文件,用于对博客系统的 BlogSettings 模型中的部分字段进行重命名,统一字段命名风格为下划线命名法
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于 blog 应用的上一次迁移 (0003_blogsettings_comment_need_review)
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
#sxc 迁移操作列表,定义了对 BlogSettings 模型字段的重命名操作
operations = [
#sxc 将 analyticscode 字段重命名为 analytics_code
migrations.RenameField(
model_name='blogsettings',#sxc 要修改的模型名称
old_name='analyticscode',#sxc 原字段名称
new_name='analytics_code',#sxc 新字段名称
),
#sxc 将 beiancode 字段重命名为 beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
#sxc 将 sitename 字段重命名为 site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -0,0 +1,360 @@
#sxc 该模块是 Django 的数据库迁移文件,用于对博客系统的多个模型进行批量修改,包括更新模型选项、字段重命名、字段属性调整以及统一将 verbose_name 从中文改为英文
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于用户模型和 blog 应用的上一次迁移 (0004_...)
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
#sxc 迁移操作列表,包含模型选项修改、字段移除、新增和属性修改等操作
operations = [
#sxc 修改 Article 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
#sxc 修改 Category 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
#sxc 修改 Links 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
#sxc 修改 SideBar 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
#sxc 修改 Tag 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
#sxc 移除 Article 模型中的 created_time 字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
#sxc 移除 Article 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
#sxc 移除 Category 模型中的 created_time 字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
#sxc 移除 Category 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
#sxc 移除 Links 模型中的 created_time 字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
#sxc 移除 SideBar 模型中的 created_time 字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
#sxc 移除 Tag 模型中的 created_time 字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
#sxc 移除 Tag 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
#sxc 为 Article 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Article 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 为 Category 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Category 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 为 Links 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 SideBar 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Tag 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Tag 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 Article 模型的 article_order 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
#sxc 修改 Article 模型的 author 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#sxc 修改 Article 模型的 body 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
#sxc 修改 Article 模型的 category 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
#sxc 修改 Article 模型的 comment_status 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
#sxc 修改 Article 模型的 pub_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
#sxc 修改 Article 模型的 show_toc 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
#sxc 修改 Article 模型的 status 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
#sxc 修改 Article 模型的 tags 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
#sxc 修改 Article 模型的 title 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
#sxc 修改 Article 模型的 type 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
#sxc 修改 Article 模型的 views 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
#sxc 修改 BlogSettings 模型的 article_comment_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
#sxc 修改 BlogSettings 模型的 article_sub_length 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
#sxc 修改 BlogSettings 模型的 google_adsense_codes 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
#sxc 修改 BlogSettings 模型的 open_site_comment 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
#sxc 修改 BlogSettings 模型的 show_google_adsense 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
#sxc 修改 BlogSettings 模型的 sidebar_article_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
#sxc 修改 BlogSettings 模型的 sidebar_comment_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
#sxc 修改 BlogSettings 模型的 site_description 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
#sxc 修改 BlogSettings 模型的 site_keywords 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
#sxc 修改 BlogSettings 模型的 site_name 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
#sxc 修改 BlogSettings 模型的 site_seo_description 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
#sxc 修改 Category 模型的 index 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
#sxc 修改 Category 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
#sxc 修改 Category 模型的 parent_category 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
#sxc 修改 Links 模型的 is_enable 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
#sxc 修改 Links 模型的 last_mod_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 Links 模型的 link 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
#sxc 修改 Links 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
#sxc 修改 Links 模型的 sequence 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
#sxc 修改 Links 模型的 show_type 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
#sxc 修改 SideBar 模型的 content 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
#sxc 修改 SideBar 模型的 is_enable 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
#sxc 修改 SideBar 模型的 last_mod_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 SideBar 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
#sxc 修改 SideBar 模型的 sequence 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
#sxc 修改 Tag 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -0,0 +1,18 @@
#sxc 该模块是 Django 的数据库迁移文件,用于修改博客系统中 BlogSettings 模型的元选项,将其 verbose_name 从中文改为英文
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于 blog 应用的上一次迁移 (0005_...)
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
#sxc 迁移操作列表,仅包含修改 BlogSettings 模型选项的操作
operations = [
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -0,0 +1,407 @@
#sxc 该模块是 Django 博客系统的核心数据模型定义模块,包含文章、分类、标签、友情链接、侧边栏、网站配置 6 大核心模型以及基础抽象模型和链接显示类型枚举同时集成缓存、URL 生成、数据验证等功能,支撑博客的内容管理和系统配置
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField #sxc 导入 Markdown 编辑器字段,用于文章正文
from uuslug import slugify #sxc 导入 slug 生成工具,用于生成 URL 友好的标识
from djangoblog.utils import cache_decorator, cache #sxc 导入缓存工具,优化数据查询性能
from djangoblog.utils import get_current_site #sxc 导入获取当前站点信息的工具
logger = logging.getLogger(__name__) #sxc 初始化日志记录器,用于记录模型操作相关日志
class LinkShowType(models.TextChoices):
#sxc 枚举类:定义友情链接的显示位置选项,支持国际化
I = ('i', _('index'))#sxc 首页显示
L = ('l', _('list'))#sxc 列表页显示
P = ('p', _('post'))#sxc 文章页显示
A = ('a', _('all'))#sxc 全站显示
S = ('s', _('slide'))#sxc 幻灯片区域显示
class BaseModel(models.Model):
#sxc 抽象基础模型:为所有业务模型提供通用字段和方法,避免代码
id = models.AutoField(primary_key=True) #sxc 自增主键重复
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
#sxc 重写保存方法:优化文章浏览量更新逻辑,自动生成 slug 字段
#sxc 判断是否为文章模型的浏览量更新操作(仅更新 views 字段)
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
#sxc 浏览量更新:直接通过 QuerySet 更新,避免触发完整 save 流程
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
#sxc 非浏览量更新:若模型有 slug 字段,自动生成 URL 友好的 slug基于 title 或 name 字段)
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
@abstractmethod
def get_absolute_url(self):
#sxc 抽象方法:要求子类实现获取自身相对 URL 的逻辑,用于路由跳转
pass
class Article(BaseModel):
"""文章"""
#sxc 文章状态选项草稿d、已发布p
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
#sxc 评论状态选项打开o、关闭c
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
#sxc 文章类型选项文章a、独立页面p如关于页、联系页
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
return self.body
def __str__(self):
#sxc 模型实例的字符串表示,返回文章标题
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
#sxc 生成文章的相对 URL用于路由跳转包含发布日期和文章 ID
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
#sxc 重写 save 方法:可扩展文章保存时的自定义逻辑(当前仅调用父类方法)
super().save(*args, **kwargs)
def viewed(self):
#sxc 文章被浏览时调用,增加浏览量并保存(仅更新 views 字段)
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
#sxc 获取文章的有效评论列表,结果缓存 100 分钟,减少数据库查询
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
#sxc 查询已启用的评论,按 ID 降序(最新评论在前)
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
#sxc 生成文章在 Django Admin 后台的编辑 URL用于快速跳转管理
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
#sxc 获取当前文章的下一篇ID 更大、已发布),结果缓存 100 分钟
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
#sxc 获取当前文章的前一篇ID 更小、已发布),结果缓存 100 分钟
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
#sxc 文章分类模型,支持层级结构(父子分类),用于组织文章归属
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)#sxc 分类名称,唯一不可重复
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)#sxc 父分类,自关联,支持层级;父分类删除时子分类也删除
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index')) #sxc 排序权重,值越大越靠前
class Meta:
ordering = ['-index'] #sxc 默认按权重降序排列
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
#sxc 生成分类详情页的 URL基于 slug 参数
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
#sxc 模型实例的字符串表示,返回分类名称
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
#sxc 递归函数:将当前分类加入列表,若有父分类则继续解析
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = []
all_categorys = Category.objects.all() #sxc 预加载所有分类,减少数据库查询
def parse(category):
#sxc 递归函数:将当前分类加入列表,再解析其所有子分类
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
class Tag(BaseModel):
#sxc 文章标签模型,用于文章的多维度标记和快速检索
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
#sxc 模型实例的字符串表示,返回标签名称
return self.name
def get_absolute_url(self):
#sxc 生成标签详情页的 URL基于 slug 参数
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
#sxc 获取关联当前标签的文章数量(去重),结果缓存 10 小时
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
#sxc 友情链接模型,用于管理博客外部链接的展示
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence']#sxc 默认按排序序号升序排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
#sxc 模型实例的字符串表示,返回链接名称
return self.name
class SideBar(models.Model):
#sxc 侧边栏模型,用于展示自定义 HTML 内容,增强页面灵活性
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
return self.site_name
def clean(self):
# 检查是否已存在其他配置对象,确保配置的唯一性
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
# 调用父类的save方法保存对象
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,19 @@
#sxc 该模块是 Django 博客系统基于 Haystack 搜索框架的索引配置模块,用于定义文章模型的搜索索引结构,指定搜索字段和索引范围,为博客提供全文搜索功能的基础数据支持
from haystack import indexes #sxc 导入 Haystack 框架的索引类,用于定义搜索索引
from blog.models import Article #sxc 导入博客文章模型,作为索引的数据源
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#sxc 文章搜索索引类,继承 Haystack 的搜索索引和索引源接口
#sxc 定义主搜索字段 textdocument=True 表示该字段为主要搜索字段use_template=True 表示使用模板定义字段内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
#sxc 实现 Indexable 接口的方法:指定索引关联的模型为 Article
return Article
def index_queryset(self, using=None):
#sxc 实现 SearchIndex 的方法:定义需要建立索引的数据集
#sxc 仅对状态为 “已发布”status='p')的文章建立索引,确保搜索结果为公开内容
return self.get_model().objects.filter(status='p')

@ -0,0 +1,384 @@
#sxc 该模块是 Django 博客系统的自定义模板标签库,提供了格式化显示、内容处理、侧边栏加载、面包屑生成等 16 个实用模板标签 / 过滤器,用于在前端模板中复用核心业务逻辑
#sxc 依赖 Django 模板系统、博客数据模型、缓存工具及第三方头像服务,同时集成插件钩子机制扩展功能
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
#sxc 注册模板标签库,使 Django 模板系统可识别该模块中的标签
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
#sxc 执行头部元信息的插件过滤钩子,返回处理后的安全 HTML 内容
#sxc takes_context=True 表示需要接收模板上下文参数
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
#sxc 简单标签:按项目配置的 TIME_FORMAT 格式化为时间字符串
#sxc 异常捕获避免时间格式错误导致页面崩溃,错误时返回空字符串
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.simple_tag
def datetimeformat(data):
#sxc 简单标签:按项目配置的 DATE_TIME_FORMAT 格式化为日期时间字符串
#sxc 异常捕获处理,错误时返回空字符串
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.filter()
@stringfilter
def custom_markdown(content):
#sxc 过滤器:将 markdown 格式的内容转换为 HTML并标记为安全内容避免 XSS 过滤)
#sxc @stringfilter 表示仅接收字符串类型输入
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
#sxc 简单标签:获取 markdown 内容的目录TOC返回安全 HTML 格式
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@register.filter()
@stringfilter
def comment_markdown(content):
#sxc 过滤器:处理评论内容的 markdown 转换,先转 HTML 再 sanitize 过滤危险标签
#sxc 确保评论内容安全,防止恶意 HTML 注入
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
"""
#sxc 过滤器:按博客配置的 “文章摘要长度” 截取内容,保留 HTML 标签结构
#sxc is_safe=True 表示返回的 HTML 内容安全,无需额外转义
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return truncatechars_html(content, blogsetting.article_sub_length)
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
#sxc 过滤器:移除 HTML 标签后截取前 150 个字符,用于纯文本摘要场景
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
"""
#sxc 包含标签:加载文章面包屑导航,渲染指定模板并传递数据
#sxc 从文章获取分类层级树,反转顺序后拼接网站名称,生成面包屑路径
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
return {
'names': names,#sxc 面包屑层级列表,每个元素为 (名称,链接)
'title': article.title,#sxc 文章标题
'count': len(names) + 1#sxc 面包屑节点数量
}
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
"""
#sxc 包含标签:加载文章的标签列表,渲染标签展示模板
#sxc 为每个标签生成链接、文章数量,并随机分配 Bootstrap 颜色
tags = article.tags.all()
tags_list = []
for tag in tags:
url = tag.get_absolute_url()
count = tag.get_article_count()
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list#sxc 传递标签数据列表到模板
}
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
"""
#sxc 包含标签:加载博客侧边栏内容,优先从缓存获取,缓存未命中则查询数据库
#sxc 缓存键包含 linktype链接显示类型确保不同页面侧边栏内容正确
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user#sxc 补充当前用户信息
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
#sxc 查询发布状态的最新文章,数量由博客配置决定
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
#sxc 查询所有分类
sidebar_categorys = Category.objects.all()
#sxc 查询启用状态的额外侧边栏,按排序号排列
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
#sxc 查询浏览量最高的文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
#sxc 按月份分组的文章发布日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
#sxc 查询启用状态的链接,筛选当前页面类型或全站显示的链接
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
#sxc 查询启用状态的最新评论,数量由博客配置决定
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
#sxc 生成标签云:计算标签字体大小(按文章数量比例),随机打乱顺序
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
#sxc 设置缓存,有效期 3 小时60603 秒)
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
#sxc 包含标签:加载分页信息,根据不同页面类型(首页 / 标签 / 作者 / 分类)生成上下页链接
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
#sxc 标签归档分页逻辑:根据标签 slug 生成链接
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
#sxc 作者归档分页逻辑:根据作者名生成链接
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
#sxc 分类归档分页逻辑:根据分类 slug 生成链接
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
#sxc 包含标签:加载文章详情内容,根据 isindex 判断是否显示摘要(列表页)或完整内容(详情页)
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
#sxc 过滤器:获取用户 Gravatar 头像 URL优先使用 OAuth 用户的自定义头像,其次调用 Gravatar 接口
#sxc 缓存头像 URL减少重复查询
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
#sxc 查询 OAuth 用户表,获取用户自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
#sxc 头像默认图(项目静态文件)
default = static('blog/img/avatar.png')
#sxc 拼接 Gravatar URL包含 MD5 加密邮箱、默认图、尺寸参数
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
#sxc 缓存头像 URL有效期 10 小时
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
#sxc 过滤器:生成 Gravatar 头像的 img 标签,直接返回可渲染的 HTML
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)

@ -0,0 +1,254 @@
#sxc 该模块基于 Django 框架,通过 TestCase 类对博客系统核心功能进行自动化测试,覆盖文章管理、用户交互、支付流程、第三方集成等场景,确保系统各模块功能正常、接口响应符合预期。
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
#sxc 初始化测试环境,创建客户端(模拟用户请求)和请求工厂(构造自定义请求),为后续测试方法提供基础对象。
def setUp(self):
self.client = Client() #sxc 模拟用户浏览器,用于发送 HTTP 请求
self.factory = RequestFactory() #sxc 用于构造自定义请求对象,支持更灵活的请求参数配置
#sxc 测试文章相关核心功能,包括用户创建、分类 / 标签 / 侧边栏数据初始化、文章增删查、搜索、分页、第三方通知等,覆盖文章生命周期关键流程。
def test_validate_article(self):
#sxc 获取当前站点域名,用于后续 URL 相关操作
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
#sxc 测试用户个人主页访问,验证响应状态码为 200正常访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
#sxc 测试管理员后台邮件发送日志页面访问(无实际业务校验,仅验证页面可达性)
response = self.client.get('/admin/servermanager/emailsendlog/')
#sxc 测试管理员后台操作日志页面访问(无实际业务校验,仅验证页面可达性)
response = self.client.get('admin/admin/logentry/')
#sxc 创建测试侧边栏数据,设置排序、名称、内容并启用,用于验证侧边栏功能
s = SideBar()
s.sequence = 1 #sxc 侧边栏排序序号,决定展示顺序
s.name = 'test' #sxc 侧边栏名称
s.content = 'test content' #sxc 侧边栏内容
s.is_enable = True #sxc 启用侧边栏
s.save()
#sxc 创建测试分类数据,设置名称和时间戳,用于文章分类关联
category = Category()
category.name = "category"
category.creation_time = timezone.now() #sxc 分类创建时间
category.last_mod_time = timezone.now() #sxc 分类最后修改时间
category.save()
#sxc 创建测试标签数据,用于文章标签关联
tag = Tag()
tag.name = "nicetag"
tag.save()
#sxc 创建测试文章设置标题、内容、作者、分类、类型a普通文章、状态p已发布
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
#sxc 验证文章初始状态下标签数量为 0未关联标签
self.assertEqual(0, article.tags.count())
#sxc 为文章关联标签并重新保存
article.tags.add(tag)
article.save()
#sxc 验证文章关联标签后数量为 1标签关联成功
self.assertEqual(1, article.tags.count())
#sxc 循环创建 20 篇测试文章,用于后续分页功能测试(模拟多数据场景)
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i) #sxc 动态生成文章标题(带序号)
article.body = "nicetitle" + str(i) #sxc 动态生成文章内容(带序号)
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)
article.save()
#sxc 检查 Elasticsearch 是否启用,若启用则执行索引构建命令,确保搜索功能可用
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") #sxc 执行 Django 自定义命令,构建文章搜索索引
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
#sxc 测试搜索功能,搜索关键词 "nicetitle",验证响应状态码为 200
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
#sxc 测试文章详情页访问,验证响应状态码为 200
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
#sxc 测试搜索不存在的关键词 “django”验证搜索页面仍能正常响应状态码 200
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
#sxc 测试文章标签模板标签load_articletags验证返回结果非空
s = load_articletags(article)
self.assertIsNotNone(s)
#sxc 模拟用户登录(使用测试账号),用于后续需登录权限的操作
self.client.login(username='liangliangyy', password='liangliangyy')
#sxc 测试文章归档页面访问,验证响应状态码为 200
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
#sxc 测试所有文章的分页功能调用check_pagination方法验证分页链接有效性
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
#sxc 测试指定标签tag下文章的分页功能
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
#sxc 测试指定作者liangliangyy下文章的分页功能
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#sxc 测试指定分类category下文章的分页功能
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
#sxc 测试博客搜索表单BlogSearchForm的search方法验证表单功能可执行
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
#sxc 调用百度爬虫通知工具,通知单篇文章的完整 URL
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
#sxc 测试头像相关模板标签gravatar_url和gravatar验证头像 URL 和 HTML 标签生成功能
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
#sxc 创建测试友情链接数据,验证友情链接页面功能
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
#sxc 测试 RSS 订阅页面访问,验证响应状态码为 200
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
#sxc 测试站点地图页面访问,验证响应状态码为 200
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
#sxc 测试管理员删除文章操作,验证页面可达性
self.client.get("/admin/blog/article/1/delete/")
#sxc 测试管理员后台邮件日志和操作日志页面访问,验证后台功能稳定性
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
#sxc 验证分页功能,遍历所有分页页面,确保上一页、下一页链接响应正常
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
#sxc 调用分页模板标签,获取分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
#sxc 验证上一页链接响应状态
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
#sxc 验证下一页链接响应状态
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
#sxc 测试图片上传功能,包括未授权拦截、授权上传、工具函数调用
def test_image(self):
import requests
#sxc 下载测试图片Python logo用于上传测试
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
#sxc 测试未授权图片上传,验证返回 403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
#sxc 生成上传授权签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#sxc 构造授权上传请求,验证上传成功(状态码 200
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
#sxc 测试 Django 自定义管理命令,覆盖索引构建、爬虫通知、数据生成等运维功能
def test_commands(self):
#sxc 创建测试超级用户,用于命令执行权限验证
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
#sxc 创建 QQ 第三方登录配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
#sxc 创建第一个 QQ 第三方用户(关联本地用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png")
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
#sxc 创建第二个 QQ 第三方用户(不关联本地用
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
#sxc 若 Elasticsearch 启用,执行索引构建命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
#sxc 执行百度爬虫通知、测试数据生成、缓存清理、头像同步、搜索词构建等命令
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")

@ -0,0 +1,76 @@
#sxc 该模块是 Django 博客应用的 URL 路由配置模块,定义了博客前台所有页面的访问路径与对应视图的映射关系,包括首页、文章详情页、分类页、标签页等核心页面,同时配置了缓存和分页路由,实现 URL 的规范化管理和页面访问控制
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
urlpatterns = [
#sxc 首页路由:映射到 IndexView 视图,名称为 'index'
path(
r'',
views.IndexView.as_view(),
name='index'),
#sxc 首页分页路由:接收页码参数,名称为 'index_page'
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
#sxc 文章详情页路由:通过年月日和文章 ID 访问,名称为 'detailbyid'
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
#sxc 分类详情页路由:通过分类 slug 访问,名称为 'category_detail'
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
#sxc 分类详情分页路由:包含分类 slug 和页码,名称为 'category_detail_page'
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
#sxc 作者文章页路由:通过作者名访问,名称为 'author_detail'
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
#sxc 作者文章分页路由:包含作者名和页码,名称为 'author_detail_page'
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
#sxc 标签详情页路由:通过标签 slug 访问,名称为 'tag_detail'
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
#sxc 标签详情分页路由:包含标签 slug 和页码,名称为 'tag_detail_page'
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
#sxc 归档页面路由:使用缓存装饰器,缓存 1 小时 (60*60 秒),名称为 'archives'
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
#sxc 友情链接页面路由:映射到 LinkListView 视图,名称为 'links'
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
#sxc 文件上传路由:用于处理文件上传请求,名称为 'upload'
path(
r'upload',
views.fileupload,
name='upload'),
#sxc 缓存清理路由:用于触发缓存清理操作,名称为 'clean'
path(
r'clean',
views.clean_cache_view,
name='clean'),
]

@ -0,0 +1,431 @@
#sxc 该模块基于 Django 框架,实现博客系统的各类视图功能,包括文章列表、详情、分类、标签、搜索、文件上传、错误页处理等,同时集成缓存、插件钩子等机制,保障页面渲染和业务逻辑的稳定性与可扩展性。
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
#sxc 文章列表视图基类,封装分页、缓存、上下文处理等通用逻辑,为子类(首页、分类、标签等列表)提供基础支持
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.get['pages']
@property
def page_number(self):
#sxc 获取当前分页页码,支持从 URL 参数或 kwargs 中提取
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
#sxc 抽象方法,子类需重写以定义查询集的缓存键生成规则,用于缓存数据的标识
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
#sxc 抽象方法,子类需重写以实现具体的查询集数据获取逻辑(如筛选文章条件)
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
#sxc 从缓存中获取查询集数据,若缓存未命中则调用 get_queryset_data 获取并缓存
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
#sxc 重写父类方法,优先从缓存获取查询集数据,减少数据库查询
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
#sxc 博客首页视图,继承自 ArticleListView 基类,专注于展示所有已发布文章,配置首页专属友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
#sxc 实现父类抽象方法,获取首页需展示的文章数据:筛选类型为 “普通文章a” 且状态为 “已发布p” 的文章
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
#sxc 实现父类抽象方法,生成首页文章列表的缓存键,格式为 “index_页码”确保不同分页数据的缓存隔离
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
#sxc 文章详情页视图,继承自 Django 的 DetailView负责单篇文章的完整信息展示包括内容、评论、上下篇导航等
template_name = 'blog/article_detail.html' #sxc 指定详情页渲染模板路径
model = Article #sxc 关联的数据模型为 Article自动从数据库获取文章数据
pk_url_kwarg = 'article_id' #sxc 指定 URL 中用于匹配文章主键的参数名为 “article_id”
context_object_name = "article" #sxc 设定模板中访问文章对象的变量名为 “article”
def get_context_data(self, **kwargs):
#sxc 重写父类方法,构建详情页所需的完整上下文数据,包括评论表单、评论分页、上下篇文章等
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
#sxc 处理评论分页页码:从 URL 参数获取,非数字 / 越界时自动修正为合法值
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
#sxc 获取当前页的评论数据,并计算上下页页码
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
#sxc 构建评论上下页的跳转 URL包含锚点 “#commentlist-container” 以定位到评论区
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
#sxc 向上下文注入评论相关数据:表单、所有评论、当前页评论、评论总数
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
#sxc 向上下文注入当前文章的上下篇文章对象,用于导航
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
#sxc 调用父类方法获取基础上下文,再扩展插件相关逻辑
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
#sxc 触发 “文章详情已获取” 的插件动作钩子,通知所有注册插件执行对应逻辑(如统计阅读量
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
#sxc 触发 “文章内容过滤” 的插件钩子,允许插件修改文章正文(如添加水印、解析特殊语法)
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
#sxc 分类目录文章列表视图,按 “分类” 维度筛选文章,展示指定分类及下属子分类的所有已发布文章
page_type = "分类目录归档"
def get_queryset_data(self):
#sxc 实现父类抽象方法,按分类筛选文章:先获取分类对象,再查询该分类及子分类下的已发布文章
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
#sxc 获取当前分类及所有子分类的名称列表(用于多分类联合筛选)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
#sxc 筛选 “属于目标分类 / 子分类” 且 “状态为已发布p” 的文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
#sxc 实现父类抽象方法,生成分类列表的专属缓存键,确保不同分类、不同分页的缓存隔离
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
#sxc 缓存键格式category_list_分类名_页码如 “category_list_技术_2”
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
#sxc 重写父类方法,向模板上下文注入分类相关标识,用于页面个性化渲染
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
#sxc 作者文章列表视图,按 “作者” 维度筛选文章,展示指定作者发布的所有已发布文章
page_type = '作者文章归档'
def get_queryset_cache_key(self):
#sxc 实现父类抽象方法,生成作者列表的专属缓存键,结合作者别名和页码确保缓存隔离
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
#sxc 实现父类抽象方法,按作者筛选文章:查询指定作者发布的已发布文章
author_name = self.kwargs['author_name']
#sxc 筛选 “作者用户名为目标值”“类型为普通文章a”“状态为已发布p” 的文章
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
#sxc 重写父类方法,向上下文注入作者相关标识,用于页面渲染
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
#sxc 标签文章列表视图,按 “标签” 维度筛选文章,展示关联指定标签的所有已发布文章
page_type = '分类标签归档'
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
#sxc 筛选 “关联目标标签”“类型为普通文章a”“状态为已发布p” 的文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
#sxc 实现父类抽象方法,生成标签列表的专属缓存键,确保不同标签、不同分页的缓存隔离
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
#sxc 重写父类方法,向上下文注入标签相关标识,用于页面个性化渲染
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
#sxc 文章归档页面视图,展示所有已发布文章的归档信息(按时间等维度聚合),不分页
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
#sxc 获取所有状态为 “已发布p” 的文章,用于归档展示
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
#sxc 归档页缓存键固定为 'archives'(因不分页,无需页码参数)
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
#sxc 友情链接列表视图,展示所有启用的友情链接
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
#sxc 筛选出所有 “已启用is_enable=True” 的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
#sxc 基于 Elasticsearch 的搜索视图,处理用户搜索请求并返回带分页和拼写建议的结果
def get_context(self):
#sxc 构建搜索结果页面的上下文数据,包含查询词、分页信息和拼写建议
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
#sxc 若搜索引擎支持拼写纠错,获取并添加拼写建议
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
@csrf_exempt
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
#sxc 处理文件上传(图床功能),支持图片压缩和签名验证,仅允许 POST 请求
if request.method == 'POST':
sign = request.GET.get('sign', None) #sxc 获取 URL 中的签名参数(用于权限验证)
#sxc 验证签名合法性:签名不存在或与系统计算值不符则拒绝访问
if not sign:
return HttpResponseForbidden() #sxc 无签名时返回 403
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() #sxc 签名不匹配时返回 403
response = [] #sxc 存储上传文件的访问 URL 列表
for filename in request.FILES: #sxc 遍历所有上传的文件
timestr = timezone.now().strftime('%Y/%m/%d')#sxc 按当前日期生成存储目录(如 2023/10/01
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] #sxc 支持的图片文件扩展名
fname = u''.join(str(filename)) #sxc 处理文件名(确保 Unicode 兼容)
#sxc 判断文件是否为图片(检查扩展名是否在图片格式列表中)
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
#sxc 确定文件存储根目录:图片存于 image 文件夹,其他文件存于 files 文件夹
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
#sxc 生成唯一文件名UUID + 原文件扩展名),避免重名
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
#sxc 防止路径遍历攻击:验证保存路径是否在基础目录内
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
#sxc 将上传的文件分块写入本地存储
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
#sxc 若为图片文件,压缩图片质量(优化存储和加载速度)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)#sxc 质量压缩至 20%
#sxc 生成文件的访问 URL 并添加到响应列表
url = static(savepath)
response.append(url)
return HttpResponse(response) #sxc 返回所有上传文件的 URL 列表
else:
return HttpResponse("only for post") #sxc 非 POST 请求直接拒绝
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
#sxc 处理 404 页面未找到错误,记录异常并返回自定义错误页面
if exception:
logger.error(exception)
url = request.get_full_path()
#sxc 渲染错误页面,传递错误信息和状态码
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
#sxc 处理 500 服务器内部错误,返回自定义错误页面
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
#sxc 处理 403 权限拒绝错误,记录异常并返回自定义错误页面
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
def clean_cache_view(request):
#sxc 清理系统缓存的视图,调用缓存清理方法并返回成功响应
cache.clear() #sxc 清除所有缓存数据
return HttpResponse('ok')

@ -0,0 +1,47 @@
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 _
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
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))
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))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

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

@ -0,0 +1,13 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm):
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment
fields = ['body']

@ -0,0 +1,38 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField(max_length=300, verbose_name='正文')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comments', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -0,0 +1,60 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
operations = [
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
migrations.RemoveField(
model_name='comment',
name='created_time',
),
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
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'),
),
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
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'),
),
]

@ -0,0 +1,39 @@
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
# Create your models here.
class Comment(models.Model):
body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id']
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def __str__(self):
return self.body

@ -0,0 +1,30 @@
from django import template
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
def parse(c):
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child)
parse(child)
parse(comment)
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
}

@ -0,0 +1,109 @@
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
# Create your tests here.
class CommentsTest(TransactionTestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
self.client.login(username='liangliangyy1', password='liangliangyy1')
category = Category()
category.name = "categoryccc"
category.save()
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
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)
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
from comments.utils import send_comment_email
send_comment_email(comment)

@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -0,0 +1,38 @@
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__)
def send_comment_email(comment):
site = get_current_site().domain
subject = _('Thanks for your comment')
article_url = f"https://{site}{comment.article.get_absolute_url()}"
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email
send_email([tomail], subject, html_content)
try:
if comment.parent_comment:
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)

@ -0,0 +1,63 @@
# Create your views here.
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
comment.article = article
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -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,122 @@
import _thread
import logging
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 comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_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'])
@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):
clearcache = False
if isinstance(instance, LogEntry):
return
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
if not is_update_views:
clearcache = True
if isinstance(instance, Comment):
if instance.is_enable:
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,))
if clearcache:
cache.clear()
@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

@ -0,0 +1,41 @@
import logging
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
self.init_plugin()
self.register_hooks()
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -0,0 +1,7 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -0,0 +1,44 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -0,0 +1,19 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -0,0 +1,345 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER':'root',
'PASSWORD':'sxc112',
'HOST':'localhost',
'PORT':'3306',
'OPTIONS':{
'charset':'utf8mb4'
}
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]

@ -0,0 +1,59 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
def items(self):
return ['blog:index', ]
def location(self, item):
return reverse(item)
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
def items(self):
return Category.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return Tag.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined

@ -0,0 +1,21 @@
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -0,0 +1,32 @@
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,64 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,232 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
try:
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -0,0 +1,16 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()

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

Loading…
Cancel
Save