develop
ZXY 2 months ago
parent 8bf638a18b
commit 0f0df1fdc7

@ -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,49 @@
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@v3
with:
languages: python
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -0,0 +1,153 @@
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@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
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@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
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@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
verbose: true
- name: Upload coverage to Codecov (fallback)
if: failure()
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella-fallback
fail_ci_if_error: false
verbose: true
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
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@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
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 @@
Subproject commit e02cb6723a0ecb5c2e5eb8da42b2589389ed92b5

@ -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,60 @@
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',)
search_fields = ('username', 'nickname', 'email')

@ -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,114 @@
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, Category, Tag, Links, SideBar, BlogSettings
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
date_hierarchy = 'creation_time'
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
if obj:
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
pass

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

@ -0,0 +1,43 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
cache.set(key, value, 60 * 60 * 10)
return value

@ -0,0 +1,213 @@
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
pass
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
class ArticleDocumentManager():
def __init__(self):
self.create_index()
def create_index(self):
ArticleDocument.init()
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
return [
ArticleDocument(
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()
def update_docs(self, docs):
for doc in docs:
doc.save()

@ -0,0 +1,19 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self):
datas = super(BlogSearchForm, self).search()
if not self.is_valid():
return self.no_query_found()
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -0,0 +1,18 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
def handle(self, *args, **options):
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))

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

@ -0,0 +1,40 @@
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):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,50 @@
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain
class Command(BaseCommand):
help = 'notify baidu url'
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,47 @@
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):
help = 'sync user avatar'
def test_picture(self, url):
try:
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass
def handle(self, *args, **options):
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:
if url.startswith(static_url):
if self.test_picture(url):
continue
else:
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -0,0 +1,42 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response

@ -0,0 +1,137 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
},
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,27 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -0,0 +1,300 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

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

@ -0,0 +1,376 @@
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
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):
pass
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
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):
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):
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):
super().save(*args, **kwargs)
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
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):
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):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
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()
def parse(category):
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):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
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):
"""友情链接"""
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']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
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):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,13 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
return Article
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')

@ -0,0 +1,401 @@
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__)
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.simple_tag
def datetimeformat(data):
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.filter()
@stringfilter
def custom_markdown(content):
html_content = CommonMarkdown.get_markdown(content)
# 然后应用插件过滤器优化HTML
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
return mark_safe(optimized_html)
@register.simple_tag(takes_context=True)
def render_article_content(context, article, is_summary=False):
"""
渲染文章内容包含完整的上下文信息供插件使用
Args:
context: 模板上下文
article: 文章对象
is_summary: 是否为摘要模式首页使用
"""
if not article or not hasattr(article, 'body'):
return ''
# 先转换Markdown为HTML
html_content = CommonMarkdown.get_markdown(article.body)
# 如果是摘要模式,先截断内容再应用插件
if is_summary:
# 截断HTML内容到合适的长度约300字符
from django.utils.html import strip_tags
from django.template.defaultfilters import truncatechars
# 先去除HTML标签截断纯文本然后重新转换为HTML
plain_text = strip_tags(html_content)
truncated_text = truncatechars(plain_text, 300)
# 重新转换截断后的文本为HTML简化版避免复杂的插件处理
html_content = CommonMarkdown.get_markdown(truncated_text)
# 然后应用插件过滤器,传递完整的上下文
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 获取request对象
request = context.get('request')
# 应用所有文章内容相关的插件
# 注意:摘要模式下某些插件(如版权声明)可能不适用
optimized_html = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME,
html_content,
article=article,
request=request,
context=context,
is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
)
return mark_safe(optimized_html)
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@register.filter()
@stringfilter
def comment_markdown(content):
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return truncatechars_html(content, blogsetting.article_sub_length)
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
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:
"""
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,
'title': article.title,
'count': len(names) + 1
}
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
"""
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
}
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
"""
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
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
}
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):
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})
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})
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})
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:
"""
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头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
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')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
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头像"""
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,232 @@
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):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
tag = Tag()
tag.name = "nicetag"
tag.save()
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)
article.save()
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
s = load_articletags(article)
self.assertIsNotNone(s)
self.client.login(username='liangliangyy', password='liangliangyy')
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
sign = get_sha256(get_sha256(settings.SECRET_KEY))
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
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()
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()
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
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,62 @@
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
path(
r'upload',
views.fileupload,
name='upload'),
path(
r'clean',
views.clean_cache_view,
name='clean'),
]

@ -0,0 +1,375 @@
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属性用于指定使用哪个模板进行渲染
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):
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
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
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:
'''
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):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
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
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
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'
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
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):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
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
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
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):
# 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):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
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:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
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)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
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):
cache.clear()
return HttpResponse('ok')

@ -0,0 +1,87 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: auto
threshold: 1%
informational: true
patch:
default:
target: auto
threshold: 1%
informational: true
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "reach,diff,flags,tree"
behavior: default
require_changes: no
ignore:
# Django 相关
- "*/migrations/*"
- "manage.py"
- "*/settings.py"
- "*/wsgi.py"
- "*/asgi.py"
# 测试相关
- "*/tests/*"
- "*/test_*.py"
- "*/*test*.py"
# 静态文件和模板
- "*/static/*"
- "*/templates/*"
- "*/collectedstatic/*"
# 国际化文件
- "*/locale/*"
- "**/*.po"
- "**/*.mo"
# 文档和部署
- "*/docs/*"
- "*/deploy/*"
- "README*.md"
- "LICENSE"
- "Dockerfile"
- "docker-compose*.yml"
- "*.yaml"
- "*.yml"
# 开发环境
- "*/venv/*"
- "*/__pycache__/*"
- "*.pyc"
- ".coverage"
- "coverage.xml"
# 日志文件
- "*/logs/*"
- "*.log"
# 特定文件
- "*/whoosh_cn_backend.py" # 搜索后端
- "*/elasticsearch_backend.py" # 搜索后端
- "*/MemcacheStorage.py" # 缓存存储
- "*/robot.py" # 机器人相关
# 配置文件
- "codecov.yml"
- ".coveragerc"
- "requirements*.txt"

@ -0,0 +1,49 @@
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]
raw_id_fields = ('author', 'article')
search_fields = ('body',)
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,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>A powerful, elegant, and modern blog system.</b>
<br>
<b>English</b><a href="/README.md">简体中文</a>
</p>
---
DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
## ✨ Features
- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
## 🛠️ Tech Stack
- **Backend**: Python 3.10, Django 4.0
- **Database**: MySQL, SQLite (configurable)
- **Cache**: Redis
- **Frontend**: HTML5, CSS3, JavaScript
- **Search**: Whoosh, Elasticsearch (configurable)
- **Editor**: Markdown (mdeditor)
## 🚀 Getting Started
### 1. Prerequisites
Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
### 2. Clone & Installation
```bash
# Clone the project to your local machine
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# Install dependencies
pip install -r requirements.txt
```
### 3. Project Configuration
- **Database**:
Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
Create the database in MySQL:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **More Configurations**:
For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
### 4. Database Initialization
```bash
python manage.py makemigrations
python manage.py migrate
# Create a superuser account
python manage.py createsuperuser
```
### 5. Running the Project
```bash
# (Optional) Generate some test data
python manage.py create_testdata
# (Optional) Collect and compress static files
python manage.py collectstatic --noinput
python manage.py compress --force
# Start the development server
python manage.py runserver
```
Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
## Deployment
- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
## 🧩 Plugin System
The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
## 🤝 Contributing
We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
## 📄 License
This project is open-sourced under the [MIT License](LICENSE).
---
## ❤️ Support & Sponsorship
If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="Alipay Sponsorship">
<img src="/docs/imgs/wechat.jpg" width="150" alt="WeChat Sponsorship">
</p>
<p align="center">
<i>(Left) Alipay / (Right) WeChat</i>
</p>
## 🙏 Acknowledgements
A special thanks to **JetBrains** for providing a free open-source license for this project.
<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>
---
> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.

@ -0,0 +1,64 @@
# Introduction to main features settings
## Cache:
Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`.
```python
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog',
'TIMEOUT': 60 * 60 * 10
},
'locmemcache': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
```
## OAuth Login:
QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration.
### Callback address examples:
QQ: http://your-domain-name/oauth/authorize?type=qq
Weibo: http://your-domain-name/oauth/authorize?type=weibo
type is in the type field of `oauthmanager`.
## owntracks:
owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap.
## Email feature:
Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify:
```python
EMAIL_HOST = 'smtp.zoho.com'
EMAIL_PORT = 587
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 = os.environ.get('DJANGO_EMAIL_USER')
```
with your email account information.
## WeChat Official Account
Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account.
## Introduction to website configuration
You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc.
OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default.
## Source code highlighting
If the code block in your article didn't show hightlight, please write the code blocks as following:
![](https://resource.lylinux.net/image/codelang.png)
That is, you should add the corresponding language name before the code block.
## Update
If you get errors as following while executing database migrations:
```python
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
```
This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed.

@ -0,0 +1,58 @@
# 主要功能配置介绍:
## 缓存:
缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量则会自动使用该redis来作为缓存或者你也可以直接修改如下代码来使用。
https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199
## oauth登录:
现在已经支持QQ微博GoogleGitHubFacebook登录需要在其对应的开放平台申请oauth登录权限然后在
**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。
### 回调地址示例:
qqhttp://你的域名/oauth/authorize?type=qq
微博http://你的域名/oauth/authorize?type=weibo
type对应在`oauthmanager`中的type字段。
## owntracks
owntracks是一个位置追踪软件可以定时的将你的坐标提交到你的服务器上现在简单的支持owntracks功能需要安装owntracks的app然后将api地址设置为:
`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。
## 邮件功能:
同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改:
```python
EMAIL_HOST = 'smtp.zoho.com'
EMAIL_PORT = 587
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 = os.environ.get('DJANGO_EMAIL_USER')
```
为你自己的邮箱配置。
## 微信公众号
集成了简单的微信公众号功能在微信后台将token地址设置为:`你的域名/robot` 即可默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。
然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。
## 网站配置介绍
在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。
其中的*静态文件保存地址*是保存oauth用户登录的头像路径填写绝对路径默认是代码目录。
## 代码高亮
如果你发现你文章的代码没有高亮,请这样书写代码块:
![](https://resource.lylinux.net/image/codelang.png)
也就是说,需要在代码块开始位置加入这段代码对应的语言。
## update
如果你发现执行数据库迁移的时候出现如下报错:
```python
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
```
可能是因为你的mysql版本低于5.6需要升级mysql版本>=5.6即可。
django 4.0登录可能会报错CSRF需要配置下`settings.py`中的`CSRF_TRUSTED_ORIGINS`
https://github.com/liangliangyy/DjangoBlog/blob/master/djangoblog/settings.py#L39

@ -0,0 +1,114 @@
# Deploying DjangoBlog with Docker
![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog)
![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date)
![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/djangoblog)
This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
## 1. Prerequisites
Before you begin, please ensure you have the following software installed on your system:
- [Docker Engine](https://docs.docker.com/engine/install/)
- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
### Step 1: Start the Basic Services
From the project's root directory, run the following command:
```bash
# Build and start the containers in detached mode (includes Django app and MySQL)
docker-compose up -d --build
```
`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
```bash
# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
```
- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
### Step 3: First-Time Initialization
After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
```bash
# Get a shell inside the djangoblog application container (named 'web')
docker-compose exec web bash
# Inside the container, run the following commands:
# Create a superuser account (follow the prompts to set username, email, and password)
python manage.py createsuperuser
# (Optional) Create some test data
python manage.py create_testdata
# (Optional, if ES is enabled) Create the search index
python manage.py rebuild_index
# Exit the container
exit
```
## 3. Alternative Method: Using the Standalone Docker Image
If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
```bash
# Pull the latest image from Docker Hub
docker pull liangliangyy/djangoblog:latest
# Run the container and connect it to your external database
docker run -d \
-p 8000:8000 \
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
-e DJANGO_MYSQL_HOST='your-mysql-host' \
-e DJANGO_MYSQL_USER='your-mysql-user' \
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
-e DJANGO_MYSQL_DATABASE='djangoblog' \
--name djangoblog \
liangliangyy/djangoblog:latest
```
- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
## 4. Configuration (Environment Variables)
Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
| Environment Variable | Default/Example Value | Notes |
|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
| `DJANGO_MYSQL_USER` | `root` | Database username. |
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
---
After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.

@ -0,0 +1,114 @@
# 使用 Docker 部署 DjangoBlog
![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog)
![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date)
![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/djangoblog)
本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
## 1. 环境准备
在开始之前,请确保您的系统中已经安装了以下软件:
- [Docker Engine](https://docs.docker.com/engine/install/)
- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
## 2. 推荐方式:使用 `docker-compose` (一键部署)
这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
### 步骤 1: 启动基础服务
在项目根目录下,执行以下命令:
```bash
# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
docker-compose up -d --build
```
`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
```bash
# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
```
- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
### 步骤 3: 首次运行的初始化操作
当容器首次启动后,您需要进入容器来执行一些初始化命令。
```bash
# 进入 djangoblog 应用容器
docker-compose exec web bash
# 在容器内执行以下命令:
# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
python manage.py createsuperuser
# (可选) 创建一些测试数据
python manage.py create_testdata
# (可选,如果启用了 ES) 创建索引
python manage.py rebuild_index
# 退出容器
exit
```
## 3. 备选方式:使用独立的 Docker 镜像
如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
```bash
# 从 Docker Hub 拉取最新镜像
docker pull liangliangyy/djangoblog:latest
# 运行容器,并链接到您的外部数据库
docker run -d \
-p 8000:8000 \
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
-e DJANGO_MYSQL_HOST='your-mysql-host' \
-e DJANGO_MYSQL_USER='your-mysql-user' \
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
-e DJANGO_MYSQL_DATABASE='djangoblog' \
--name djangoblog \
liangliangyy/djangoblog:latest
```
- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`
- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
## 4. 配置说明 (环境变量)
本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
| 环境变量名称 | 默认值/示例 | 备注 |
|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
---
部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。

@ -0,0 +1,28 @@
# 集成Elasticsearch
如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单,
首先需要注意如下几点:
1. 你的`Elasticsearch`支持`ik`中文分词
2. 你的`Elasticsearch`版本>=7.3.0
接下来在`settings.py`做如下改动即可:
- 增加es链接如下所示
```python
ELASTICSEARCH_DSL = {
'default': {
'hosts': '127.0.0.1:9200'
},
}
```
- 修改`HAYSTACK`配置:
```python
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
```
然后终端执行:
```shell script
./manage.py build_index
```
这将会在你的es中创建两个索引分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@ -0,0 +1,141 @@
# Deploying DjangoBlog with Kubernetes
This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
## Architecture Overview
This deployment utilizes a microservices-based, cloud-native architecture:
- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
## 1. Prerequisites
Before you begin, please ensure you have the following:
- A running Kubernetes cluster.
- The `kubectl` command-line tool configured to connect to your cluster.
- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
## 2. Deployment Steps
### Step 1: Create a Namespace
We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
```bash
# Create a namespace named 'djangoblog'
kubectl create namespace djangoblog
```
### Step 2: Configure Persistent Storage
This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
```bash
# Log in to your master node
ssh user@master-node
# Create the required storage directories
sudo mkdir -p /mnt/local-storage-db
sudo mkdir -p /mnt/local-storage-djangoblog
sudo mkdir -p /mnt/resource/
sudo mkdir -p /mnt/local-storage-elasticsearch
# Log out from the node
exit
```
**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
After creating the directories, apply the storage-related configurations:
```bash
# Apply the StorageClass
kubectl apply -f deploy/k8s/storageclass.yaml
# Apply the PersistentVolumes (PVs)
kubectl apply -f deploy/k8s/pv.yaml
# Apply the PersistentVolumeClaims (PVCs)
kubectl apply -f deploy/k8s/pvc.yaml
```
### Step 3: Configure the Application
Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
**It is strongly recommended to change the following fields:**
- `DJANGO_SECRET_KEY`: Change to a random, complex string.
- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
```bash
# Edit the ConfigMap file
vim deploy/k8s/configmap.yaml
# Apply the configuration
kubectl apply -f deploy/k8s/configmap.yaml
```
### Step 4: Deploy the Application Stack
Now, we can deploy all the core services.
```bash
# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
kubectl apply -f deploy/k8s/deployment.yaml
# Deploy the Services (to create internal endpoints for the Deployments)
kubectl apply -f deploy/k8s/service.yaml
```
The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
```bash
kubectl get pods -n djangoblog -w
```
### Step 5: Expose the Application Externally
Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
```bash
# Apply the Ingress rule
kubectl apply -f deploy/k8s/gateway.yaml
```
Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
```bash
kubectl get ingress -n djangoblog
```
### Step 6: First-Time Initialization
Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
```bash
# First, get the name of a djangoblog pod
kubectl get pods -n djangoblog | grep djangoblog
# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
kubectl exec -it [pod-name] -n djangoblog -- bash
# Inside the Pod, run the following commands:
# Create a superuser account (follow the prompts)
python manage.py createsuperuser
# (Optional) Create some test data
python manage.py create_testdata
# (Optional, if ES is enabled) Create the search index
python manage.py rebuild_index
# Exit the Pod
exit
```
Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.

@ -0,0 +1,141 @@
# 使用 Kubernetes 部署 DjangoBlog
本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。
## 架构概览
本次部署采用的是微服务化的云原生架构:
- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。
- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。**
- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。
- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。
- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。
## 1. 环境准备
在开始之前,请确保您已具备以下环境:
- 一个正在运行的 Kubernetes 集群。
- `kubectl` 命令行工具已配置并能够连接到您的集群。
- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。
- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。
## 2. 部署步骤
### 步骤 1: 创建命名空间
我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。
```bash
# 创建一个名为 djangoblog 的命名空间
kubectl create namespace djangoblog
```
### 步骤 2: 配置持久化存储
此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。
```bash
# 登录到您的 master 节点
ssh user@master-node
# 创建所需的存储目录
sudo mkdir -p /mnt/local-storage-db
sudo mkdir -p /mnt/local-storage-djangoblog
sudo mkdir -p /mnt/resource/
sudo mkdir -p /mnt/local-storage-elasticsearch
# 退出节点
exit
```
**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity``local.path` 的配置。
创建目录后,应用存储相关的配置文件:
```bash
# 应用 StorageClass
kubectl apply -f deploy/k8s/storageclass.yaml
# 应用 PersistentVolume (PV)
kubectl apply -f deploy/k8s/pv.yaml
# 应用 PersistentVolumeClaim (PVC)
kubectl apply -f deploy/k8s/pvc.yaml
```
### 步骤 3: 配置应用
在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。
**强烈建议修改以下字段:**
- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。
- `DJANGO_MYSQL_PASSWORD``MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。
```bash
# 编辑 ConfigMap 文件
vim deploy/k8s/configmap.yaml
# 应用配置
kubectl apply -f deploy/k8s/configmap.yaml
```
### 步骤 4: 部署应用服务栈
现在,我们可以部署所有的核心服务了。
```bash
# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
kubectl apply -f deploy/k8s/deployment.yaml
# 部署 Services (为 Deployments 创建内部访问端点)
kubectl apply -f deploy/k8s/service.yaml
```
部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`)
```bash
kubectl get pods -n djangoblog -w
```
### 步骤 5: 暴露应用到外部
最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。
```bash
# 应用 Ingress 规则
kubectl apply -f deploy/k8s/gateway.yaml
```
部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址:
```bash
kubectl get ingress -n djangoblog
```
### 步骤 6: 首次运行的初始化操作
与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。
```bash
# 首先,获取 djangoblog pod 的名称
kubectl get pods -n djangoblog | grep djangoblog
# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称)
kubectl exec -it [pod-name] -n djangoblog -- bash
# 在 Pod 内部执行以下命令:
# 创建超级管理员账户 (请按照提示操作)
python manage.py createsuperuser
# (可选) 创建测试数据
python manage.py create_testdata
# (可选,如果启用了 ES) 创建索引
python manage.py rebuild_index
# 退出 Pod
exit
```
至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署!

File diff suppressed because it is too large Load Diff

@ -0,0 +1,685 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 16:02+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\accounts\admin.py:12
msgid "password"
msgstr "password"
#: .\accounts\admin.py:13
msgid "Enter password again"
msgstr "Enter password again"
#: .\accounts\admin.py:24 .\accounts\forms.py:89
msgid "passwords do not match"
msgstr "passwords do not match"
#: .\accounts\forms.py:36
msgid "email already exists"
msgstr "email already exists"
#: .\accounts\forms.py:46 .\accounts\forms.py:50
msgid "New password"
msgstr "New password"
#: .\accounts\forms.py:60
msgid "Confirm password"
msgstr "Confirm password"
#: .\accounts\forms.py:70 .\accounts\forms.py:116
msgid "Email"
msgstr "Email"
#: .\accounts\forms.py:76 .\accounts\forms.py:80
msgid "Code"
msgstr "Code"
#: .\accounts\forms.py:100 .\accounts\tests.py:194
msgid "email does not exist"
msgstr "email does not exist"
#: .\accounts\models.py:12 .\oauth\models.py:17
msgid "nick name"
msgstr "nick name"
#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
#: .\oauth\models.py:53
msgid "creation time"
msgstr "creation time"
#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
#: .\oauth\models.py:54
msgid "last modify time"
msgstr "last modify time"
#: .\accounts\models.py:15
msgid "create source"
msgstr "create source"
#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
msgid "user"
msgstr "user"
#: .\accounts\tests.py:216 .\accounts\utils.py:39
msgid "Verification code error"
msgstr "Verification code error"
#: .\accounts\utils.py:13
msgid "Verify Email"
msgstr "Verify Email"
#: .\accounts\utils.py:21
#, python-format
msgid ""
"You are resetting the password, the verification code is%(code)s, valid "
"within 5 minutes, please keep it properly"
msgstr ""
"You are resetting the password, the verification code is%(code)s, valid "
"within 5 minutes, please keep it properly"
#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
#: .\oauth\models.py:12
msgid "author"
msgstr "author"
#: .\blog\admin.py:53
msgid "Publish selected articles"
msgstr "Publish selected articles"
#: .\blog\admin.py:54
msgid "Draft selected articles"
msgstr "Draft selected articles"
#: .\blog\admin.py:55
msgid "Close article comments"
msgstr "Close article comments"
#: .\blog\admin.py:56
msgid "Open article comments"
msgstr "Open article comments"
#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
#: .\templates\blog\tags\sidebar.html:40
msgid "category"
msgstr "category"
#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
msgid "index"
msgstr "index"
#: .\blog\models.py:21
msgid "list"
msgstr "list"
#: .\blog\models.py:22
msgid "post"
msgstr "post"
#: .\blog\models.py:23
msgid "all"
msgstr "all"
#: .\blog\models.py:24
msgid "slide"
msgstr "slide"
#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
msgid "modify time"
msgstr "modify time"
#: .\blog\models.py:63
msgid "Draft"
msgstr "Draft"
#: .\blog\models.py:64
msgid "Published"
msgstr "Published"
#: .\blog\models.py:67
msgid "Open"
msgstr "Open"
#: .\blog\models.py:68
msgid "Close"
msgstr "Close"
#: .\blog\models.py:71 .\comments\admin.py:47
msgid "Article"
msgstr "Article"
#: .\blog\models.py:72
msgid "Page"
msgstr "Page"
#: .\blog\models.py:74 .\blog\models.py:280
msgid "title"
msgstr "title"
#: .\blog\models.py:75
msgid "body"
msgstr "body"
#: .\blog\models.py:77
msgid "publish time"
msgstr "publish time"
#: .\blog\models.py:79
msgid "status"
msgstr "status"
#: .\blog\models.py:84
msgid "comment status"
msgstr "comment status"
#: .\blog\models.py:88 .\oauth\models.py:43
msgid "type"
msgstr "type"
#: .\blog\models.py:89
msgid "views"
msgstr "views"
#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
msgid "order"
msgstr "order"
#: .\blog\models.py:98
msgid "show toc"
msgstr "show toc"
#: .\blog\models.py:105 .\blog\models.py:249
msgid "tag"
msgstr "tag"
#: .\blog\models.py:115 .\comments\models.py:21
msgid "article"
msgstr "article"
#: .\blog\models.py:171
msgid "category name"
msgstr "category name"
#: .\blog\models.py:174
msgid "parent category"
msgstr "parent category"
#: .\blog\models.py:234
msgid "tag name"
msgstr "tag name"
#: .\blog\models.py:256
msgid "link name"
msgstr "link name"
#: .\blog\models.py:257 .\blog\models.py:271
msgid "link"
msgstr "link"
#: .\blog\models.py:260
msgid "is show"
msgstr "is show"
#: .\blog\models.py:262
msgid "show type"
msgstr "show type"
#: .\blog\models.py:281
msgid "content"
msgstr "content"
#: .\blog\models.py:283 .\oauth\models.py:52
msgid "is enable"
msgstr "is enable"
#: .\blog\models.py:289
msgid "sidebar"
msgstr "sidebar"
#: .\blog\models.py:299
msgid "site name"
msgstr "site name"
#: .\blog\models.py:305
msgid "site description"
msgstr "site description"
#: .\blog\models.py:311
msgid "site seo description"
msgstr "site seo description"
#: .\blog\models.py:313
msgid "site keywords"
msgstr "site keywords"
#: .\blog\models.py:318
msgid "article sub length"
msgstr "article sub length"
#: .\blog\models.py:319
msgid "sidebar article count"
msgstr "sidebar article count"
#: .\blog\models.py:320
msgid "sidebar comment count"
msgstr "sidebar comment count"
#: .\blog\models.py:321
msgid "article comment count"
msgstr "article comment count"
#: .\blog\models.py:322
msgid "show adsense"
msgstr "show adsense"
#: .\blog\models.py:324
msgid "adsense code"
msgstr "adsense code"
#: .\blog\models.py:325
msgid "open site comment"
msgstr "open site comment"
#: .\blog\models.py:352
msgid "Website configuration"
msgstr "Website configuration"
#: .\blog\models.py:360
msgid "There can only be one configuration"
msgstr "There can only be one configuration"
#: .\blog\views.py:348
msgid ""
"Sorry, the page you requested is not found, please click the home page to "
"see other?"
msgstr ""
"Sorry, the page you requested is not found, please click the home page to "
"see other?"
#: .\blog\views.py:356
msgid "Sorry, the server is busy, please click the home page to see other?"
msgstr "Sorry, the server is busy, please click the home page to see other?"
#: .\blog\views.py:369
msgid "Sorry, you do not have permission to access this page?"
msgstr "Sorry, you do not have permission to access this page?"
#: .\comments\admin.py:15
msgid "Disable comments"
msgstr "Disable comments"
#: .\comments\admin.py:16
msgid "Enable comments"
msgstr "Enable comments"
#: .\comments\admin.py:46
msgid "User"
msgstr "User"
#: .\comments\models.py:25
msgid "parent comment"
msgstr "parent comment"
#: .\comments\models.py:29
msgid "enable"
msgstr "enable"
#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
msgid "comment"
msgstr "comment"
#: .\comments\utils.py:13
msgid "Thanks for your comment"
msgstr "Thanks for your comment"
#: .\comments\utils.py:15
#, python-format
msgid ""
"<p>Thank you very much for your comments on this site</p>\n"
" You can visit <a href=\"%(article_url)s\" rel=\"bookmark"
"\">%(article_title)s</a>\n"
" to review your comments,\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s"
msgstr ""
"<p>Thank you very much for your comments on this site</p>\n"
" You can visit <a href=\"%(article_url)s\" rel=\"bookmark"
"\">%(article_title)s</a>\n"
" to review your comments,\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s"
#: .\comments\utils.py:26
#, python-format
msgid ""
"Your comment on <a href=\"%(article_url)s\" rel=\"bookmark\">"
"%(article_title)s</a><br/> has \n"
" received a reply. <br/> %(comment_body)s\n"
" <br/> \n"
" go check it out!\n"
" <br/>\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s\n"
" "
msgstr ""
"Your comment on <a href=\"%(article_url)s\" rel=\"bookmark\">"
"%(article_title)s</a><br/> has \n"
" received a reply. <br/> %(comment_body)s\n"
" <br/> \n"
" go check it out!\n"
" <br/>\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s\n"
" "
#: .\djangoblog\logentryadmin.py:63
msgid "object"
msgstr "object"
#: .\djangoblog\settings.py:140
msgid "English"
msgstr "English"
#: .\djangoblog\settings.py:141
msgid "Simplified Chinese"
msgstr "Simplified Chinese"
#: .\djangoblog\settings.py:142
msgid "Traditional Chinese"
msgstr "Traditional Chinese"
#: .\oauth\models.py:30
msgid "oauth user"
msgstr "oauth user"
#: .\oauth\models.py:37
msgid "weibo"
msgstr "weibo"
#: .\oauth\models.py:38
msgid "google"
msgstr "google"
#: .\oauth\models.py:48
msgid "callback url"
msgstr "callback url"
#: .\oauth\models.py:59
msgid "already exists"
msgstr "already exists"
#: .\oauth\views.py:154
#, python-format
msgid ""
"\n"
" <p>Congratulations, you have successfully bound your email address. You "
"can use\n"
" %(oauthuser_type)s to directly log in to this website without a "
"password.</p>\n"
" You are welcome to continue to follow this site, the address is\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link to your "
"browser.\n"
" %(site)s\n"
" "
msgstr ""
"\n"
" <p>Congratulations, you have successfully bound your email address. You "
"can use\n"
" %(oauthuser_type)s to directly log in to this website without a "
"password.</p>\n"
" You are welcome to continue to follow this site, the address is\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link to your "
"browser.\n"
" %(site)s\n"
" "
#: .\oauth\views.py:165
msgid "Congratulations on your successful binding!"
msgstr "Congratulations on your successful binding!"
#: .\oauth\views.py:217
#, python-format
msgid ""
"\n"
" <p>Please click the link below to bind your email</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link "
"to your browser.\n"
" <br />\n"
" %(url)s\n"
" "
msgstr ""
"\n"
" <p>Please click the link below to bind your email</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link "
"to your browser.\n"
" <br />\n"
" %(url)s\n"
" "
#: .\oauth\views.py:228 .\oauth\views.py:240
msgid "Bind your email"
msgstr "Bind your email"
#: .\oauth\views.py:242
msgid ""
"Congratulations, the binding is just one step away. Please log in to your "
"email to check the email to complete the binding. Thank you."
msgstr ""
"Congratulations, the binding is just one step away. Please log in to your "
"email to check the email to complete the binding. Thank you."
#: .\oauth\views.py:245
msgid "Binding successful"
msgstr "Binding successful"
#: .\oauth\views.py:247
#, python-format
msgid ""
"Congratulations, you have successfully bound your email address. You can use "
"%(oauthuser_type)s to directly log in to this website without a password. "
"You are welcome to continue to follow this site."
msgstr ""
"Congratulations, you have successfully bound your email address. You can use "
"%(oauthuser_type)s to directly log in to this website without a password. "
"You are welcome to continue to follow this site."
#: .\templates\account\forget_password.html:7
msgid "forget the password"
msgstr "forget the password"
#: .\templates\account\forget_password.html:18
msgid "get verification code"
msgstr "get verification code"
#: .\templates\account\forget_password.html:19
msgid "submit"
msgstr "submit"
#: .\templates\account\login.html:36
msgid "Create Account"
msgstr "Create Account"
#: .\templates\account\login.html:42
#, fuzzy
#| msgid "forget the password"
msgid "Forget Password"
msgstr "forget the password"
#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
msgid "login"
msgstr "login"
#: .\templates\account\result.html:22
msgid "back to the homepage"
msgstr "back to the homepage"
#: .\templates\blog\article_archives.html:7
#: .\templates\blog\article_archives.html:24
msgid "article archive"
msgstr "article archive"
#: .\templates\blog\article_archives.html:32
msgid "year"
msgstr "year"
#: .\templates\blog\article_archives.html:36
msgid "month"
msgstr "month"
#: .\templates\blog\tags\article_info.html:12
msgid "pin to top"
msgstr "pin to top"
#: .\templates\blog\tags\article_info.html:28
msgid "comments"
msgstr "comments"
#: .\templates\blog\tags\article_info.html:58
msgid "toc"
msgstr "toc"
#: .\templates\blog\tags\article_meta_info.html:6
msgid "posted in"
msgstr "posted in"
#: .\templates\blog\tags\article_meta_info.html:14
msgid "and tagged"
msgstr "and tagged"
#: .\templates\blog\tags\article_meta_info.html:25
msgid "by "
msgstr "by"
#: .\templates\blog\tags\article_meta_info.html:29
#, python-format
msgid ""
"\n"
" title=\"View all articles published by "
"%(article.author.username)s\"\n"
" "
msgstr ""
"\n"
" title=\"View all articles published by "
"%(article.author.username)s\"\n"
" "
#: .\templates\blog\tags\article_meta_info.html:44
msgid "on"
msgstr "on"
#: .\templates\blog\tags\article_meta_info.html:54
msgid "edit"
msgstr "edit"
#: .\templates\blog\tags\article_pagination.html:4
msgid "article navigation"
msgstr "article navigation"
#: .\templates\blog\tags\article_pagination.html:9
msgid "earlier articles"
msgstr "earlier articles"
#: .\templates\blog\tags\article_pagination.html:12
msgid "newer articles"
msgstr "newer articles"
#: .\templates\blog\tags\article_tag_list.html:5
msgid "tags"
msgstr "tags"
#: .\templates\blog\tags\sidebar.html:7
msgid "search"
msgstr "search"
#: .\templates\blog\tags\sidebar.html:50
msgid "recent comments"
msgstr "recent comments"
#: .\templates\blog\tags\sidebar.html:57
msgid "published on"
msgstr "published on"
#: .\templates\blog\tags\sidebar.html:65
msgid "recent articles"
msgstr "recent articles"
#: .\templates\blog\tags\sidebar.html:77
msgid "bookmark"
msgstr "bookmark"
#: .\templates\blog\tags\sidebar.html:96
msgid "Tag Cloud"
msgstr "Tag Cloud"
#: .\templates\blog\tags\sidebar.html:107
msgid "Welcome to star or fork the source code of this site"
msgstr "Welcome to star or fork the source code of this site"
#: .\templates\blog\tags\sidebar.html:118
msgid "Function"
msgstr "Function"
#: .\templates\blog\tags\sidebar.html:120
msgid "management site"
msgstr "management site"
#: .\templates\blog\tags\sidebar.html:122
msgid "logout"
msgstr "logout"
#: .\templates\blog\tags\sidebar.html:129
msgid "Track record"
msgstr "Track record"
#: .\templates\blog\tags\sidebar.html:135
msgid "Click me to return to the top"
msgstr "Click me to return to the top"
#: .\templates\oauth\oauth_applications.html:5
#| msgid "login"
msgid "quick login"
msgstr "quick login"
#: .\templates\share_layout\nav.html:26
msgid "Article archive"
msgstr "Article archive"

@ -0,0 +1,667 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 16:02+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\accounts\admin.py:12
msgid "password"
msgstr "密码"
#: .\accounts\admin.py:13
msgid "Enter password again"
msgstr "再次输入密码"
#: .\accounts\admin.py:24 .\accounts\forms.py:89
msgid "passwords do not match"
msgstr "密码不匹配"
#: .\accounts\forms.py:36
msgid "email already exists"
msgstr "邮箱已存在"
#: .\accounts\forms.py:46 .\accounts\forms.py:50
msgid "New password"
msgstr "新密码"
#: .\accounts\forms.py:60
msgid "Confirm password"
msgstr "确认密码"
#: .\accounts\forms.py:70 .\accounts\forms.py:116
msgid "Email"
msgstr "邮箱"
#: .\accounts\forms.py:76 .\accounts\forms.py:80
msgid "Code"
msgstr "验证码"
#: .\accounts\forms.py:100 .\accounts\tests.py:194
msgid "email does not exist"
msgstr "邮箱不存在"
#: .\accounts\models.py:12 .\oauth\models.py:17
msgid "nick name"
msgstr "昵称"
#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
#: .\oauth\models.py:53
msgid "creation time"
msgstr "创建时间"
#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
#: .\oauth\models.py:54
msgid "last modify time"
msgstr "最后修改时间"
#: .\accounts\models.py:15
msgid "create source"
msgstr "来源"
#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
msgid "user"
msgstr "用户"
#: .\accounts\tests.py:216 .\accounts\utils.py:39
msgid "Verification code error"
msgstr "验证码错误"
#: .\accounts\utils.py:13
msgid "Verify Email"
msgstr "验证邮箱"
#: .\accounts\utils.py:21
#, python-format
msgid ""
"You are resetting the password, the verification code is%(code)s, valid "
"within 5 minutes, please keep it properly"
msgstr "您正在重置密码,验证码为:%(code)s5分钟内有效 请妥善保管."
#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
#: .\oauth\models.py:12
msgid "author"
msgstr "作者"
#: .\blog\admin.py:53
msgid "Publish selected articles"
msgstr "发布选中的文章"
#: .\blog\admin.py:54
msgid "Draft selected articles"
msgstr "选中文章设为草稿"
#: .\blog\admin.py:55
msgid "Close article comments"
msgstr "关闭文章评论"
#: .\blog\admin.py:56
msgid "Open article comments"
msgstr "打开文章评论"
#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
#: .\templates\blog\tags\sidebar.html:40
msgid "category"
msgstr "分类目录"
#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
msgid "index"
msgstr "首页"
#: .\blog\models.py:21
msgid "list"
msgstr "列表"
#: .\blog\models.py:22
msgid "post"
msgstr "文章"
#: .\blog\models.py:23
msgid "all"
msgstr "所有"
#: .\blog\models.py:24
msgid "slide"
msgstr "侧边栏"
#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
msgid "modify time"
msgstr "修改时间"
#: .\blog\models.py:63
msgid "Draft"
msgstr "草稿"
#: .\blog\models.py:64
msgid "Published"
msgstr "发布"
#: .\blog\models.py:67
msgid "Open"
msgstr "打开"
#: .\blog\models.py:68
msgid "Close"
msgstr "关闭"
#: .\blog\models.py:71 .\comments\admin.py:47
msgid "Article"
msgstr "文章"
#: .\blog\models.py:72
msgid "Page"
msgstr "页面"
#: .\blog\models.py:74 .\blog\models.py:280
msgid "title"
msgstr "标题"
#: .\blog\models.py:75
msgid "body"
msgstr "内容"
#: .\blog\models.py:77
msgid "publish time"
msgstr "发布时间"
#: .\blog\models.py:79
msgid "status"
msgstr "状态"
#: .\blog\models.py:84
msgid "comment status"
msgstr "评论状态"
#: .\blog\models.py:88 .\oauth\models.py:43
msgid "type"
msgstr "类型"
#: .\blog\models.py:89
msgid "views"
msgstr "阅读量"
#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
msgid "order"
msgstr "排序"
#: .\blog\models.py:98
msgid "show toc"
msgstr "显示目录"
#: .\blog\models.py:105 .\blog\models.py:249
msgid "tag"
msgstr "标签"
#: .\blog\models.py:115 .\comments\models.py:21
msgid "article"
msgstr "文章"
#: .\blog\models.py:171
msgid "category name"
msgstr "分类名"
#: .\blog\models.py:174
msgid "parent category"
msgstr "上级分类"
#: .\blog\models.py:234
msgid "tag name"
msgstr "标签名"
#: .\blog\models.py:256
msgid "link name"
msgstr "链接名"
#: .\blog\models.py:257 .\blog\models.py:271
msgid "link"
msgstr "链接"
#: .\blog\models.py:260
msgid "is show"
msgstr "是否显示"
#: .\blog\models.py:262
msgid "show type"
msgstr "显示类型"
#: .\blog\models.py:281
msgid "content"
msgstr "内容"
#: .\blog\models.py:283 .\oauth\models.py:52
msgid "is enable"
msgstr "是否启用"
#: .\blog\models.py:289
msgid "sidebar"
msgstr "侧边栏"
#: .\blog\models.py:299
msgid "site name"
msgstr "站点名称"
#: .\blog\models.py:305
msgid "site description"
msgstr "站点描述"
#: .\blog\models.py:311
msgid "site seo description"
msgstr "站点SEO描述"
#: .\blog\models.py:313
msgid "site keywords"
msgstr "关键字"
#: .\blog\models.py:318
msgid "article sub length"
msgstr "文章摘要长度"
#: .\blog\models.py:319
msgid "sidebar article count"
msgstr "侧边栏文章数目"
#: .\blog\models.py:320
msgid "sidebar comment count"
msgstr "侧边栏评论数目"
#: .\blog\models.py:321
msgid "article comment count"
msgstr "文章页面默认显示评论数目"
#: .\blog\models.py:322
msgid "show adsense"
msgstr "是否显示广告"
#: .\blog\models.py:324
msgid "adsense code"
msgstr "广告内容"
#: .\blog\models.py:325
msgid "open site comment"
msgstr "公共头部"
#: .\blog\models.py:352
msgid "Website configuration"
msgstr "网站配置"
#: .\blog\models.py:360
msgid "There can only be one configuration"
msgstr "只能有一个配置"
#: .\blog\views.py:348
msgid ""
"Sorry, the page you requested is not found, please click the home page to "
"see other?"
msgstr "抱歉,你所访问的页面找不到,请点击首页看看别的?"
#: .\blog\views.py:356
msgid "Sorry, the server is busy, please click the home page to see other?"
msgstr "抱歉,服务出错了,请点击首页看看别的?"
#: .\blog\views.py:369
msgid "Sorry, you do not have permission to access this page?"
msgstr "抱歉,你没用权限访问此页面。"
#: .\comments\admin.py:15
msgid "Disable comments"
msgstr "禁用评论"
#: .\comments\admin.py:16
msgid "Enable comments"
msgstr "启用评论"
#: .\comments\admin.py:46
msgid "User"
msgstr "用户"
#: .\comments\models.py:25
msgid "parent comment"
msgstr "上级评论"
#: .\comments\models.py:29
msgid "enable"
msgstr "启用"
#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
msgid "comment"
msgstr "评论"
#: .\comments\utils.py:13
msgid "Thanks for your comment"
msgstr "感谢你的评论"
#: .\comments\utils.py:15
#, python-format
msgid ""
"<p>Thank you very much for your comments on this site</p>\n"
" You can visit <a href=\"%(article_url)s\" rel=\"bookmark"
"\">%(article_title)s</a>\n"
" to review your comments,\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s"
msgstr ""
"<p>非常感谢您对此网站的评论</p>\n"
" 您可以访问<a href=\"%(article_url)s\" rel=\"书签\">%(article_title)s</a>\n"
"查看您的评论,\n"
"再次感谢您!\n"
" <br />\n"
" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n"
"%(article_url)s"
#: .\comments\utils.py:26
#, python-format
msgid ""
"Your comment on <a href=\"%(article_url)s\" rel=\"bookmark\">"
"%(article_title)s</a><br/> has \n"
" received a reply. <br/> %(comment_body)s\n"
" <br/> \n"
" go check it out!\n"
" <br/>\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s\n"
" "
msgstr ""
"您对 <a href=\"%(article_url)s\" rel=\"bookmark\">%(article_title)s</a><br/> "
"的评论有\n"
" 收到回复。<br/> %(comment_body)s\n"
"<br/>\n"
"快去看看吧!\n"
"<br/>\n"
" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n"
" %(article_url)s\n"
" "
#: .\djangoblog\logentryadmin.py:63
msgid "object"
msgstr "对象"
#: .\djangoblog\settings.py:140
msgid "English"
msgstr "英文"
#: .\djangoblog\settings.py:141
msgid "Simplified Chinese"
msgstr "简体中文"
#: .\djangoblog\settings.py:142
msgid "Traditional Chinese"
msgstr "繁体中文"
#: .\oauth\models.py:30
msgid "oauth user"
msgstr "第三方用户"
#: .\oauth\models.py:37
msgid "weibo"
msgstr "微博"
#: .\oauth\models.py:38
msgid "google"
msgstr "谷歌"
#: .\oauth\models.py:48
msgid "callback url"
msgstr "回调地址"
#: .\oauth\models.py:59
msgid "already exists"
msgstr "已经存在"
#: .\oauth\views.py:154
#, python-format
msgid ""
"\n"
" <p>Congratulations, you have successfully bound your email address. You "
"can use\n"
" %(oauthuser_type)s to directly log in to this website without a "
"password.</p>\n"
" You are welcome to continue to follow this site, the address is\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link to your "
"browser.\n"
" %(site)s\n"
" "
msgstr ""
"\n"
" <p>恭喜你已经绑定成功 你可以使用\n"
" %(oauthuser_type)s 来免密登录本站 </p>\n"
" 欢迎继续关注本站, 地址是\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" 再次感谢你\n"
" <br />\n"
" 如果上面链接无法打开,请复制此链接到你的浏览器 \n"
" %(site)s\n"
" "
#: .\oauth\views.py:165
msgid "Congratulations on your successful binding!"
msgstr "恭喜你绑定成功"
#: .\oauth\views.py:217
#, python-format
msgid ""
"\n"
" <p>Please click the link below to bind your email</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link "
"to your browser.\n"
" <br />\n"
" %(url)s\n"
" "
msgstr ""
"\n"
" <p>请点击下面的链接绑定您的邮箱</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
"再次感谢您!\n"
" <br />\n"
"如果上面的链接打不开,请复制此链接到您的浏览器。\n"
"%(url)s\n"
" "
#: .\oauth\views.py:228 .\oauth\views.py:240
msgid "Bind your email"
msgstr "绑定邮箱"
#: .\oauth\views.py:242
msgid ""
"Congratulations, the binding is just one step away. Please log in to your "
"email to check the email to complete the binding. Thank you."
msgstr "恭喜您,还差一步就绑定成功了,请登录您的邮箱查看邮件完成绑定,谢谢。"
#: .\oauth\views.py:245
msgid "Binding successful"
msgstr "绑定成功"
#: .\oauth\views.py:247
#, python-format
msgid ""
"Congratulations, you have successfully bound your email address. You can use "
"%(oauthuser_type)s to directly log in to this website without a password. "
"You are welcome to continue to follow this site."
msgstr ""
"恭喜您绑定成功,您以后可以使用%(oauthuser_type)s来直接免密码登录本站啦感谢"
"您对本站对关注。"
#: .\templates\account\forget_password.html:7
msgid "forget the password"
msgstr "忘记密码"
#: .\templates\account\forget_password.html:18
msgid "get verification code"
msgstr "获取验证码"
#: .\templates\account\forget_password.html:19
msgid "submit"
msgstr "提交"
#: .\templates\account\login.html:36
msgid "Create Account"
msgstr "创建账号"
#: .\templates\account\login.html:42
#| msgid "forget the password"
msgid "Forget Password"
msgstr "忘记密码"
#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
msgid "login"
msgstr "登录"
#: .\templates\account\result.html:22
msgid "back to the homepage"
msgstr "返回首页吧"
#: .\templates\blog\article_archives.html:7
#: .\templates\blog\article_archives.html:24
msgid "article archive"
msgstr "文章归档"
#: .\templates\blog\article_archives.html:32
msgid "year"
msgstr "年"
#: .\templates\blog\article_archives.html:36
msgid "month"
msgstr "月"
#: .\templates\blog\tags\article_info.html:12
msgid "pin to top"
msgstr "置顶"
#: .\templates\blog\tags\article_info.html:28
msgid "comments"
msgstr "评论"
#: .\templates\blog\tags\article_info.html:58
msgid "toc"
msgstr "目录"
#: .\templates\blog\tags\article_meta_info.html:6
msgid "posted in"
msgstr "发布于"
#: .\templates\blog\tags\article_meta_info.html:14
msgid "and tagged"
msgstr "并标记为"
#: .\templates\blog\tags\article_meta_info.html:25
msgid "by "
msgstr "由"
#: .\templates\blog\tags\article_meta_info.html:29
#, python-format
msgid ""
"\n"
" title=\"View all articles published by "
"%(article.author.username)s\"\n"
" "
msgstr ""
"\n"
" title=\"查看所有由 %(article.author.username)s\"发布的文章\n"
" "
#: .\templates\blog\tags\article_meta_info.html:44
msgid "on"
msgstr "在"
#: .\templates\blog\tags\article_meta_info.html:54
msgid "edit"
msgstr "编辑"
#: .\templates\blog\tags\article_pagination.html:4
msgid "article navigation"
msgstr "文章导航"
#: .\templates\blog\tags\article_pagination.html:9
msgid "earlier articles"
msgstr "早期文章"
#: .\templates\blog\tags\article_pagination.html:12
msgid "newer articles"
msgstr "较新文章"
#: .\templates\blog\tags\article_tag_list.html:5
msgid "tags"
msgstr "标签"
#: .\templates\blog\tags\sidebar.html:7
msgid "search"
msgstr "搜索"
#: .\templates\blog\tags\sidebar.html:50
msgid "recent comments"
msgstr "近期评论"
#: .\templates\blog\tags\sidebar.html:57
msgid "published on"
msgstr "发表于"
#: .\templates\blog\tags\sidebar.html:65
msgid "recent articles"
msgstr "近期文章"
#: .\templates\blog\tags\sidebar.html:77
msgid "bookmark"
msgstr "书签"
#: .\templates\blog\tags\sidebar.html:96
msgid "Tag Cloud"
msgstr "标签云"
#: .\templates\blog\tags\sidebar.html:107
msgid "Welcome to star or fork the source code of this site"
msgstr "欢迎您STAR或者FORK本站源代码"
#: .\templates\blog\tags\sidebar.html:118
msgid "Function"
msgstr "功能"
#: .\templates\blog\tags\sidebar.html:120
msgid "management site"
msgstr "管理站点"
#: .\templates\blog\tags\sidebar.html:122
msgid "logout"
msgstr "登出"
#: .\templates\blog\tags\sidebar.html:129
msgid "Track record"
msgstr "运动轨迹记录"
#: .\templates\blog\tags\sidebar.html:135
msgid "Click me to return to the top"
msgstr "点我返回顶部"
#: .\templates\oauth\oauth_applications.html:5
#| msgid "login"
msgid "quick login"
msgstr "快捷登录"
#: .\templates\share_layout\nav.html:26
msgid "Article archive"
msgstr "文章归档"

@ -0,0 +1,668 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 16:02+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\accounts\admin.py:12
msgid "password"
msgstr "密碼"
#: .\accounts\admin.py:13
msgid "Enter password again"
msgstr "再次輸入密碼"
#: .\accounts\admin.py:24 .\accounts\forms.py:89
msgid "passwords do not match"
msgstr "密碼不匹配"
#: .\accounts\forms.py:36
msgid "email already exists"
msgstr "郵箱已存在"
#: .\accounts\forms.py:46 .\accounts\forms.py:50
msgid "New password"
msgstr "新密碼"
#: .\accounts\forms.py:60
msgid "Confirm password"
msgstr "確認密碼"
#: .\accounts\forms.py:70 .\accounts\forms.py:116
msgid "Email"
msgstr "郵箱"
#: .\accounts\forms.py:76 .\accounts\forms.py:80
msgid "Code"
msgstr "驗證碼"
#: .\accounts\forms.py:100 .\accounts\tests.py:194
msgid "email does not exist"
msgstr "郵箱不存在"
#: .\accounts\models.py:12 .\oauth\models.py:17
msgid "nick name"
msgstr "昵稱"
#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
#: .\oauth\models.py:53
msgid "creation time"
msgstr "創建時間"
#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
#: .\oauth\models.py:54
msgid "last modify time"
msgstr "最後修改時間"
#: .\accounts\models.py:15
msgid "create source"
msgstr "來源"
#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
msgid "user"
msgstr "用戶"
#: .\accounts\tests.py:216 .\accounts\utils.py:39
msgid "Verification code error"
msgstr "驗證碼錯誤"
#: .\accounts\utils.py:13
msgid "Verify Email"
msgstr "驗證郵箱"
#: .\accounts\utils.py:21
#, python-format
msgid ""
"You are resetting the password, the verification code is%(code)s, valid "
"within 5 minutes, please keep it properly"
msgstr "您正在重置密碼,驗證碼為:%(code)s5分鐘內有效 請妥善保管."
#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
#: .\oauth\models.py:12
msgid "author"
msgstr "作者"
#: .\blog\admin.py:53
msgid "Publish selected articles"
msgstr "發布選中的文章"
#: .\blog\admin.py:54
msgid "Draft selected articles"
msgstr "選中文章設為草稿"
#: .\blog\admin.py:55
msgid "Close article comments"
msgstr "關閉文章評論"
#: .\blog\admin.py:56
msgid "Open article comments"
msgstr "打開文章評論"
#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
#: .\templates\blog\tags\sidebar.html:40
msgid "category"
msgstr "分類目錄"
#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
msgid "index"
msgstr "首頁"
#: .\blog\models.py:21
msgid "list"
msgstr "列表"
#: .\blog\models.py:22
msgid "post"
msgstr "文章"
#: .\blog\models.py:23
msgid "all"
msgstr "所有"
#: .\blog\models.py:24
msgid "slide"
msgstr "側邊欄"
#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
msgid "modify time"
msgstr "修改時間"
#: .\blog\models.py:63
msgid "Draft"
msgstr "草稿"
#: .\blog\models.py:64
msgid "Published"
msgstr "發布"
#: .\blog\models.py:67
msgid "Open"
msgstr "打開"
#: .\blog\models.py:68
msgid "Close"
msgstr "關閉"
#: .\blog\models.py:71 .\comments\admin.py:47
msgid "Article"
msgstr "文章"
#: .\blog\models.py:72
msgid "Page"
msgstr "頁面"
#: .\blog\models.py:74 .\blog\models.py:280
msgid "title"
msgstr "標題"
#: .\blog\models.py:75
msgid "body"
msgstr "內容"
#: .\blog\models.py:77
msgid "publish time"
msgstr "發布時間"
#: .\blog\models.py:79
msgid "status"
msgstr "狀態"
#: .\blog\models.py:84
msgid "comment status"
msgstr "評論狀態"
#: .\blog\models.py:88 .\oauth\models.py:43
msgid "type"
msgstr "類型"
#: .\blog\models.py:89
msgid "views"
msgstr "閱讀量"
#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
msgid "order"
msgstr "排序"
#: .\blog\models.py:98
msgid "show toc"
msgstr "顯示目錄"
#: .\blog\models.py:105 .\blog\models.py:249
msgid "tag"
msgstr "標簽"
#: .\blog\models.py:115 .\comments\models.py:21
msgid "article"
msgstr "文章"
#: .\blog\models.py:171
msgid "category name"
msgstr "分類名"
#: .\blog\models.py:174
msgid "parent category"
msgstr "上級分類"
#: .\blog\models.py:234
msgid "tag name"
msgstr "標簽名"
#: .\blog\models.py:256
msgid "link name"
msgstr "鏈接名"
#: .\blog\models.py:257 .\blog\models.py:271
msgid "link"
msgstr "鏈接"
#: .\blog\models.py:260
msgid "is show"
msgstr "是否顯示"
#: .\blog\models.py:262
msgid "show type"
msgstr "顯示類型"
#: .\blog\models.py:281
msgid "content"
msgstr "內容"
#: .\blog\models.py:283 .\oauth\models.py:52
msgid "is enable"
msgstr "是否啟用"
#: .\blog\models.py:289
msgid "sidebar"
msgstr "側邊欄"
#: .\blog\models.py:299
msgid "site name"
msgstr "站點名稱"
#: .\blog\models.py:305
msgid "site description"
msgstr "站點描述"
#: .\blog\models.py:311
msgid "site seo description"
msgstr "站點SEO描述"
#: .\blog\models.py:313
msgid "site keywords"
msgstr "關鍵字"
#: .\blog\models.py:318
msgid "article sub length"
msgstr "文章摘要長度"
#: .\blog\models.py:319
msgid "sidebar article count"
msgstr "側邊欄文章數目"
#: .\blog\models.py:320
msgid "sidebar comment count"
msgstr "側邊欄評論數目"
#: .\blog\models.py:321
msgid "article comment count"
msgstr "文章頁面默認顯示評論數目"
#: .\blog\models.py:322
msgid "show adsense"
msgstr "是否顯示廣告"
#: .\blog\models.py:324
msgid "adsense code"
msgstr "廣告內容"
#: .\blog\models.py:325
msgid "open site comment"
msgstr "公共頭部"
#: .\blog\models.py:352
msgid "Website configuration"
msgstr "網站配置"
#: .\blog\models.py:360
msgid "There can only be one configuration"
msgstr "只能有一個配置"
#: .\blog\views.py:348
msgid ""
"Sorry, the page you requested is not found, please click the home page to "
"see other?"
msgstr "抱歉,你所訪問的頁面找不到,請點擊首頁看看別的?"
#: .\blog\views.py:356
msgid "Sorry, the server is busy, please click the home page to see other?"
msgstr "抱歉,服務出錯了,請點擊首頁看看別的?"
#: .\blog\views.py:369
msgid "Sorry, you do not have permission to access this page?"
msgstr "抱歉,你沒用權限訪問此頁面。"
#: .\comments\admin.py:15
msgid "Disable comments"
msgstr "禁用評論"
#: .\comments\admin.py:16
msgid "Enable comments"
msgstr "啟用評論"
#: .\comments\admin.py:46
msgid "User"
msgstr "用戶"
#: .\comments\models.py:25
msgid "parent comment"
msgstr "上級評論"
#: .\comments\models.py:29
msgid "enable"
msgstr "啟用"
#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
msgid "comment"
msgstr "評論"
#: .\comments\utils.py:13
msgid "Thanks for your comment"
msgstr "感謝你的評論"
#: .\comments\utils.py:15
#, python-format
msgid ""
"<p>Thank you very much for your comments on this site</p>\n"
" You can visit <a href=\"%(article_url)s\" rel=\"bookmark"
"\">%(article_title)s</a>\n"
" to review your comments,\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s"
msgstr ""
"<p>非常感謝您對此網站的評論</p>\n"
" 您可以訪問<a href=\"%(article_url)s\" rel=\"書簽\">%(article_title)s</a>\n"
"查看您的評論,\n"
"再次感謝您!\n"
" <br />\n"
" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n"
"%(article_url)s"
#: .\comments\utils.py:26
#, python-format
msgid ""
"Your comment on <a href=\"%(article_url)s\" rel=\"bookmark\">"
"%(article_title)s</a><br/> has \n"
" received a reply. <br/> %(comment_body)s\n"
" <br/> \n"
" go check it out!\n"
" <br/>\n"
" If the link above cannot be opened, please copy this "
"link to your browser.\n"
" %(article_url)s\n"
" "
msgstr ""
"您對 <a href=\"%(article_url)s\" rel=\"bookmark\">%(article_title)s</a><br/> "
"的評論有\n"
" 收到回復。<br/> %(comment_body)s\n"
"<br/>\n"
"快去看看吧!\n"
"<br/>\n"
" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n"
" %(article_url)s\n"
" "
#: .\djangoblog\logentryadmin.py:63
msgid "object"
msgstr "對象"
#: .\djangoblog\settings.py:140
msgid "English"
msgstr "英文"
#: .\djangoblog\settings.py:141
msgid "Simplified Chinese"
msgstr "簡體中文"
#: .\djangoblog\settings.py:142
msgid "Traditional Chinese"
msgstr "繁體中文"
#: .\oauth\models.py:30
msgid "oauth user"
msgstr "第三方用戶"
#: .\oauth\models.py:37
msgid "weibo"
msgstr "微博"
#: .\oauth\models.py:38
msgid "google"
msgstr "谷歌"
#: .\oauth\models.py:48
msgid "callback url"
msgstr "回調地址"
#: .\oauth\models.py:59
msgid "already exists"
msgstr "已經存在"
#: .\oauth\views.py:154
#, python-format
msgid ""
"\n"
" <p>Congratulations, you have successfully bound your email address. You "
"can use\n"
" %(oauthuser_type)s to directly log in to this website without a "
"password.</p>\n"
" You are welcome to continue to follow this site, the address is\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link to your "
"browser.\n"
" %(site)s\n"
" "
msgstr ""
"\n"
" <p>恭喜你已經綁定成功 你可以使用\n"
" %(oauthuser_type)s 來免密登錄本站 </p>\n"
" 歡迎繼續關註本站, 地址是\n"
" <a href=\"%(site)s\" rel=\"bookmark\">%(site)s</a>\n"
" 再次感謝你\n"
" <br />\n"
" 如果上面鏈接無法打開,請復製此鏈接到你的瀏覽器 \n"
" %(site)s\n"
" "
#: .\oauth\views.py:165
msgid "Congratulations on your successful binding!"
msgstr "恭喜你綁定成功"
#: .\oauth\views.py:217
#, python-format
msgid ""
"\n"
" <p>Please click the link below to bind your email</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
" Thank you again!\n"
" <br />\n"
" If the link above cannot be opened, please copy this link "
"to your browser.\n"
" <br />\n"
" %(url)s\n"
" "
msgstr ""
"\n"
" <p>請點擊下面的鏈接綁定您的郵箱</p>\n"
"\n"
" <a href=\"%(url)s\" rel=\"bookmark\">%(url)s</a>\n"
"\n"
"再次感謝您!\n"
" <br />\n"
"如果上面的鏈接打不開,請復製此鏈接到您的瀏覽器。\n"
"%(url)s\n"
" "
#: .\oauth\views.py:228 .\oauth\views.py:240
msgid "Bind your email"
msgstr "綁定郵箱"
#: .\oauth\views.py:242
msgid ""
"Congratulations, the binding is just one step away. Please log in to your "
"email to check the email to complete the binding. Thank you."
msgstr "恭喜您,還差一步就綁定成功了,請登錄您的郵箱查看郵件完成綁定,謝謝。"
#: .\oauth\views.py:245
msgid "Binding successful"
msgstr "綁定成功"
#: .\oauth\views.py:247
#, python-format
msgid ""
"Congratulations, you have successfully bound your email address. You can use "
"%(oauthuser_type)s to directly log in to this website without a password. "
"You are welcome to continue to follow this site."
msgstr ""
"恭喜您綁定成功,您以後可以使用%(oauthuser_type)s來直接免密碼登錄本站啦感謝"
"您對本站對關註。"
#: .\templates\account\forget_password.html:7
msgid "forget the password"
msgstr "忘記密碼"
#: .\templates\account\forget_password.html:18
msgid "get verification code"
msgstr "獲取驗證碼"
#: .\templates\account\forget_password.html:19
msgid "submit"
msgstr "提交"
#: .\templates\account\login.html:36
msgid "Create Account"
msgstr "創建賬號"
#: .\templates\account\login.html:42
#, fuzzy
#| msgid "forget the password"
msgid "Forget Password"
msgstr "忘記密碼"
#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
msgid "login"
msgstr "登錄"
#: .\templates\account\result.html:22
msgid "back to the homepage"
msgstr "返回首頁吧"
#: .\templates\blog\article_archives.html:7
#: .\templates\blog\article_archives.html:24
msgid "article archive"
msgstr "文章歸檔"
#: .\templates\blog\article_archives.html:32
msgid "year"
msgstr "年"
#: .\templates\blog\article_archives.html:36
msgid "month"
msgstr "月"
#: .\templates\blog\tags\article_info.html:12
msgid "pin to top"
msgstr "置頂"
#: .\templates\blog\tags\article_info.html:28
msgid "comments"
msgstr "評論"
#: .\templates\blog\tags\article_info.html:58
msgid "toc"
msgstr "目錄"
#: .\templates\blog\tags\article_meta_info.html:6
msgid "posted in"
msgstr "發布於"
#: .\templates\blog\tags\article_meta_info.html:14
msgid "and tagged"
msgstr "並標記為"
#: .\templates\blog\tags\article_meta_info.html:25
msgid "by "
msgstr "由"
#: .\templates\blog\tags\article_meta_info.html:29
#, python-format
msgid ""
"\n"
" title=\"View all articles published by "
"%(article.author.username)s\"\n"
" "
msgstr ""
"\n"
" title=\"查看所有由 %(article.author.username)s\"發布的文章\n"
" "
#: .\templates\blog\tags\article_meta_info.html:44
msgid "on"
msgstr "在"
#: .\templates\blog\tags\article_meta_info.html:54
msgid "edit"
msgstr "編輯"
#: .\templates\blog\tags\article_pagination.html:4
msgid "article navigation"
msgstr "文章導航"
#: .\templates\blog\tags\article_pagination.html:9
msgid "earlier articles"
msgstr "早期文章"
#: .\templates\blog\tags\article_pagination.html:12
msgid "newer articles"
msgstr "較新文章"
#: .\templates\blog\tags\article_tag_list.html:5
msgid "tags"
msgstr "標簽"
#: .\templates\blog\tags\sidebar.html:7
msgid "search"
msgstr "搜索"
#: .\templates\blog\tags\sidebar.html:50
msgid "recent comments"
msgstr "近期評論"
#: .\templates\blog\tags\sidebar.html:57
msgid "published on"
msgstr "發表於"
#: .\templates\blog\tags\sidebar.html:65
msgid "recent articles"
msgstr "近期文章"
#: .\templates\blog\tags\sidebar.html:77
msgid "bookmark"
msgstr "書簽"
#: .\templates\blog\tags\sidebar.html:96
msgid "Tag Cloud"
msgstr "標簽雲"
#: .\templates\blog\tags\sidebar.html:107
msgid "Welcome to star or fork the source code of this site"
msgstr "歡迎您STAR或者FORK本站源代碼"
#: .\templates\blog\tags\sidebar.html:118
msgid "Function"
msgstr "功能"
#: .\templates\blog\tags\sidebar.html:120
msgid "management site"
msgstr "管理站點"
#: .\templates\blog\tags\sidebar.html:122
msgid "logout"
msgstr "登出"
#: .\templates\blog\tags\sidebar.html:129
msgid "Track record"
msgstr "運動軌跡記錄"
#: .\templates\blog\tags\sidebar.html:135
msgid "Click me to return to the top"
msgstr "點我返回頂部"
#: .\templates\oauth\oauth_applications.html:5
#| msgid "login"
msgid "quick login"
msgstr "快捷登錄"
#: .\templates\share_layout\nav.html:26
msgid "Article archive"
msgstr "文章歸檔"

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

Loading…
Cancel
Save