Compare commits

...

110 Commits

Author SHA1 Message Date
盛钧涛 9be7adbd14 Merge branch 'sjt_branch'
1 month ago
盛钧涛 beb070948b add 1111
1 month ago
盛钧涛 322e77599c Merge branch 'develop'
2 months ago
盛钧涛 3808109f8a add 111
2 months ago
盛钧涛 9b51b07881 Merge branch 'master' of https://bdgit.educoder.net/puhanfmc3/tentest
2 months ago
盛钧涛 466a6864bc add new code
2 months ago
陌渝 8bc899d943 Merge branch 'develop'
2 months ago
陌渝 fb5f469f4a Merge branch 'master' of https://bdgit.educoder.net/puhanfmc3/tentest
2 months ago
陌渝 30400814ab 1
2 months ago
陌渝 2cb5514f11 Merge remote-tracking branch 'origin' into develop
2 months ago
陌渝 54ab827919 Merge branch 'wr_branch' into develop
2 months ago
陌渝 9c3bf6e105 1
2 months ago
陌渝 4abe1ee4a9 2
2 months ago
盛钧涛 1c77fe8e22 Merge branch 'develop'
2 months ago
盛钧涛 184d73b9e4 Merge branch 'mk_branch' into develop
2 months ago
陌渝 75628202e3 1
2 months ago
陌渝 5c389a179c 1
2 months ago
陌渝 c47c8ea166 1
2 months ago
陌渝 9928651766 2
2 months ago
陌渝 24c10c00ce Merge branch 'wr_branch' of https://bdgit.educoder.net/puhanfmc3/tentest into wr_branch
2 months ago
陌渝 6352e52bed 1
2 months ago
陌渝 f9bbb4c87e 1
2 months ago
陌渝 26d4c9752f 1
2 months ago
陌渝 87d4d4e590 1
2 months ago
陌渝 c8413e7577 6
2 months ago
盛钧涛 c51b8b0c56 add oauth
2 months ago
盛钧涛 cedd0a20cf add oauth
2 months ago
盛钧涛 d52dc2800c Merge branch 'mk_branch'
2 months ago
盛钧涛 e9ed2007f8 Merge branch 'mk_branch' into develop
2 months ago
盛钧涛 48708d60dd Merge branch 'frr_branch'
2 months ago
盛钧涛 d6d92db1f3 Merge branch 'frr_branch' into develop
2 months ago
盛钧涛 8641273b52 11
2 months ago
盛钧涛 d888017d59 Merge branch 'frr_branch'
2 months ago
盛钧涛 afe4e723e0 del
2 months ago
盛钧涛 4c2e221de9 Merge branch 'frr_branch' into develop
2 months ago
盛钧涛 ebd9538c68 del
2 months ago
盛钧涛 0cc903fbc4 del
2 months ago
盛钧涛 8380c07eab del others
2 months ago
frr 2da8518e4f 修改
2 months ago
frr 37d35baab4 首次提交
2 months ago
frr db19f9ab12 111
2 months ago
frr de046165a5 2
2 months ago
frr d4b3780968 1
2 months ago
pxksbc67f 763ee35ee5 Add frrweek8work3
2 months ago
盛钧涛 c36961b293 change accounts
2 months ago
盛钧涛 6143a4a432 Merge branch 'master' into develop
2 months ago
盛钧涛 f91eca2d4d add DjangoBlog
2 months ago
盛钧涛 51967e47d9 add DjangoBlog
2 months ago
盛钧涛 0d52b1cd2e Merge branch 'develop'
2 months ago
盛钧涛 12ee321c7f del DjangoBlog
2 months ago
盛钧涛 017f932fe9 del DjangoBlog
2 months ago
盛钧涛 506541d6f7 add DjangoBlog
2 months ago
mk c06d15ba90 Merge branch 'mk_branch' into develop
2 months ago
盛钧涛 074e9f5476 Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 dcbee62795 add change
3 months ago
盛钧涛 58fcb6b24a Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 9e1408389e add change
3 months ago
mk 746a6adc0b add
3 months ago
mk d208e833ff Merge remote-tracking branch 'origin/develop' into develop
3 months ago
盛钧涛 2c96334b4b add week6work
3 months ago
盛钧涛 260a92ff65 Merge branch 'sjt_branch' into develop
3 months ago
盛钧涛 7fd7fde93d del test
3 months ago
盛钧涛 f2cdcfba87 add week5work
3 months ago
盛钧涛 245a18b53f add test
3 months ago
陌渝 b59800b7a9 删除文件
3 months ago
陌渝 509ec8360d 添加文件
3 months ago
陌渝 32dd36a0e2 添加文件
3 months ago
陌渝 8e8d9ea64c 添加文件
3 months ago
陌渝 e899883087 添加文件
3 months ago
陌渝 5be757fbbf 添加文件
3 months ago
盛钧涛 59897191ac del 2test
3 months ago
盛钧涛 a07a4e068b Merge branch 'frr_branch' of https://bdgit.educoder.net/puhanfmc3/tentest into develop
3 months ago
pxksbc67f 9792d18d9a Add frrweek4work2
3 months ago
盛钧涛 ee8fcfefcb add doc and src
3 months ago
盛钧涛 3e0fc26d2c shanchu
3 months ago
盛钧涛 22a24281be Merge branch 'develop' of https://bdgit.educoder.net/puhanfmc3/tentest into develop
3 months ago
puhanfmc3 62e81357dd Add week4work3
3 months ago
盛钧涛 15da01e6e9 rm week4work
3 months ago
puhanfmc3 84f4c9e8f3 Add week4work
3 months ago
盛钧涛 77df3e9e9e add src
3 months ago
盛钧涛 ac177291fe add doc
3 months ago
盛钧涛 43fabaeb51 add doc
3 months ago
盛钧涛 580968353e add src
3 months ago
盛钧涛 65f769084f add doc
3 months ago
盛钧涛 a7541d3093 test2
3 months ago
盛钧涛 1a332d9c92 test
3 months ago
盛钧涛 ed17f8fd02 Merge branch 'sjt_branch' of https://bdgit.educoder.net/puhanfmc3/tentest into sjt_branch
3 months ago
盛钧涛 9bdaac90a5 add doc and src
3 months ago
puhanfmc3 afc75321c9 Add src
3 months ago
puhanfmc3 3b612a260f Delete 'src'
3 months ago
puhanfmc3 04a70ac42b Add src
3 months ago
puhanfmc3 feddbef978 Add doc
3 months ago
puhanfmc3 cf77dfa0e7 Add src
3 months ago
puhanfmc3 2bb0abdc49 Add doc
3 months ago
puhanfmc3 cca871a53b Delete 'src.md'
3 months ago
puhanfmc3 4762f52d39 Delete 'doc.md'
3 months ago
puhanfmc3 cc874b667f Delete '.idea/vcs.xml'
3 months ago
puhanfmc3 797f57fe57 Delete '.idea/modules.xml'
3 months ago
puhanfmc3 45f725295e Delete '.idea/misc.xml'
3 months ago
puhanfmc3 2b8a829300 Delete '.idea/PythonGITproject.iml'
3 months ago
puhanfmc3 c6fb2238ac Delete '.idea/inspectionProfiles/profiles_settings.xml'
3 months ago
puhanfmc3 d4f097745d Delete '.idea/.gitignore'
3 months ago
puhanfmc3 fd175c6b8f Delete 'test1.py'
3 months ago
盛钧涛 731d3625dd add test1
3 months ago
盛钧涛 d0115e3660 add test1
3 months ago
陌渝 735a5e5a66 Merge branch 'master' of https://bdgit.educoder.net/puhanfmc3/tentest
4 months ago
陌渝 8583e895d3 提交wr666
4 months ago
pxksbc67f c138ab8d57 Add src
4 months ago
pxksbc67f c3c378b76b Add doc
4 months ago
盛钧涛 51df660506 xiugaichenggtest
4 months ago

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

@ -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,176 @@
name: 自动部署到生产环境
on:
workflow_run:
workflows: ["Django CI"]
types:
- completed
branches:
- master
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'production'
type: choice
options:
- production
- staging
image_tag:
description: '镜像标签 (默认: latest)'
required: false
default: 'latest'
type: string
skip_tests:
description: '跳过测试直接部署'
required: false
default: false
type: boolean
env:
REGISTRY: registry.cn-shenzhen.aliyuncs.com
IMAGE_NAME: liangliangyy/djangoblog
NAMESPACE: djangoblog
jobs:
deploy:
name: 构建镜像并部署到生产环境
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置部署参数
id: deploy-params
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
else
echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
echo "environment=production" >> $GITHUB_OUTPUT
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "skip_tests=false" >> $GITHUB_OUTPUT
fi
- name: 显示部署信息
run: |
echo "🚀 部署信息:"
echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
- name: 设置Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录私有镜像仓库
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: 提取镜像元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
- name: 构建并推送Docker镜像
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: 部署到生产服务器
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_PORT || 22 }}
script: |
echo "🚀 开始部署 DjangoBlog..."
# 检查kubectl是否可用
if ! command -v kubectl &> /dev/null; then
echo "❌ 错误: kubectl 未安装或不在PATH中"
exit 1
fi
# 检查命名空间是否存在
if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
exit 1
fi
# 更新deployment镜像
echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
kubectl set image deployment/djangoblog \
djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
-n ${{ env.NAMESPACE }}
# 重启deployment
echo "🔄 重启deployment..."
kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
# 等待deployment完成
echo "⏳ 等待deployment完成..."
kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
# 检查deployment状态
echo "✅ 检查deployment状态..."
kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
echo "🎉 部署完成!"
- name: 发送部署通知
if: always()
run: |
# 设置通知内容
if [ "${{ job.status }}" = "success" ]; then
TITLE="✅ DjangoBlog部署成功"
STATUS="成功"
else
TITLE="❌ DjangoBlog部署失败"
STATUS="失败"
fi
MESSAGE="部署状态: ${STATUS}
触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
部署环境: ${{ steps.deploy-params.outputs.environment }}
镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
提交者: ${{ github.actor }}
时间: $(date '+%Y-%m-%d %H:%M:%S')
查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# 发送Server酱通知
if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
--header "Content-Type: application/json" \
--data @/tmp/serverchan.json \
--silent > /dev/null
rm -f /tmp/serverchan.json
echo "📱 部署通知已发送"
fi

@ -0,0 +1,371 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# 标准测试 - Python 3.10
- python-version: "3.10"
test-type: "standard"
database: "mysql"
elasticsearch: false
coverage: false
# 标准测试 - Python 3.11
- python-version: "3.11"
test-type: "standard"
database: "mysql"
elasticsearch: false
coverage: false
# 完整测试 - 包含ES和覆盖率
- python-version: "3.11"
test-type: "full"
database: "mysql"
elasticsearch: true
coverage: true
# Docker构建测试
- python-version: "3.11"
test-type: "docker"
database: "none"
elasticsearch: false
coverage: false
name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
steps:
- name: Checkout代码
uses: actions/checkout@v4
- name: 设置测试信息
id: test-info
run: |
echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
if [ "${{ matrix.test-type }}" = "docker" ]; then
echo "skip_python_setup=true" >> $GITHUB_OUTPUT
else
echo "skip_python_setup=false" >> $GITHUB_OUTPUT
fi
# MySQL数据库设置 (只有需要数据库的测试才执行)
- name: 启动MySQL数据库
if: matrix.database == '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
# Elasticsearch设置 (只有完整测试才执行)
- name: 配置系统参数 (ES)
if: matrix.elasticsearch == true
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
- name: 启动Elasticsearch
if: matrix.elasticsearch == true
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'
# Python环境设置 (Docker测试跳过)
- name: 设置Python ${{ matrix.python-version }}
if: steps.test-info.outputs.skip_python_setup == 'false'
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'requirements.txt'
# 多层缓存策略优化
- name: 缓存Python依赖
if: steps.test-info.outputs.skip_python_setup == 'false'
uses: actions/cache@v4
with:
path: |
~/.cache/pip
.pytest_cache
key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
${{ runner.os }}-python-${{ matrix.python-version }}-
${{ runner.os }}-python-
# Django缓存优化 (测试数据库等)
- name: 缓存Django资源
if: matrix.test-type != 'docker'
uses: actions/cache@v4
with:
path: |
.coverage*
htmlcov/
.django_cache/
key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-django-${{ matrix.test-type }}-
${{ runner.os }}-django-
- name: 安装Python依赖
if: steps.test-info.outputs.skip_python_setup == 'false'
run: |
echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
python -m pip install --upgrade pip setuptools wheel
# 安装基础依赖
pip install -r requirements.txt
# 根据测试类型安装额外依赖
if [ "${{ matrix.coverage }}" = "true" ]; then
echo "📊 安装覆盖率工具"
pip install coverage[toml]
fi
# 验证关键依赖
echo "🔍 验证关键依赖安装"
python -c "import django; print(f'Django version: {django.get_version()}')"
python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
python -c "import elasticsearch; print('Elasticsearch client: OK')"
fi
# Django环境准备
- name: 准备Django环境
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🔧 准备Django测试环境"
# 等待数据库就绪
echo "⏳ 等待MySQL数据库启动..."
for i in {1..30}; do
if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
echo "✅ MySQL数据库连接成功"
break
fi
echo "🔄 等待数据库启动... ($i/30)"
sleep 2
done
# 等待Elasticsearch就绪 (如果启用)
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
echo "⏳ 等待Elasticsearch启动..."
for i in {1..30}; do
if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
echo "✅ Elasticsearch连接成功"
break
fi
echo "🔄 等待Elasticsearch启动... ($i/30)"
sleep 2
done
fi
# Django测试执行
- name: 执行数据库迁移
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🗄️ 执行数据库迁移"
# 检查迁移文件
echo "📋 检查待应用的迁移..."
python manage.py showmigrations
# 检查是否有未创建的迁移
python manage.py makemigrations --check --verbosity 2
# 执行迁移
python manage.py migrate --verbosity 2
echo "✅ 数据库迁移完成"
- name: 运行Django测试
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
# 显示Django配置信息
python manage.py diffsettings | head -20
# 运行测试
if [ "${{ matrix.coverage }}" = "true" ]; then
echo "📊 运行测试并生成覆盖率报告"
coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
echo "📈 生成覆盖率报告"
coverage xml
coverage report --show-missing
coverage html
echo "📋 覆盖率统计:"
coverage report | tail -1
else
echo "🧪 运行标准测试"
python manage.py test --verbosity=2 --failfast
fi
echo "✅ 测试执行完成"
# 覆盖率报告上传 (只有完整测试才执行)
- name: 上传覆盖率到Codecov
if: matrix.coverage == true && success()
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-${{ steps.test-info.outputs.test_name }}
fail_ci_if_error: false
verbose: true
- name: 上传覆盖率到Codecov (备用)
if: matrix.coverage == true && failure()
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
fail_ci_if_error: false
verbose: true
# Docker构建测试
- name: 设置QEMU
if: matrix.test-type == 'docker'
uses: docker/setup-qemu-action@v3
- name: 设置Docker Buildx
if: matrix.test-type == 'docker'
uses: docker/setup-buildx-action@v3
- name: Docker构建测试
if: matrix.test-type == 'docker'
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: djangoblog/djangoblog:test-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 收集测试工件 (失败时收集调试信息)
- name: 收集测试工件
if: failure() && matrix.test-type != 'docker'
run: |
echo "🔍 收集测试失败的调试信息"
# 收集Django日志
if [ -d "logs" ]; then
echo "📄 Django日志文件:"
ls -la logs/
if [ -f "logs/djangoblog.log" ]; then
echo "🔍 最新日志内容:"
tail -100 logs/djangoblog.log
fi
fi
# 显示数据库状态
echo "🗄️ 数据库连接状态:"
python -c "
try:
from django.db import connection
cursor = connection.cursor()
cursor.execute('SELECT VERSION()')
print(f'MySQL版本: {cursor.fetchone()[0]}')
cursor.execute('SHOW TABLES')
tables = cursor.fetchall()
print(f'数据库表数量: {len(tables)}')
except Exception as e:
print(f'数据库连接错误: {e}')
" || true
# Elasticsearch状态 (如果启用)
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
echo "🔍 Elasticsearch状态:"
curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
fi
# 上传测试工件
- name: 上传覆盖率HTML报告
if: matrix.coverage == true && always()
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ steps.test-info.outputs.test_name }}
path: htmlcov/
retention-days: 30
# 性能统计
- name: 测试性能统计
if: always() && matrix.test-type != 'docker'
run: |
echo "⚡ 测试性能统计:"
echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
# 系统资源使用情况
echo "💻 系统资源:"
echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
# 测试结果汇总
- name: 测试完成总结
if: always()
run: |
echo "📋 ============ 测试执行总结 ============"
echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
echo " 🐍 Python版本: ${{ matrix.python-version }}"
echo " 🗄️ 数据库: ${{ matrix.database }}"
echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
echo " 📊 覆盖率: ${{ matrix.coverage }}"
echo " ⚡ 状态: ${{ job.status }}"
echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================"
# 根据测试结果显示不同消息
if [ "${{ job.status }}" = "success" ]; then
echo "🎉 测试执行成功!"
else
echo "❌ 测试执行失败,请检查上面的日志"
fi

@ -0,0 +1,43 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
workflow_dispatch:
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,79 @@
# 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
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/

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

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

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

@ -0,0 +1,63 @@
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):
#sjt 后台创建用户表单:处理密码设置和验证
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # sjt 密码字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # sjt 确认密码字段
class Meta:
model = BlogUser # sjt 关联BlogUser模型
fields = ('email',) # sjt 表单包含的字段
def clean_password2(self):
#sjt 验证两次输入密码一致性
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):
#sjt 保存用户:加密密码并标记来源为后台
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) # sjt 加密密码
if commit:
user.source = 'adminsite' # sjt 标记来源为后台添加
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
#sjt 后台修改用户表单:配置修改用户的字段
class Meta:
model = BlogUser # sjt 关联BlogUser模型
fields = '__all__' # sjt 包含所有字段
field_classes = {'username': UsernameField} # sjt 用户名字段类
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
#sjt Admin后台用户管理配置定义显示和操作方式
form = BlogUserChangeForm # sjt 修改用户表单
add_form = BlogUserCreationForm # sjt 创建用户表单
list_display = ( # sjt 列表页显示的字段
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username') # sjt 列表页可点击的链接字段
ordering = ('-id',) # sjt 按ID倒序排列
search_fields = ('username', 'nickname', 'email') # sjt 搜索字段

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

@ -0,0 +1,128 @@
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):
#sjt 登录表单:定义登录表单字段样式
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#sjt 注册表单:验证用户名、邮箱、密码合法性
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# sjt 设置确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#sjt 验证邮箱唯一性:已存在则抛出异常
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model() # sjt 关联用户模型
fields = ("username", "email") # sjt 表单包含的字段
class ForgetPasswordForm(forms.Form):
#sjt 密码找回表单:验证邮箱、验证码、新密码合法性
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
) # sjt 新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
) # sjt 确认密码字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
) # sjt 邮箱字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
) # sjt 验证码字段
def clean_new_password2(self):
#sjt 验证两次输入密码一致性,并检查密码强度
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2) # sjt 调用Django密码验证器
return password2
def clean_email(self):
#sjt 验证邮箱是否已注册
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#sjt 验证验证码有效性
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
#sjt 发送验证码表单:验证邮箱格式
email = forms.EmailField(
label=_('Email'),
) # sjt 邮箱字段(用于发送验证码)

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

@ -0,0 +1,194 @@
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):
#sjt 测试初始化:创建测试客户端和测试用户
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--=" # sjt 测试用新密码
def test_validate_account(self):
#sjt 测试用户账号验证:管理员登录、文章创建等功能
site = get_current_site().domain
# sjt 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# sjt 测试登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# sjt 测试创建分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# sjt 测试创建文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# sjt 测试访问文章管理页
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#sjt 测试注册流程:注册->邮箱验证->登录->功能验证
# sjt 验证初始状态无此用户
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 提交注册表单
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# sjt 验证用户创建成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 验证邮箱并激活用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
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)
# sjt 测试登录及权限
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
# ... 后续测试创建分类、文章等功能
def test_verify_email_code(self):
#sjt 测试验证码验证功能
to_email = "admin@admin.com"
code = generate_code() # sjt 生成验证码
utils.set_code(to_email, code) # sjt 缓存验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
# sjt 验证正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# sjt 验证错误邮箱的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#sjt 测试成功发送密码找回验证码
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#sjt 测试发送密码找回验证码失败情况(无效邮箱)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
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):
#sjt 测试成功找回密码流程
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# sjt 验证密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first()
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#sjt 测试使用未注册邮箱找回密码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
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):
#sjt 测试使用错误验证码找回密码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # sjt 错误验证码
)
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" # sjt 应用命名空间
urlpatterns = [re_path(r'^login/$', # sjt 登录页面路由
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$', # sjt 注册页面路由
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$', # sjt 注销功能路由
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html', # sjt 注册/验证结果页路由
views.account_result,
name='result'),
re_path(r'^forget_password/$', # sjt 密码找回页面路由
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$', # sjt 发送密码找回验证码路由
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

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

@ -0,0 +1,46 @@
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) # sjt 验证码有效期5分钟
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
'''sjt 发送密码重置验证码邮件
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]:
'''sjt 验证验证码有效性
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误返回错误信息字符串否则返回None
'''
cache_code = get_code(email) # sjt 从缓存获取验证码
if cache_code != code: # sjt 验证码不匹配
return gettext("Verification code error")
def set_code(email: str, code: str):
#sjt 将验证码存入缓存有效期5分钟
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
#sjt 从缓存获取验证码
return cache.get(email)

@ -0,0 +1,225 @@
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):
#sjt 用户注册视图:处理注册表单提交,发送邮箱验证链接
form_class = RegisterForm # sjt 注册表单类
template_name = 'account/registration_form.html' # sjt 注册页面模板
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#sjt 处理请求分发添加CSRF保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:创建未激活用户,生成验证链接并发送邮件
if form.is_valid():
user = form.save(False) # sjt 不立即保存用户
user.is_active = False # sjt 初始为未激活状态(需邮箱验证)
user.source = 'Register' # sjt 标记来源为注册
user.save(True) # sjt 保存用户
site = get_current_site().domain # sjt 获取当前站点域名
# sjt 生成双重加密签名(防止链接篡改)
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG: # sjt 开发环境下使用本地域名
site = '127.0.0.1:8000'
path = reverse('account:result') # sjt 获取结果页路由
# sjt 构建验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# sjt 构建验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# sjt 发送验证邮件
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
# sjt 重定向到注册结果页
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
#sjt 用户注销视图:处理用户注销并跳转登录页
url = '/login/' # sjt 注销后跳转的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发,禁止缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
#sjt 处理GET请求执行注销操作并清除缓存
logout(request) # sjt 注销用户
delete_sidebar_cache() # sjt 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
#sjt 用户登录视图:支持用户名/邮箱登录,处理登录状态
form_class = LoginForm # sjt 登录表单类
template_name = 'account/login.html' # sjt 登录页面模板
success_url = '/' # sjt 登录成功默认跳转页
redirect_field_name = REDIRECT_FIELD_NAME # sjt 重定向字段名
login_ttl = 2626560 # sjt 记住登录状态的有效期(一个月,单位秒)
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发保护敏感参数、CSRF保护、禁止缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#sjt 构建上下文数据获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(** kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:验证用户凭据,处理登录状态(含"记住我"功能)
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid(): # sjt 验证用户凭据
delete_sidebar_cache() # sjt 删除侧边栏缓存
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) # sjt 登录用户
if self.request.POST.get("remember"): # sjt 如果勾选"记住我"
self.request.session.set_expiry(self.login_ttl) # sjt 设置会话有效期
return super(LoginView, self).form_valid(form)
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
#sjt 获取登录成功后的跳转URL验证安全性
redirect_to = self.request.POST.get(self.redirect_field_name)
# sjt 验证跳转URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
#sjt 注册/验证结果页:处理邮箱验证逻辑,激活用户账号
type = request.GET.get('type') # sjt 获取操作类型(注册/验证)
id = request.GET.get('id') # sjt 获取用户ID
user = get_object_or_404(get_user_model(), id=id) # sjt 获取用户对象
logger.info(type)
if user.is_active: # sjt 如果用户已激活,直接跳转首页
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register': # sjt 注册成功结果页
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else: # sjt 邮箱验证结果页
# sjt 验证签名是否正确
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign: # sjt 签名错误返回403
return HttpResponseForbidden()
user.is_active = True # sjt 激活用户
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', { # sjt 渲染结果页
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
#sjt 密码找回视图:验证验证码后重置密码
form_class = ForgetPasswordForm # sjt 密码找回表单
template_name = 'account/forget_password.html' # sjt 密码找回页面模板
def form_valid(self, form):
#sjt 表单验证通过后:更新用户密码并跳转登录页
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# sjt 加密并更新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # sjt 跳转登录页
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#sjt 发送密码找回验证码视图:处理发送验证码请求
def post(self, request: HttpRequest):
#sjt 处理POST请求验证邮箱并发送验证码
form = ForgetPasswordCodeForm(request.POST) # sjt 验证邮箱表单
if not form.is_valid(): # sjt 邮箱验证失败
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"] # sjt 获取目标邮箱
code = generate_code() # sjt 生成验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
utils.set_code(to_email, code) # sjt 缓存验证码有效期5分钟
return HttpResponse("ok") # sjt 发送成功返回"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,77 @@
# FRR该模块用于配置Django后台系统中评论(Comment)模型的管理界面,
# 包括自定义列表展示、批量操作、字段过滤及关联对象链接等功能,
# 方便管理员在后台对评论数据进行高效管理。
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 _
# FRR自定义批量操作函数用于将选中的评论设置为"禁用"状态
def disable_commentstatus(modeladmin, request, queryset):
# FRR通过queryset批量更新is_enable字段为False
queryset.update(is_enable=False)
# FRR自定义批量操作函数用于将选中的评论设置为"启用"状态
def enable_commentstatus(modeladmin, request, queryset):
# FRR通过queryset批量更新is_enable字段为True
queryset.update(is_enable=True)
# FRR为批量操作函数设置在后台显示的名称支持国际化
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# FRR评论模型(Comment)的后台管理配置类继承自Django默认的ModelAdmin
class CommentAdmin(admin.ModelAdmin):
# FRR每页显示20条评论数据
list_per_page = 20
# FRR列表页展示的字段包括自定义的关联对象链接字段
list_display = (
'id', # 评论ID
'body', # 评论内容
'link_to_userinfo', # 自定义字段:评论作者的链接
'link_to_article', # 自定义字段:评论所属文章的链接
'is_enable', # 是否启用
'creation_time' # 创建时间
)
# FRR列表页中可点击跳转详情页的字段
list_display_links = ('id', 'body', 'is_enable')
# LGM右侧过滤栏可按"是否启用"筛选评论
list_filter = ('is_enable',)
# FRR编辑页中排除的字段创建时间和最后修改时间通常自动生成不允许手动编辑
exclude = ('creation_time', 'last_modify_time')
# FRR注册批量操作函数在列表页顶部"动作"下拉框中显示
actions = [disable_commentstatus, enable_commentstatus]
# FRR将外键字段(author和article)显示为输入框(而非下拉框),适合数据量大的场景
raw_id_fields = ('author', 'article')
# FRR设置搜索框可搜索的字段按评论内容搜索
search_fields = ('body',)
# FRR自定义列表字段显示评论作者的链接点击可跳转到作者的后台编辑页
def link_to_userinfo(self, obj):
# FRR获取作者模型(author)的元数据用于生成admin链接
info = (obj.author._meta.app_label, obj.author._meta.model_name)
#FRR生成作者在admin后台的编辑页URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
#FRR返回HTML格式的链接显示作者昵称若无昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# FRR自定义列表字段显示评论所属文章的链接点击可跳转到文章的后台编辑页
def link_to_article(self, obj):
# FRR获取文章模型(article)的元数据用于生成admin链接
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# FRR生成文章在admin后台的编辑页URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# FRR返回HTML格式的链接显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# FRR设置自定义字段在列表页的表头名称支持国际化
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,10 @@
# FRR该模块定义了Django应用"comments"的配置类,
# 用于告诉Django如何识别和配置该应用是Django应用的核心配置文件之一。
from django.apps import AppConfig
# FRR评论应用(comments)的配置类继承自Django的AppConfig基类
class CommentsConfig(AppConfig):
# FRR应用的名称必须与应用目录名一致Django通过该名称识别应用
name = 'comments'

@ -0,0 +1,21 @@
# FRR该模块定义评论相关的表单类用于处理用户提交评论的数据验证和交互
# 继承Django的ModelForm实现与Comment模型的绑定简化表单开发流程。
from django import forms
from django.forms import ModelForm
from .models import Comment # 导入评论模型,用于表单与模型的关联
# FRR评论表单类继承ModelForm实现基于Comment模型的表单自动生成
class CommentForm(ModelForm):
# FRR定义父评论ID字段用于支持评论回复功能
# 采用HiddenInput控件隐藏输入框用户不可见但表单会提交该值
# required=False表示允许为空即该评论可以是一级评论无父评论
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
# FRRMeta内部类用于配置表单与模型的关联信息
class Meta:
model = Comment # 指定关联的模型为Comment
fields = ['body'] # 表单中需要包含的模型字段此处仅包含评论内容字段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,61 @@
# FRR该模块定义了评论(Comment)数据模型,用于存储用户对文章的评论信息,
# 包括评论内容、作者、关联文章、创建时间等字段,同时支持评论回复功能(父子评论关联)。
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 # 导入文章模型,建立评论与文章的关联
# FRR评论模型类继承自Django的Model基类映射数据库中的评论表
class Comment(models.Model):
# FRR评论正文字段TextField支持长文本max_length限制最大长度为300字符
body = models.TextField('正文', max_length=300)
# FRR评论创建时间字段默认值为当前时间verbose_name用于后台显示的字段名国际化
creation_time = models.DateTimeField(_('creation time'), default=now)
# FRR评论最后修改时间字段默认值为当前时间用于记录评论更新时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# FRR评论作者外键关联Django内置的用户模型settings.AUTH_USER_MODEL
# on_delete=models.CASCADE表示若用户被删除其评论也会被级联删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# FRR关联的文章外键评论属于某篇文章
# on_delete=models.CASCADE表示若文章被删除其下所有评论也会被级联删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# FRR父评论外键实现评论回复功能自关联
# 'self'表示关联自身模型blank=True和null=True允许为空即一级评论无父评论
# on_delete=models.CASCADE表示若父评论被删除其下所有子评论也会被级联删除
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
# FRR评论是否启用的状态字段默认值为False可能需要管理员审核后启用
# blank=False和null=False表示该字段为必填项不允许为空
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
# FRRMeta内部类用于配置模型的元数据
class Meta:
ordering = ['-id'] # 数据查询时按id降序排列最新评论在前
verbose_name = _('comment') # 模型的单数显示名称(国际化)
verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同)
get_latest_by = 'id' # 指定通过id字段获取最新记录
# FRR定义模型实例的字符串表示返回评论正文便于在后台和调试时识别对象
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,106 @@
# FRR该模块为评论功能(comments)的单元测试类,
# 主要测试评论提交、评论列表展示、评论嵌套结构解析及邮件通知等核心功能,
# 确保评论功能在各种场景下正常工作。
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse # 用于生成URL反向解析
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 # 导入工具函数测试ID获取功能
# FRR评论功能测试类继承TransactionTestCase以支持事务性测试避免测试数据污染
class CommentsTest(TransactionTestCase):
# LGM测试前的初始化方法会在每个测试方法执行前运行
def setUp(self):
self.client = Client() # 创建测试客户端,模拟用户请求
self.factory = RequestFactory() # 创建请求工厂,用于构造复杂请求
# FRR配置博客评论设置需要审核才能显示
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True # 评论需要审核
value.save() # 保存设置到数据库
# FRR创建超级用户用于测试登录状态下的评论功能
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1") # 用户名和密码用于测试登录
# FRR辅助方法批量更新文章下所有评论为"启用"状态(模拟管理员审核通过)
def update_article_comment_status(self, article):
comments = article.comment_set.all() # 获取文章下所有评论
for comment in comments:
comment.is_enable = True # 设为启用
comment.save() # 保存修改
# FRR核心测试方法验证评论提交、嵌套回复、模板渲染等功能
def test_validate_comment(self):
# 1. 登录测试用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类和文章(评论必须关联到具体文章)
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' # 文章类型(假设'a'表示普通文章)
article.status = 'p' # 发布状态(假设'p'表示已发布)
article.save() # 保存文章
# 3. 测试提交一级评论(无父评论)
# 生成评论提交URL通过文章ID反向解析
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# 模拟POST请求提交评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff' # 评论内容
})
# 验证提交成功302表示重定向通常是提交后跳回文章页
self.assertEqual(response.status_code, 302)
# 由于评论需要审核(默认未启用),此时评论列表应为空
article = Article.objects.get(pk=article.pk) # 重新获取文章(刷新数据)
self.assertEqual(len(article.comment_list()), 0) # 评论列表长度为0
# 手动审核评论(设为启用)
self.update_article_comment_status(article)
# 审核后评论列表应包含1条评论
self.assertEqual(len(article.comment_list()), 1)
# 4. 再次提交一条评论,测试多条评论场景
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302) # 验证提交成功
# 审核后评论列表应包含2条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# 5. 测试提交嵌套回复(针对第一条评论的回复)
parent_comment_id = article.comment_list()[0].id # 获取第一条评论ID作为父评论
# 提交带Markdown格式的回复内容测试富文本支持
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os

@ -0,0 +1,20 @@
# FRR该模块定义评论应用(comments)的URL路由配置
# 映射评论相关的视图函数/类,实现前端请求与后端处理逻辑的关联。
from django.urls import path # 导入Django的path函数用于定义URL路径
from . import views # 导入当前应用的views模块关联评论处理视图
app_name = "comments" # 定义应用命名空间避免不同应用间URL名称冲突
# FRRURL路由列表每个path对应一个评论相关的请求路径
urlpatterns = [
# FRR评论提交路由用于处理用户提交评论的请求
# 路径包含文章ID(article_id),通过<int:article_id>捕获整数类型的文章ID参数
# 关联视图类CommentPostView的as_view()方法(将类视图转为可调用视图函数)
# 命名为'postcomment',便于在模板中通过{% url 'comments:postcomment' article_id %}反向解析
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -0,0 +1,58 @@
# FRR该模块提供评论相关的邮件通知功能
# 当用户提交评论或收到回复时,自动发送自动发送邮件通知,提升用户交互体验。
import logging # 导入日志模块,用于记录记录发送邮件过程中的错误信息
from django.utils.translation import gettext_lazy as _ # 导入国际化翻译函数
from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数
from djangoblog.utils import send_email # 导入发送邮件的工具函数
# FRR创建日志记录器用于记录当前模块的日志信息
logger = logging.getLogger(__name__)
def send_comment_email(comment):
"""
FRR发送评论相关邮件通知
1. 向评论作者发送评论提交成功的感谢邮件
2. 若当前评论是回复有父评论向父评论作者发送回复通知邮件
:param comment: 评论对象Comment实例
"""
# FRR获取当前网站域名用于构建文章链接
site = get_current_site().domain
# FRR邮件主题支持国际化
subject = _('Thanks for your comment')
# FRR构建文章详情页的完整URL包含协议和域名
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# FRR1. 向当前评论作者发送感谢邮件
# 构建HTML格式的邮件内容支持国际化和变量替换
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) # 调用工具函数发送邮件
# FRR2. 若当前评论是回复(有父评论),通知父评论作者
try:
if comment.parent_comment: # 判断当前评论是否有父评论(即是否为回复)
# 构建回复通知的HTML邮件内容
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:
# FRR捕获发送过程中的异常并记录日志不中断主流程
logger.error(e)

@ -0,0 +1,82 @@
# FRR该模块定义评论提交的视图类负责处理用户提交评论的表单验证、
# 数据存储及页面跳转等逻辑,是评论功能与用户交互的核心处理层。
from django.core.exceptions import ValidationError # 导入验证异常类,用于处理评论关闭等错误
from django.http import HttpResponseRedirect # 导入重定向响应类,用于提交后跳转
from django.shortcuts import get_object_or_404 # 导入对象获取工具不存在时返回404
from django.utils.decorators import method_decorator # 导入方法装饰器工具
from django.views.decorators.csrf import csrf_protect # 导入CSRF保护装饰器
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 # 导入评论模型,用于存储评论数据
# FRR评论提交视图类继承FormView处理表单提交逻辑
class CommentPostView(FormView):
form_class = CommentForm # 指定使用的表单类为CommentForm
template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
# FRR为视图方法添加CSRF保护装饰器防止跨站请求伪造攻击
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
# 调用父类的dispatch方法确保视图正常处理请求
return super(CommentPostView, self).dispatch(*args, **kwargs)
# FRR处理GET请求直接访问评论提交URL时
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] # 从URL中获取文章ID
article = get_object_or_404(Article, pk=article_id) # 获取对应的文章不存在则返回404
url = article.get_absolute_url() # 获取文章详情页的URL
# 重定向到文章详情页的评论区(#comments为评论区锚点
return HttpResponseRedirect(url + "#comments")
# FRR表单验证失败时的处理逻辑
def form_invalid(self, form):
article_id = self.kwargs['article_id'] # 获取文章ID
article = get_object_or_404(Article, pk=article_id) # 获取文章对象
# 渲染文章详情页,传递错误的表单对象和文章对象(便于前端显示错误信息)
return self.render_to_response({
'form': form,
'article': article
})
# FRR表单验证成功后的处理逻辑核心方法
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user # 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) # 获取用户对应的BlogUser实例
article_id = self.kwargs['article_id'] # 从URL获取文章ID
article = get_object_or_404(Article, pk=article_id) # 获取文章对象
# FRR检查文章是否允许评论状态为关闭则抛出验证异常
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# FRR创建评论对象但不保存到数据库save(False)
comment = form.save(False)
comment.article = article # 关联评论到当前文章
# FRR根据网站设置决定评论是否需要审核默认需要审核is_enable=False
from djangoblog.utils import get_blog_setting
settings = get_blog_setting() # 获取博客全局设置
if not settings.comment_need_review: # 若无需审核
comment.is_enable = True # 直接设为启用状态
comment.author = author # 关联评论作者为当前登录用户
# FRR处理回复功能若存在父评论ID则关联父评论
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) # 获取父评论
comment.parent_comment = parent_comment # 关联到父评论
# FRR保存评论到数据库save(True)触发模型的save方法和信号
comment.save(True)
# FRR重定向到文章详情页的当前评论位置锚点定位到具体评论
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: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
path: /health/
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,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

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

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

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

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/djangoblog.iml" filepath="$PROJECT_DIR$/.idea/djangoblog.iml" />
</modules>
</component>
</project>

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

@ -0,0 +1,7 @@
#wr 配置当前应用的默认配置类
# 当Django加载该应用时若未在INSTALLED_APPS中显式指定配置类会自动使用此处指定的配置类
# 'djangoblog.apps.DjangoblogAppConfig' 表示配置类的完整路径:
# - djangoblog应用名称
# - apps存放配置类的模块
# - DjangoblogAppConfig具体的配置类通常包含应用初始化、信号注册等逻辑
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,89 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
from django.contrib.admin import AdminSite #wr 导入Django内置的AdminSite基类用于构建管理后台
class DjangoBlogAdminSite(AdminSite):
"""
自定义管理站点类继承自Django的AdminSite
用于个性化配置博客系统的管理后台如标题权限控制等
"""
#wr 管理后台页面顶部的标题(显示在登录页和管理首页的顶部横幅)
site_header = 'djangoblog administration'
#wr 管理后台的页面标题(显示在浏览器标签页上)
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""
初始化方法调用父类的初始化逻辑
:param name: 管理站点的标识名称默认使用'admin'与Django默认管理站点名称一致便于路由匹配
"""
super().__init__(name)
def has_permission(self, request):
"""
控制访问管理后台的权限校验
仅允许超级用户is_superuser为True访问管理后台
:param request: 当前HTTP请求对象包含用户信息等
:return: 布尔值True表示有权限访问False表示无权限
"""
return request.user.is_superuser
#wr 创建自定义管理站点的实例,名称为'admin'与Django默认管理站点标识一致便于在urls.py中配置路由
admin_site = DjangoBlogAdminSite(name='admin')
#wr 以下为注册模型到自定义管理站点:
#wr 将数据模型与对应的Admin配置类绑定使模型在管理后台可见并可操作
#wr 注册文章模型(Article)及其实例配置类(ArticlelAdmin),用于管理博客文章
admin_site.register(Article, ArticlelAdmin)
#wr 注册分类模型(Category)及其实例配置类(CategoryAdmin),用于管理文章分类
admin_site.register(Category, CategoryAdmin)
#wr 注册标签模型(Tag)及其实例配置类(TagAdmin),用于管理文章标签
admin_site.register(Tag, TagAdmin)
#wr 注册链接模型(Links)及其实例配置类(LinksAdmin),用于管理友情链接
admin_site.register(Links, LinksAdmin)
#wr 注册侧边栏模型(SideBar)及其实例配置类(SideBarAdmin),用于管理网站侧边栏内容
admin_site.register(SideBar, SideBarAdmin)
#wr 注册博客设置模型(BlogSettings)及其实例配置类(BlogSettingsAdmin),用于管理博客全局设置
admin_site.register(BlogSettings, BlogSettingsAdmin)
#wr 注册命令日志模型(commands)及其实例配置类(CommandsAdmin),用于管理系统命令执行记录
admin_site.register(commands, CommandsAdmin)
#wr 注册邮件发送日志模型(EmailSendLog)及其实例配置类(EmailSendLogAdmin),用于管理邮件发送记录
admin_site.register(EmailSendLog, EmailSendLogAdmin)
#wr 注册自定义用户模型(BlogUser)及其实例配置类(BlogUserAdmin),用于管理网站用户
admin_site.register(BlogUser, BlogUserAdmin)
#wr 注册评论模型(Comment)及其实例配置类(CommentAdmin),用于管理文章评论
admin_site.register(Comment, CommentAdmin)
#wr 注册第三方登录用户模型(OAuthUser)及其实例配置类(OAuthUserAdmin),用于管理第三方登录用户
admin_site.register(OAuthUser, OAuthUserAdmin)
#wr 注册第三方登录配置模型(OAuthConfig)及其实例配置类(OAuthConfigAdmin),用于管理第三方登录平台配置
admin_site.register(OAuthConfig, OAuthConfigAdmin)
#wr 注册位置追踪日志模型(OwnTrackLog)及其实例配置类(OwnTrackLogsAdmin),用于管理位置追踪记录
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
#wr 注册站点模型(Site)及其实例配置类(SiteAdmin)用于管理多站点配置Django自带的sites框架
admin_site.register(Site, SiteAdmin)
#wr 注册操作日志模型(LogEntry)及其实例配置类(LogEntryAdmin),用于管理用户在管理后台的操作记录
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,29 @@
#wr 从Django的apps模块导入AppConfig类该类是所有应用配置的基类
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""
wr 自定义应用配置类用于配置'djangoblog'应用的行为
继承自Django的AppConfig可自定义应用初始化信号注册插件加载等逻辑
"""
#wr 指定模型默认的自增字段类型Django 3.2+新增配置)
#wr BigAutoField是大整数类型的自增字段可支持更大范围的ID值避免整数溢出
default_auto_field = 'django.db.models.BigAutoField'
#wr 应用的名称必须与应用的目录名一致Django通过此名称识别应用
name = 'djangoblog'
def ready(self):
"""
wr 应用就绪时执行的方法Django加载完应用后自动调用
通常用于执行初始化操作如注册信号加载插件启动定时任务等
"""
#wr 调用父类的ready方法确保基类的初始化逻辑正常执行
super().ready()
#wr 导入并加载插件(应用就绪后加载插件,确保插件依赖的资源已初始化)
#wr 从当前应用的plugin_manage.loader模块导入load_plugins函数
from .plugin_manage.loader import load_plugins
#wr 执行插件加载函数,完成插件的注册和初始化
load_plugins()

@ -0,0 +1,177 @@
import _thread # 导入线程模块,用于异步执行任务(如发送邮件避免阻塞主进程)
import logging # 导入日志模块,用于记录系统运行信息和错误
import django.dispatch # 导入Django信号工具用于定义和处理自定义信号
from django.conf import settings # 导入Django项目配置
from django.contrib.admin.models import LogEntry # 导入管理员操作日志模型
from django.contrib.auth.signals import user_logged_in, user_logged_out # 导入用户登录/登出内置信号
from django.core.mail import EmailMultiAlternatives # 导入Django邮件工具支持HTML内容
from django.db.models.signals import post_save # 导入模型保存后触发的内置信号
from django.dispatch import receiver # 导入信号接收器装饰器
from comments.models import Comment # 导入评论模型
from comments.utils import send_comment_email # 导入发送评论通知邮件的工具函数
from djangoblog.spider_notify import SpiderNotify # 导入搜索引擎通知工具(如百度收录推送)
from djangoblog.utils import ( # 导入项目自定义工具函数
cache, # 缓存操作工具
expire_view_cache, # 清除视图缓存
delete_sidebar_cache, # 清除侧边栏缓存
delete_view_cache # 清除指定视图缓存
)
from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具
from oauth.models import OAuthUser # 导入第三方登录用户模型
#wr 初始化日志记录器(指定记录器名称为当前模块)
logger = logging.getLogger(__name__)
#wr 定义自定义信号第三方登录用户登录信号携带用户ID参数
oauth_user_login_signal = django.dispatch.Signal(['id'])
#wr 定义自定义信号:发送邮件信号(携带收件人、标题、内容参数)
send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""
wr发送邮件信号的接收器处理邮件发送逻辑并记录发送日志
当send_email_signal信号触发时自动执行此函数
"""
#wr 从信号参数中获取邮件信息
emailto = kwargs['emailto'] #wr 收件人列表
title = kwargs['title'] #wr 邮件标题
content = kwargs['content'] #wr 邮件内容HTML格式
#wr 创建邮件对象:指定标题、内容、发件人(从配置获取)、收件人
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto
)
msg.content_subtype = "html" #wr 声明邮件内容为HTML格式
#wr 初始化邮件发送日志记录
from servermanager.models import EmailSendLog #wr 导入邮件发送日志模型
log = EmailSendLog()
log.title = title #wr 记录邮件标题
log.content = content #wr 记录邮件内容
log.emailto = ','.join(emailto) #wr 记录收件人(用逗号拼接列表)
try:
#wr 发送邮件:返回成功发送的数量
result = msg.send()
log.send_result = result > 0 #wr 发送成功标识大于0表示至少成功发送1封
except Exception as e:
#wr 记录发送失败日志
logger.error(f"邮件发送失败,收件人: {emailto}, 错误: {e}")
log.send_result = False #wr 标记发送失败
log.save() #wr 保存日志记录到数据库
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""
wr 第三方登录用户登录信号的接收器处理用户头像本地化及缓存清理
当oauth_user_login_signal信号触发时自动执行此函数
"""
#wr 从信号参数中获取第三方用户ID
id = kwargs['id']
#wr 查询对应的第三方用户对象
oauthuser = OAuthUser.objects.get(id=id)
#wr 获取当前站点域名(用于判断头像是否为本地地址)
site = get_current_site().domain
#wr 若用户头像存在且不是本地地址(未包含当前站点域名),则本地化头像
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar #wr 导入头像保存工具
#wr 下载并保存头像到本地返回本地头像URL
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save() #wr 更新用户信息
#wr 清除侧边栏缓存(用户信息可能影响侧边栏显示,如登录状态)
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,** kwargs):
"""
wr 模型保存后信号的接收器处理模型保存后的后续操作如通知搜索引擎清理缓存等
当任意模型执行save()自动触发此函数
"""
clearcache = False # 缓存清理标识
#wr 忽略管理员操作日志模型(避免递归触发或无效处理)
if isinstance(instance, LogEntry):
return
#wr 若模型实例有get_full_url方法通常为可访问的内容模型如文章
if 'get_full_url' in dir(instance):
#wr 判断是否仅更新了浏览量字段(避免不必要的操作)
is_update_views = update_fields == {'views'}
#wr 非测试环境且非仅更新浏览量时,通知搜索引擎(如百度)更新收录
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url() # 获取模型实例的完整URL
SpiderNotify.baidu_notify([notify_url]) # 通知百度收录该URL
except Exception as ex:
logger.error("通知搜索引擎失败", ex) # 记录通知失败日志
#wr 非仅更新浏览量时,标记需要清理缓存
if not is_update_views:
clearcache = True
#wr 若保存的是评论模型实例
if isinstance(instance, Comment):
#wr 仅处理已启用的评论
if instance.is_enable:
#wr 获取评论所属文章的URL
path = instance.article.get_absolute_url()
#wr 获取当前站点域名(处理端口号,避免缓存键错误)
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')] # 移除端口号,仅保留域名
#wr 清除文章详情页的视图缓存(评论更新后需刷新页面)
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail'
)
#wr 清除SEO处理器缓存评论可能影响页面元数据
if cache.get('seo_processor'):
cache.delete('seo_processor')
#wr 清除该文章的评论缓存
comment_cache_key = f'article_comments_{instance.article.id}'
cache.delete(comment_cache_key)
#wr 清除侧边栏缓存(侧边栏可能显示最新评论)
delete_sidebar_cache()
#wr 清除评论列表视图的缓存
delete_view_cache('article_comments', [str(instance.article.pk)])
#wr 启动新线程发送评论通知邮件(异步执行,避免阻塞主请求)
_thread.start_new_thread(send_comment_email, (instance,))
#wr 若标记需要清理缓存,则清除全局缓存
if clearcache:
cache.clear()
@receiver(user_logged_in) #wr 绑定用户登录信号
@receiver(user_logged_out) #wr 绑定用户登出信号
def user_auth_callback(sender, request, user, **kwargs):
"""
wr 用户登录/登出信号的接收器处理用户身份变更后的缓存清理
当用户登录或登出时自动触发此函数
"""
#wr 若用户存在且用户名有效
if user and user.username:
logger.info(user) # 记录用户登录/登出日志
delete_sidebar_cache() # 清除侧边栏缓存(侧边栏可能显示用户状态)
#wr cache.clear() # 注释:可选清除全局缓存(视业务需求启用)

@ -0,0 +1,291 @@
#wr 导入必要模块
from django.utils.encoding import force_str #wr 用于字符串编码转换
from elasticsearch_dsl import Q #wr Elasticsearch查询构建工具
#wr 导入Haystack核心组件引擎、后端、查询基类及日志工具
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm #wr Haystack默认搜索表单
from haystack.models import SearchResult #wr Haystack搜索结果封装类
from haystack.utils import log as logging #wr Haystack日志工具
#wr 导入项目自定义的Elasticsearch文档和文档管理器用于文章索引管理
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article #wr 博客文章模型
#wr 初始化日志记录器
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""
wr自定义Elasticsearch搜索后端继承自Haystack的BaseSearchBackend
负责与Elasticsearch交互处理索引创建更新删除及搜索逻辑
"""
def __init__(self, connection_alias, **connection_options):
"""
wr初始化方法设置Elasticsearch连接参数及文档管理器
:param connection_alias: 连接别名用于多后端区分
:param connection_options: 连接选项如主机端口等
"""
super(ElasticSearchBackend, self).__init__(connection_alias,** connection_options)
self.manager = ArticleDocumentManager() #wr 初始化文章文档管理器(处理索引操作)
self.include_spelling = True #wr 启用拼写建议功能
def _get_models(self, iterable):
"""
wr将模型实例转换为Elasticsearch文档对象
:param iterable: 模型实例列表如文章列表
:return: 转换后的Elasticsearch文档列表
"""
#wr 若输入为空,默认获取所有文章;否则使用输入的模型实例
models = iterable if iterable and iterable[0] else Article.objects.all()
#wr 调用文档管理器将模型转换为文档
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
"""
wr创建索引并初始化数据
:param models: 需要索引的模型实例列表
"""
self.manager.create_index() #wr 创建Elasticsearch索引
docs = self._get_models(models) #wr 转换模型为文档
self.manager.rebuild(docs) #wr 重建索引(全量覆盖)
def _delete(self, models):
"""
wr从索引中删除模型对应的文档
:param models: 需要删除的模型实例列表
:return: 操作结果True表示成功
"""
for m in models:
m.delete() #wr 调用文档的删除方法实际由ArticleDocument实现
return True
def _rebuild(self, models):
"""
wr重建索引增量更新
:param models: 需要更新的模型实例列表为空则更新所有文章
"""
models = models if models else Article.objects.all() #wr 处理空输入
docs = self.manager.convert_to_doc(models) #wr 转换模型为文档
self.manager.update_docs(docs) #wr 增量更新索引
def update(self, index, iterable, commit=True):
"""
wrHaystack接口更新索引用于Haystack的信号触发更新
:param index: 索引名称当前实现未使用
:param iterable: 需更新的模型实例列表
:param commit: 是否立即提交当前实现未使用
"""
models = self._get_models(iterable) #wr 转换模型为文档
self.manager.update_docs(models) #wr 执行更新
def remove(self, obj_or_string):
"""
wrHaystack接口从索引中移除对象
:param obj_or_string: 模型实例或标识字符串
"""
models = self._get_models([obj_or_string]) #wr 转换为文档
self._delete(models) #wr 执行删除
def clear(self, models=None, commit=True):
"""
wrHaystack接口清空索引
:param models: 需清空的模型类当前实现未使用默认清空所有
:param commit: 是否立即提交当前实现未使用
"""
self.remove(None) #wr 调用删除方法清空
@staticmethod
def get_suggestion(query: str) -> str:
"""
wr获取搜索建议词基于Elasticsearch的term suggest功能
:param query: 原始搜索词
:return: 推荐的搜索词多个词用空格拼接
"""
#wr 构建搜索:匹配文章内容,并添加拼写建议
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
#wr 提取建议结果:若有建议则取第一个,否则保留原词
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords) # 拼接建议词为字符串
@log_query #wr Haystack装饰器记录查询日志
def search(self, query_string, **kwargs):
"""
wr核心方法执行搜索查询
:param query_string: 搜索关键词
:param kwargs: 额外参数如分页偏移量过滤条件等
:return: 搜索结果字典包含结果列表总命中数建议词等
"""
logger.info('search query_string:' + query_string) #wr 记录搜索关键词
#wr 获取分页参数(起始偏移量和结束偏移量)
start_offset = kwargs.get('start_offset', 0)
end_offset = kwargs.get('end_offset', 10) #wr 默认返回10条结果
#wr 处理搜索建议:若启用建议模式,则获取推荐词;否则使用原词
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
#wr 构建Elasticsearch查询
#wr 1. 布尔查询匹配标题或内容至少70%匹配度)
#wr 2. 过滤条件状态为已发布status='p'、类型为文章type='a'
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)], #wr 匹配标题或内容
minimum_should_match="70%") #wr 最小匹配度
#wr 执行搜索排除原始文档内容source=False应用分页
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute() #wr 执行查询
hits = results['hits'].total #wr 总命中数
raw_results = []
#wr 转换Elasticsearch结果为Haystack的SearchResult对象适配Haystack接口
for raw_result in results['hits']['hits']:
app_label = 'blog' #wr 应用标签文章属于blog应用
model_name = 'Article' #wr 模型名称
additional_fields = {} #wr 额外字段(当前未使用)
#wr 构建Haystack搜索结果对象
result = SearchResult(
app_label,
model_name,
raw_result['_id'], #wr 文档ID对应文章ID
raw_result['_score'], #wr 匹配分数
**additional_fields)
raw_results.append(result)
#wr 准备返回数据:结果列表、总命中数、空 facets暂未实现、拼写建议
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion #wr 若建议词与原词不同则返回
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
"""
wr自定义搜索查询类继承自Haystack的BaseSearchQuery
负责处理查询参数解析查询构建等逻辑
"""
def _convert_datetime(self, date):
"""
wr转换日期时间为Elasticsearch支持的格式
:param date: 日期时间对象
:return: 格式化的字符串如20231001123000
"""
if hasattr(date, 'hour'): #wr 包含小时信息datetime
return force_str(date.strftime('%Y%m%d%H%M%S'))
else: #wr 仅日期date默认时间为00:00:00
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
wr 清理查询片段用户输入处理保留词和特殊字符
:param query_fragment: 原始查询片段
:return: 清理后的查询字符串
"""
words = query_fragment.split() #wr 拆分关键词
cleaned_words = []
for word in words:
#wr 处理保留词(转为小写,避免冲突)
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
#wr 处理特殊字符:若包含保留字符则用单引号包裹
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = f"'{word}'"
break
cleaned_words.append(word)
return ' '.join(cleaned_words) #wr 拼接清理后的关键词
def build_query_fragment(self, field, filter_type, value):
"""
wr构建查询片段适配Haystack的查询构建逻辑
:param field: 查询字段
:param filter_type: 过滤类型
:param value: 查询值
:return: 查询字符串
"""
return value.query_string #wr 直接返回查询字符串(简化实现)
def get_count(self):
"""
wr获取搜索结果总数
:return: 结果数量
"""
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""
wr获取拼写建议适配Haystack接口
:param preferred_query: 优先查询词未使用
:return: 建议词
"""
return self._spelling_suggestion #wr 返回搜索后端生成的建议词
def build_params(self, spelling_query=None):
"""
wr 构建查询参数适配Haystack接口
:param spelling_query: 拼写建议查询词
:return: 参数字典
"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""
wr 自定义搜索表单继承自Haystack的ModelSearchForm
扩展搜索功能支持搜索建议开关
"""
def search(self):
"""
wr 执行搜索重写父类方法添加建议模式控制
:return: 搜索结果集SearchQuerySet
"""
#wr 根据表单参数设置是否启用搜索建议("is_suggest"为"no"时禁用)
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
#wr 调用父类方法执行搜索
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
"""
wr 自定义Elasticsearch引擎类继承自Haystack的BaseEngine
关联后端和查询类作为Haystack的引擎入口
"""
backend = ElasticSearchBackend #wr 指定使用的搜索后端
query = ElasticSearchQuery #wr 指定使用的查询类

@ -0,0 +1,97 @@
#wr 导入必要模块
from django.contrib.auth import get_user_model #wr 获取项目自定义的用户模型避免直接引用User类
from django.contrib.syndication.views import Feed #wr Django内置的RSS/Atom订阅生成基类
from django.utils import timezone #wr 处理时间相关操作(用于生成版权信息中的年份)
from django.utils.feedgenerator import Rss201rev2Feed #wr RSS 2.0标准格式生成器
from blog.models import Article #wr 博客文章模型RSS订阅的核心内容来源
from djangoblog.utils import CommonMarkdown #wr 自定义Markdown转换工具将Markdown转为HTML
class DjangoBlogFeed(Feed):
"""
wr自定义RSS订阅生成类继承自Django的Feed基类
用于生成博客文章的RSS订阅内容支持标准RSS 2.0格式
访问路径为/feed/用户可通过订阅工具获取最新文章推送
"""
#wr 指定RSS订阅的格式版本采用RSS 2.0标准(兼容性最广)
feed_type = Rss201rev2Feed
#wr RSS订阅的描述信息将显示在订阅工具的描述栏
description = '大巧无工,重剑无锋.'
#wr RSS订阅的标题显示在订阅工具的标题栏
title = "且听风吟 大巧无工,重剑无锋. "
#wr RSS订阅的访问URL路径与urls.py中配置的路由一致
link = "/feed/"
def author_name(self):
"""
wr订阅内容的作者名称
这里取项目中第一个用户的昵称默认作者可根据实际需求修改
:return: 作者昵称字符串
"""
return get_user_model().objects.first().nickname
def author_link(self):
"""
wr作者的个人页面链接
调用用户模型的get_absolute_url方法生成作者个人主页URL
:return: 作者个人页面的绝对URL
"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""
wrRSS订阅的核心内容列表即要推送的文章
过滤条件类型为文章type='a'状态为已发布status='p'
排序规则按发布时间倒序最新文章优先
数量限制仅取前5篇避免订阅内容过多
:return: 筛选后的文章查询集
"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""
wr单个订阅项文章的标题
直接使用文章自身的标题
:param item: 单个Article模型实例
:return: 文章标题字符串
"""
return item.title
def item_description(self, item):
"""
wr单个订阅项文章的描述内容
将文章的Markdown格式正文转换为HTMLRSS支持HTML格式确保排版正常
:param item: 单个Article模型实例
:return: 转换后的HTML格式文章内容
"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""
wr订阅内容的版权声明
动态获取当前年份生成格式为"Copyright© 年份 且听风吟"的版权信息
:return: 版权声明字符串
"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""
wr单个订阅项文章的访问链接
调用文章模型的get_absolute_url方法生成文章详情页的绝对URL
:param item: 单个Article模型实例
:return: 文章详情页的绝对URL
"""
return item.get_absolute_url()
def item_guid(self, item):
"""
wr单个订阅项文章的唯一标识GUID
用于订阅工具区分不同文章避免重复推送
原代码未完成实现建议返回文章的唯一标识如文章ID+URL组合或绝对URL
示例实现return f"{item.get_absolute_url()}?id={item.id}"
:param item: 单个Article模型实例
:return: 文章的唯一标识字符串
"""
return #wr 原代码未完成,需根据实际需求补充唯一标识逻辑

@ -0,0 +1,137 @@
from django.contrib import admin # 导入Django Admin核心模块
from django.contrib.admin.models import DELETION # 导入表示"删除"操作的常量
from django.contrib.contenttypes.models import ContentType # 导入内容类型模型(用于关联不同模型)
from django.urls import reverse, NoReverseMatch # 导入URL反向解析工具及异常
from django.utils.encoding import force_str # 用于字符串编码转换兼容Python 2/3
from django.utils.html import escape # 用于HTML转义防止XSS攻击
from django.utils.safestring import mark_safe # 标记安全的HTML字符串允许在模板中渲染
from django.utils.translation import gettext_lazy as _ # 国际化翻译工具
class LogEntryAdmin(admin.ModelAdmin):
"""
wr自定义LogEntry模型的Admin配置类
LogEntry是Django自带的模型用于记录管理员在后台的操作日志如新增修改删除对象
此类控制日志在Admin后台的显示搜索过滤及操作权限
"""
#wr 列表页的过滤条件(右侧过滤器):按内容类型(即操作的模型类型)过滤
list_filter = [
'content_type'
]
#wr 搜索字段可通过对象名称object_repr和操作描述change_message搜索日志
search_fields = [
'object_repr',
'change_message'
]
#wr 列表页中可点击的字段(点击跳转到日志详情页)
list_display_links = [
'action_time', #wr 操作时间
'get_change_message', #wr 操作描述(自定义方法)
]
#wr 列表页显示的字段(按顺序排列)
list_display = [
'action_time', #wr 操作时间
'user_link', #wr 操作人(带链接的自定义字段)
'content_type', #wr 操作的模型类型(如文章、用户等)
'object_link', #wr 操作的对象(带链接的自定义字段)
'get_change_message', #wr 操作描述Django原生方法返回格式化的操作信息
]
def has_add_permission(self, request):
"""
wr控制是否允许添加日志条目返回False禁止手动添加日志
原因日志是系统自动记录的不允许人工干预
"""
return False
def has_change_permission(self, request, obj=None):
"""
wr控制是否允许修改日志条目仅允许超级用户或有修改权限的用户以非POST方式访问即仅查看
原因日志记录应保持原始性禁止修改
"""
return (
request.user.is_superuser or #wr 超级用户有权限
request.user.has_perm('admin.change_logentry') #wr 有明确权限的用户
) and request.method != 'POST' #wr 禁止POST请求即禁止提交修改
def has_delete_permission(self, request, obj=None):
"""
wr控制是否允许删除日志条目返回False禁止删除日志
原因日志是系统操作记录需长期保存用于审计
"""
return False
def object_link(self, obj):
"""
wr自定义字段显示操作对象的链接若存在
功能如果操作不是删除且能获取到内容类型尝试生成对象的Admin修改页链接
"""
object_link = escape(obj.object_repr) #wr 转义对象名称防止XSS
content_type = obj.content_type #wr 获取操作的模型类型
#wr 若操作不是删除,且内容类型存在(即有对应的模型)
if obj.action_flag != DELETION and content_type is not None:
try:
#wr 生成对象在Admin中的修改页URL格式admin:应用名_模型名_change
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id] #wr 传入对象ID
)
#wr 生成带链接的对象名称
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
#wr 若URL反向解析失败如模型未注册到Admin则仅显示对象名称
pass
return mark_safe(object_link) #wr 标记为安全HTML允许在页面渲染链接
#wr 配置自定义字段的排序和显示名称
object_link.admin_order_field = 'object_repr' #wr 允许按对象名称排序
object_link.short_description = _('object') #wr 列表页表头显示名称(支持国际化)
def user_link(self, obj):
"""
wr自定义字段显示操作人的链接跳转到用户的Admin修改页
"""
#wr 获取操作人User模型的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user)) #wr 转义用户名并确保为字符串
try:
#wr 生成用户在Admin中的修改页URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk] #wr 传入用户ID
)
#wr 生成带链接的用户名
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
#wr 若URL解析失败仅显示用户名
pass
return mark_safe(user_link) #wr 标记为安全HTML
#wr 配置自定义字段的排序和显示名称
user_link.admin_order_field = 'user' #wr 允许按用户排序
user_link.short_description = _('user') #wr 列表页表头显示名称(支持国际化)
def get_queryset(self, request):
"""
wr优化查询集预加载content_type关联数据减少数据库查询次数
提升列表页加载性能避免N+1查询问题
"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type') #wr 预加载content_type
def get_actions(self, request):
"""
wr移除"删除选中项"操作确保日志无法通过批量操作删除
"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected'] #wr 删除批量删除操作
return actions

@ -0,0 +1,275 @@
#wr 导入必要模块日志用于记录插件运行信息Path用于路径处理
#wr Django模板相关模块用于模板渲染及处理模板不存在的异常
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist # wrDjango模板不存在时抛出的异常
from django.template.loader import render_to_string # wrDjango的模板渲染函数
#wr创建当前模块的日志记录器用于记录插件相关日志
logger = logging.getLogger(__name__)
class BasePlugin:
"""
wr插件基类所有自定义插件需继承此类并实现必要的抽象方法
提供插件元数据管理位置渲染模板处理静态资源管理等核心功能
实现了插件系统的基础框架
"""
# ===================== 插件元数据(子类必须定义) =====================
PLUGIN_NAME = None # wr插件名称例如"文章推荐插件"
PLUGIN_DESCRIPTION = None # wr插件描述说明插件功能
PLUGIN_VERSION = None # wr插件版本例如"1.0.0"
PLUGIN_AUTHOR = None #wr 插件作者(可选,可留空)
# ===================== 插件配置(子类可根据需求重写) =====================
SUPPORTED_POSITIONS = [] # wr插件支持的显示位置列表例如['sidebar', 'article_bottom']
DEFAULT_PRIORITY = 100 #wr 默认优先级:数字越小,插件在同位置越靠前显示
POSITION_PRIORITIES = {} #wr 位置特定优先级:为不同位置单独设置优先级(覆盖默认值)
def __init__(self):
"""
wr初始化插件实例
检查元数据完整性获取插件路径和标识符执行初始化逻辑并注册钩子
"""
#wr 校验插件元数据:名称、描述、版本为必填项,未定义则抛出异常
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("插件元数据PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION必须定义。")
#wr 获取插件所在目录路径和唯一标识符slug
self.plugin_dir = self._get_plugin_directory() # 插件目录路径Path对象
self.plugin_slug = self._get_plugin_slug() # 插件唯一标识(默认使用目录名)
# wr执行插件初始化逻辑子类可重写
self.init_plugin()
# wr注册插件钩子子类可重写以注册自定义钩子
self.register_hooks()
def _get_plugin_directory(self):
"""
wr获取插件所在的目录路径
通过反射获取当前插件类所在的文件路径进而得到目录路径
"""
import inspect
# 获取当前类(子类)的定义文件路径
plugin_file = inspect.getfile(self.__class__)
# 返回文件所在的目录路径
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""
wr获取插件的唯一标识符slug
默认使用插件目录的名称作为slug确保唯一性
"""
return self.plugin_dir.name
def init_plugin(self):
"""
wr插件初始化逻辑钩子方法
子类可重写此方法实现自定义初始化操作如加载配置连接数据库等
默认仅记录初始化日志
"""
logger.info(f'{self.PLUGIN_NAME} 已初始化。')
def register_hooks(self):
"""
wr注册插件钩子钩子方法
子类可重写此方法注册自定义钩子如监听系统事件注册URL路由等
默认不执行任何操作
"""
pass
# ===================== 位置渲染系统(核心功能) =====================
def render_position_widget(self, position, context, **kwargs):
"""
wr根据指定位置渲染插件组件是位置渲染的入口方法
Args:
position: 位置标识'sidebar'表示侧边栏
context: Django模板上下文包含页面渲染所需数据
**kwargs: 额外参数如文章ID用户信息等按需传递
Returns:
dict: 包含渲染结果的字典{'html': HTML内容, 'priority': 优先级, 'plugin_name': 插件名}
若位置不支持或无需显示则返回None
"""
#wr 检查当前位置是否在插件支持的位置列表中不支持则直接返回None
if position not in self.SUPPORTED_POSITIONS:
return None
#wr 检查插件是否应在当前位置显示调用should_display判断
if not self.should_display(position, context, **kwargs):
return None
#wr 动态拼接当前位置对应的渲染方法名如position为'sidebar',则方法名为'render_sidebar_widget'
method_name = f'render_{position}_widget'
#wr 检查当前类是否实现了对应位置的渲染方法
if hasattr(self, method_name):
#wr 调用对应方法获取HTML内容
html = getattr(self, method_name)(context, **kwargs)
#wr 若渲染成功返回非空HTML则构造结果字典
if html:
#wr 优先级:优先使用位置特定优先级,否则使用默认优先级
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
#wr 若未实现对应渲染方法或渲染失败返回None
return None
def should_display(self, position, context, **kwargs):
"""
wr判断插件是否应在指定位置显示钩子方法
子类可重写此方法实现条件显示逻辑如仅在特定页面/用户组显示
默认返回True始终显示
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: True表示显示False表示不显示
"""
return True
# ===================== 位置渲染方法(子类需按需重写) =====================
def render_sidebar_widget(self, context, **kwargs):
"""wr渲染侧边栏组件钩子方法。子类重写此方法实现侧边栏显示内容。"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""wr渲染文章底部组件钩子方法。子类重写此方法实现文章底部显示内容。"""
return None
def render_article_top_widget(self, context, **kwargs):
"""wr渲染文章顶部组件钩子方法。子类重写此方法实现文章顶部显示内容。"""
return None
def render_header_widget(self, context, **kwargs):
"""wr渲染页头组件钩子方法。子类重写此方法实现页头显示内容。"""
return None
def render_footer_widget(self, context, **kwargs):
"""wr渲染页脚组件钩子方法。子类重写此方法实现页脚显示内容。"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""wr渲染评论前组件钩子方法。子类重写此方法实现评论区前显示内容。"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""wr渲染评论后组件钩子方法。子类重写此方法实现评论区后显示内容。"""
return None
# ===================== 模板系统(插件模板渲染) =====================
def render_template(self, template_name, context=None):
"""
wr 渲染插件自带的模板文件
模板路径固定为"plugins/[插件slug]/[模板文件名]"
Args:
template_name: 模板文件名"sidebar.html"
context: 模板上下文字典类型默认为空
Returns:
str: 渲染后的HTML字符串若模板不存在返回空字符串并记录警告日志
"""
if context is None:
context = {} # 确保上下文不为None
#wr 构造模板路径:插件模板需放在"plugins/插件slug/"目录下
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
#wr 使用Django的render_to_string渲染模板
return render_to_string(template_path, context)
except TemplateDoesNotExist:
#wr 模板不存在时记录警告日志
logger.warning(f"插件模板不存在:{template_path}")
return ""
# ===================== 静态资源系统CSS/JS等资源管理 =====================
def get_static_url(self, static_file):
"""
wr获取插件静态文件的URL
静态文件需放在插件目录的"static/[插件slug]/"遵循Django静态文件规范
Args:
static_file: 静态文件相对路径"css/style.css"
Returns:
str: 静态文件的完整URL"/static/demo_plugin/css/style.css"
"""
from django.templatetags.static import static # wr导入Django静态文件工具
#wr 构造静态文件路径并生成URL
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""
wr获取插件所需的CSS文件列表钩子方法
子类重写此方法返回CSS文件路径列表系统会自动在页面加载这些CSS
Returns:
list: CSS文件路径列表["css/style.css"]
"""
return []
def get_js_files(self):
"""
wr获取插件所需的JavaScript文件列表钩子方法
子类重写此方法返回JS文件路径列表系统会自动在页面加载这些JS
Returns:
list: JS文件路径列表["js/script.js"]
"""
return []
def get_head_html(self, context=None):
"""
wr获取需要插入到HTML头部<head>标签内的内容钩子方法
子类重写此方法返回自定义头部内容如额外的CSS链接meta标签等
Args:
context: 模板上下文
Returns:
str: 需插入到<head>的HTML字符串
"""
return ""
def get_body_html(self, context=None):
"""
wr获取需要插入到HTML底部</body>标签前的内容钩子方法
子类重写此方法返回自定义底部内容如额外的JS脚本
Args:
context: 模板上下文
Returns:
str: 需插入到<body>底部的HTML字符串
"""
return ""
def get_plugin_info(self):
"""
wr获取插件的详细信息元数据+配置
用于插件管理界面展示插件信息
Returns:
dict: 包含插件信息的字典
"""
return {
'name': self.PLUGIN_NAME, #wr 插件名称
'description': self.PLUGIN_DESCRIPTION, #wr 插件描述
'version': self.PLUGIN_VERSION, #wr 插件版本
'author': self.PLUGIN_AUTHOR, #wr 插件作者
'slug': self.plugin_slug, #wr 插件唯一标识
'directory': str(self.plugin_dir), #wr 插件目录路径
'supported_positions': self.SUPPORTED_POSITIONS, # wr支持的显示位置
'priorities': self.POSITION_PRIORITIES #wr 各位置的优先级
}

@ -0,0 +1,31 @@
# wr文章相关操作事件钩子常量
# wr用于标识文章生命周期中不同操作的事件插件可通过监听这些事件执行对应逻辑
ARTICLE_DETAIL_LOAD = 'article_detail_load' # wr文章详情页加载事件当用户查看文章详情时触发
ARTICLE_CREATE = 'article_create' # wr文章创建事件当文章被创建时触发
ARTICLE_UPDATE = 'article_update' # wr文章更新事件当文章被修改时触发
ARTICLE_DELETE = 'article_delete' # wr文章删除事件当文章被删除时触发
# wr文章内容处理钩子名称
# wr用于标识对文章内容进行处理的钩子如内容过滤、替换、添加额外信息等
ARTICLE_CONTENT_HOOK_NAME = "the_content"
#wr 位置钩子常量字典
#wr 键:页面中的具体位置标识(如文章顶部、侧边栏等)
#wr 值:对应位置的组件钩子名称(插件可通过该钩子注册组件,系统会在对应位置渲染这些组件)
POSITION_HOOKS = {
'article_top': 'article_top_widgets', #wr 文章顶部位置的组件钩子
'article_bottom': 'article_bottom_widgets', #wr文章底部位置的组件钩子
'sidebar': 'sidebar_widgets', # wr侧边栏位置的组件钩子
'header': 'header_widgets', #wr 页头位置的组件钩子
'footer': 'footer_widgets', # wr页脚位置的组件钩子
'comment_before': 'comment_before_widgets', # wr评论区之前位置的组件钩子
'comment_after': 'comment_after_widgets', # wr评论区之后位置的组件钩子
}
#wr 资源注入钩子常量
# wr用于标识需要在HTML特定区域注入资源如CSS、JavaScript、meta标签等的钩子
HEAD_RESOURCES_HOOK = 'head_resources' # wrHTML头部<head>标签内)的资源注入钩子
BODY_RESOURCES_HOOK = 'body_resources' # wrHTML body底部</body>标签前)的资源注入钩子

@ -0,0 +1,85 @@
import logging
# wr创建当前模块的日志记录器用于记录钩子系统的运行日志如注册、执行、错误等
logger = logging.getLogger(__name__)
# wr私有字典用于存储所有注册的钩子
# wr键为钩子名称str值为该钩子对应的回调函数列表list[callable]
_hooks = {}
def register(hook_name: str, callback: callable):
"""
wr注册一个回调函数到指定钩子
一个钩子可以注册多个回调函数执行时会按注册顺序依次调用
Args:
hook_name: 钩子名称需与后续执行/过滤时的钩子名称对应
callback: 回调函数当钩子触发时需要执行的函数
"""
#wr 如果钩子名称不在_hooks中先初始化一个空列表用于存储回调
if hook_name not in _hooks:
_hooks[hook_name] = []
#wr 将回调函数添加到该钩子的回调列表中
_hooks[hook_name].append(callback)
#wr 记录调试日志,说明已注册钩子及对应的回调函数名
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
wr执行指定的"动作钩子Action Hook"
动作钩子用于触发一系列操作无返回值会按注册顺序执行所有绑定的回调函数
Args:
hook_name: 要执行的钩子名称
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
"""
# wr检查该钩子是否有已注册的回调函数
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
# wr遍历该钩子的所有回调函数并执行
for callback in _hooks[hook_name]:
try:
#wr 传递参数调用回调函数
callback(*args, **kwargs)
except Exception as e:
# wr捕获回调执行中的异常记录错误日志包含异常详情
logger.error(
f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True #wr 记录完整的异常堆栈信息
)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
wr执行指定的"过滤钩子Filter Hook"
过滤钩子用于对一个值进行链式处理有输入有输出会将初始值依次传递给所有绑定的回调函数
每个回调的返回值作为下一个回调的输入最终返回经过所有处理后的结果
Args:
hook_name: 要执行的钩子名称
value: 初始值需要被过滤/处理的值
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
Returns:
经过所有回调函数处理后的最终值
"""
#wr 检查该钩子是否有已注册的回调函数
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
# wr遍历该钩子的所有回调函数对值进行链式处理
for callback in _hooks[hook_name]:
try:
# wr调用回调函数处理当前值将返回值作为新值继续处理
value = callback(value, *args, **kwargs)
except Exception as e:
#wr 捕获回调执行中的异常,记录错误日志(包含异常详情)
logger.error(
f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True #wr 记录完整的异常堆栈信息
)
#wr 返回最终处理后的值(若没有回调,直接返回初始值)
return value

@ -0,0 +1,116 @@
import os
import logging
from django.conf import settings # wr导入Django配置用于获取插件相关设置
# wr创建当前模块的日志记录器用于记录插件加载过程中的日志信息
logger = logging.getLogger(__name__)
# wr全局插件注册表存储所有已成功加载的插件实例
_loaded_plugins = []
def load_plugins():
"""
wr动态加载并初始化'plugins'目录中的插件
此函数应在Django应用注册表就绪时调用确保Django环境已初始化
"""
global _loaded_plugins # wr声明使用全局变量_loaded_plugins
_loaded_plugins = [] # wr清空现有插件列表重新加载
# wr遍历配置中激活的插件列表settings.ACTIVE_PLUGINS定义了需要加载的插件名
for plugin_name in settings.ACTIVE_PLUGINS:
# wr构建插件目录的绝对路径PLUGINS_DIR插件根目录 + 插件名
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# wr检查插件目录是否存在且目录下是否有必要的'plugin.py'文件(插件入口)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# wr导入插件模块格式为'plugins.插件名.plugin'对应plugin.py文件
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# wr检查插件模块中是否有'plugin'属性(通常是插件类的实例)
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin # wr获取插件实例
_loaded_plugins.append(plugin_instance) # wr添加到全局注册表
# wr记录成功加载的日志包含插件名和插件元数据中的名称
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
# wr插件模块中没有'plugin'实例时记录警告
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
# wr导入插件模块失败时记录错误如文件缺失、语法错误等
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
# wr获取插件实例时发生属性错误如缺少必要属性
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
# wr其他未预料的错误
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""
wr获取所有已加载的插件实例列表
Returns:
list: 已初始化的插件实例集合
"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""
wr根据插件名称slug查找插件实例
实际通过插件的plugin_slug属性匹配与函数名对应可能存在命名统一设计
Args:
plugin_name: 插件的slug名称通常为插件目录名
Returns:
匹配的插件实例若未找到则返回None
"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""
wr根据插件slug查找插件实例与get_plugin_by_name功能一致可能为兼容保留
Args:
plugin_slug: 插件的唯一标识符slug
Returns:
匹配的插件实例若未找到则返回None
"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""
wr获取所有已加载插件的信息字典列表
信息由插件的get_plugin_info()方法提供包含元数据和配置
Returns:
list: 每个元素为一个插件的信息字典
"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""
wr筛选出支持指定显示位置的插件实例
Args:
position: 位置标识'sidebar''article_bottom'
Returns:
list: 所有支持该位置的插件实例
"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -0,0 +1,435 @@
"""
Django settings for djangoblog project.
项目基本配置文件包含Django运行所需的核心配置项
Generated by 'django-admin startproject' using Django 1.10.2.
基于Django 1.10.2版本创建
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
#wr 导入必要模块
import os
import sys
from pathlib import Path # 用于路径处理的现代工具
from django.utils.translation import gettext_lazy as _ # 国际化翻译工具
def env_to_bool(env, default):
"""
将环境变量值转换为布尔值
:param env: 环境变量名称
:param default: 当环境变量不存在时的默认值
:return: 转换后的布尔值环境变量为'True'时返回True否则返回default
"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
#wr 项目路径配置
# 构建项目内部路径BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent # 项目根目录(当前文件的父级父级目录)
#wr 快速开发设置 - 不适用于生产环境
# 安全警告:生产环境中必须保护好密钥
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# 优先从环境变量获取密钥,不存在则使用默认(仅开发环境用)
# 安全警告:生产环境必须关闭调试模式
DEBUG = env_to_bool('DJANGO_DEBUG', True) # 调试模式开关,默认开启
# DEBUG = False # 生产环境关闭调试的示例
#wr 测试模式判断:当执行命令包含'test'时视为测试环境
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
#wr 允许访问的主机列表(生产环境需指定具体域名,不可用'*'
# ALLOWED_HOSTS = [] # 默认空列表(仅允许本地访问)
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] # 开发环境允许所有主机访问
#wr Django 4.0新增配置信任的CSRF来源跨域请求时需要
CSRF_TRUSTED_ORIGINS = ['http://example.com']
#wr 应用定义安装的所有Django应用
INSTALLED_APPS = [
# 'django.contrib.admin', # 默认管理员界面(全功能版)
'django.contrib.admin.apps.SimpleAdminConfig', # 简化版管理员界面
'django.contrib.auth', # 身份认证系统
'django.contrib.contenttypes', # 内容类型框架(用于权限管理)
'django.contrib.sessions', # 会话管理
'django.contrib.messages', # 消息提示系统
'django.contrib.staticfiles', # 静态文件管理
'django.contrib.sites', # 多站点支持框架
'django.contrib.sitemaps', # 站点地图生成
'mdeditor', # Markdown编辑器第三方应用
'haystack', # 全文搜索框架(第三方应用)
'blog', # 自定义博客应用
'accounts', # 自定义用户账户应用
'comments', # 自定义评论应用
'oauth', # 第三方登录OAuth应用
'servermanager', # 服务器管理应用
'owntracks', # 位置追踪应用
'compressor', # 静态文件压缩工具(第三方应用)
'djangoblog' # 项目主应用
]
#wr 中间件配置(请求/响应处理的钩子函数)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全相关处理如HTTPS重定向
'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理中间件
'django.middleware.locale.LocaleMiddleware', # 国际化语言处理
'django.middleware.gzip.GZipMiddleware', # 响应内容GZip压缩
# 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新中间件(按需启用)
'django.middleware.common.CommonMiddleware', # 通用中间件如URL重写
# 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取中间件(按需启用)
'django.middleware.csrf.CsrfViewMiddleware', # CSRF防护中间件
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证中间件
'django.contrib.messages.middleware.MessageMiddleware', # 消息处理中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护
'django.middleware.http.ConditionalGetMiddleware', # 处理条件请求如304响应
'blog.middleware.OnlineMiddleware' # 自定义在线状态中间件
]
#wr URL根配置
ROOT_URLCONF = 'djangoblog.urls' # 主URL配置模块路径
#wr 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用Django模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录
'APP_DIRS': True, # 允许从应用内的templates目录加载模板
'OPTIONS': {
'context_processors': [ # 模板上下文处理器(全局变量)
'django.template.context_processors.debug', # 调试相关上下文
'django.template.context_processors.request', # 请求对象(request)
'django.contrib.auth.context_processors.auth', # 认证相关上下文
'django.contrib.messages.context_processors.messages', # 消息相关上下文
'blog.context_processors.seo_processor' # 自定义SEO相关上下文
],
},
},
]
#wr WSGI应用配置部署用
WSGI_APPLICATION = 'djangoblog.wsgi.application' # WSGI应用入口路径
wr# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库引擎
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', # 数据库密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机地址
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口默认3306
'OPTIONS': {
'charset': 'utf8mb4'}, # 字符集支持emoji表情
}}
#wr 密码验证配置
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
# 验证密码与用户属性(如用户名、邮箱)的相似度
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
# 验证密码最小长度默认8位
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
# 验证密码是否在常见密码列表中
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
# 验证密码是否仅包含数字
},
]
#wr 国际化配置
LANGUAGES = (
('en', _('English')), # 英语
('zh-hans', _('Simplified Chinese')), # 简体中文
('zh-hant', _('Traditional Chinese')), # 繁体中文
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), # 翻译文件存储目录
)
LANGUAGE_CODE = 'zh-hans' # 默认语言(简体中文)
TIME_ZONE = 'Asia/Shanghai' # 时区(上海)
USE_I18N = True # 启用国际化
USE_L10N = True # 启用本地化
USE_TZ = False # 不使用UTC时区使用本地时区
#wr 静态文件配置CSS、JavaScript、图片等
#wr 全文搜索配置基于Haystack框架
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用Whoosh搜索引擎支持中文
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引文件存储路径
},
}
#wr 实时更新搜索索引(当数据变化时自动更新)
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
#wr 认证后端:允许使用用户名或邮箱登录
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
#wr 静态文件收集目录生产环境用通过collectstatic命令收集
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
#wr 静态文件URL前缀
STATIC_URL = '/static/'
#wr 静态文件主目录
STATICFILES = os.path.join(BASE_DIR, 'static')
#wr 额外的静态文件目录(如插件静态文件)
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'plugins'), # 插件静态文件目录
]
#wr 自定义用户模型替换Django默认用户模型
AUTH_USER_MODEL = 'accounts.BlogUser'
# 登录页面URL未登录时访问受保护页面会重定向到此处
LOGIN_URL = '/login/'
#wr 时间格式配置(模板中使用)
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 完整时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' # 日期格式
#wr Bootstrap样式颜色类型前端样式用
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
#wr 分页配置
PAGINATE_BY = 10 # 每页显示10条数据
#wr HTTP缓存超时时间2592000 = 30天
CACHE_CONTROL_MAX_AGE = 2592000
#wr 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存
'TIMEOUT': 10800, # 缓存超时时间10800 = 3小时
'LOCATION': 'unique-snowflake', # 缓存位置标识(唯一即可)
}
}
#wr 若存在Redis环境变量则使用Redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', # Redis缓存引擎
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis连接地址
}
}
#wr 站点框架配置(用于多站点管理)
SITE_ID = 1
#wr 百度链接提交API地址用于SEO
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
#wr 邮件配置(用于发送通知、验证码等)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP邮件后端
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用TLS加密
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用SSL加密
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # 邮件服务器地址
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # 邮件服务器端口
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮箱用户名
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮箱密码/授权码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人
SERVER_EMAIL = EMAIL_HOST_USER # 服务器发件人(用于错误报告)
#wr 管理员邮箱(用于接收系统错误报告)
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
#wr 微信管理员密码二次MD5加密
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
#wr 日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs') # 日志文件存储目录
#wr 若日志目录不存在则创建
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1, # 日志配置版本
'disable_existing_loggers': False, # 不禁用已存在的日志器
'root': { # 根日志器
'level': 'INFO', # 日志级别INFO及以上
'handlers': ['console', 'log_file'], # 使用的处理器
},
'formatters': { # 日志格式
'verbose': { # 详细格式
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': { # 日志过滤器
'require_debug_false': { # 仅当DEBUG=False时生效
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': { # 仅当DEBUG=True时生效
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': { # 日志处理器
'log_file': { # 文件处理器
'level': 'INFO', # 处理INFO及以上级别
'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的文件处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
'when': 'D', # 每天轮转一次
'formatter': 'verbose', # 使用详细格式
'interval': 1, # 轮转间隔1天
'delay': True, # 延迟创建文件
'backupCount': 5, # 保留5个备份
'encoding': 'utf-8' # 编码格式
},
'console': { # 控制台处理器
'level': 'DEBUG', # 处理DEBUG及以上级别
'filters': ['require_debug_true'], # 仅调试模式生效
'class': 'logging.StreamHandler', # 流处理器(输出到控制台)
'formatter': 'verbose' # 使用详细格式
},
'null': { # 空处理器(不处理日志)
'class': 'logging.NullHandler',
},
'mail_admins': { # 邮件通知处理器
'level': 'ERROR', # 仅处理ERROR及以上级别
'filters': ['require_debug_false'], # 仅生产环境生效
'class': 'django.utils.log.AdminEmailHandler' # 发送邮件给管理员
}
},
'loggers': { # 日志器
'djangoblog': { # 项目主日志器
'handlers': ['log_file', 'console'], # 使用文件和控制台处理器
'level': 'INFO', # 日志级别
'propagate': True, # 是否向上传播日志
}
}
}
#wr 静态文件压缩配置使用django-compressor
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', # 从文件系统查找静态文件
'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 从应用目录查找静态文件
'compressor.finders.CompressorFinder', # 压缩器查找器
)
COMPRESS_ENABLED = True # 启用压缩
#wr 根据环境变量决定是否启用离线压缩(预压缩静态文件)
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
#wr 压缩文件输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
#wr 压缩文件名包含哈希值(用于缓存失效)
COMPRESS_CSS_HASHING_METHOD = 'mtime' # 基于修改时间生成哈希
COMPRESS_JS_HASHING_METHOD = 'mtime'
#wr CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter', # 转换为绝对URL
'compressor.filters.cssmin.CSSCompressorFilter', # CSS压缩
]
#wr JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.SlimItFilter', # JS压缩
]
#wr 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default' # 使用默认缓存
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey' # 缓存键生成函数
#wr 预编译配置处理Sass/SCSS
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'), # 编译SCSS
('text/x-sass', 'django_libsass.SassCompiler'), # 编译Sass
)
#wr 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
#wr 压缩器配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor' # CSS压缩器
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor' # JS压缩器
#wr 静态文件缓存配置(文件名包含哈希,用于缓存破坏)
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
#wr 压缩文件URL和根目录与静态文件一致
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
#wr 媒体文件配置(用户上传文件)
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') # 上传文件存储目录
MEDIA_URL = '/media/' # 媒体文件URL前缀
#wr X-Frame-Options配置允许同源页面嵌入iframe如Markdown编辑器预览
X_FRAME_OPTIONS = 'SAMEORIGIN'
#wr 默认自增字段类型Django 3.2+新增)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 使用BigInt类型自增ID
#wr Elasticsearch配置若存在环境变量则启用
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch地址
},
}
#wr 替换Haystack引擎为Elasticsearch
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
#wr 插件系统配置
PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录
ACTIVE_PLUGINS = [ # 激活的插件列表
'article_copyright', # 文章版权信息插件
'reading_time', # 阅读时间估算插件
'external_links', # 外部链接处理插件
'view_count', # 浏览量统计插件
'seo_optimizer', # SEO优化插件
'image_lazy_loading', # 图片懒加载插件
'article_recommendation', # 文章推荐插件
]

@ -0,0 +1,98 @@
#wr 导入Django站点地图核心类及相关工具
from django.contrib.sitemaps import Sitemap # 站点地图基类,所有自定义站点地图需继承此类
from django.urls import reverse # URL反向解析工具用于生成页面URL
#wr 导入博客相关模型(站点地图需包含这些模型对应的页面)
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""
wr静态页面站点地图用于收录网站中固定不变的静态页面如首页
站点地图Sitemap用于告诉搜索引擎网站的页面结构帮助爬虫高效索引
"""
priority = 0.5
#wr 页面更新频率可选值always, hourly, daily, weekly, monthly, yearly, never
changefreq = 'daily' #wr 静态页面假设每日更新
def items(self):
"""
wr返回需要收录的静态页面视图名称列表
这里仅包含博客首页视图名称为'blog:index'对应urls.py中的命名空间+名称
"""
return ['blog:index', ]
def location(self, item):
"""
wr生成每个静态页面的URL
:param item: items()返回的视图名称'blog:index'
:return: 页面的绝对URL
"""
return reverse(item) # 通过reverse反向解析视图名称为URL
class ArticleSiteMap(Sitemap):
"""wr文章页面站点地图用于收录所有已发布的博客文章页面"""
changefreq = "monthly" #wr 文章页面更新频率设为每月(假设文章发布后较少修改)
priority = "0.6" #wr 文章页面优先级设为0.6(高于静态页面,低于核心页面)
def items(self):
"""wr返回需要收录的文章对象列表仅包含状态为"已发布"status='p')的文章"""
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""
wr返回文章的最后修改时间供搜索引擎判断页面是否更新
:param obj: items()返回的Article实例
:return: 文章最后修改时间
"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""wr分类页面站点地图用于收录所有文章分类页面"""
changefreq = "Weekly" #wr 分类页面更新频率设为每周(分类信息较少变动)
priority = "0.6" #wr 分类页面优先级与文章页面相同
def items(self):
"""wr返回所有分类对象列表所有分类页面都需要被收录"""
return Category.objects.all()
def lastmod(self, obj):
"""wr返回分类的最后修改时间"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""wr标签页面站点地图用于收录所有文章标签页面"""
changefreq = "Weekly" #wr 标签页面更新频率设为每周
priority = "0.3" #wr 标签页面优先级较低0.3),因为重要性低于文章和分类
def items(self):
"""wr返回所有标签对象列表所有标签页面都需要被收录"""
return Tag.objects.all()
def lastmod(self, obj):
"""wr返回标签的最后修改时间"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""wr用户页面站点地图用于收录所有发表过文章的作者页面"""
changefreq = "Weekly" #v 用户页面更新频率设为每周
priority = "0.3" #wr 用户页面优先级较低
def items(self):
"""
wr返回所有发表过文章的作者列表去重处理
逻辑通过map提取所有已发布文章的作者再用set去重最后转为列表
"""
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""
wr返回用户相关的最后更新时间这里用用户注册时间代替也可改为用户最后发表文章时间
:param obj: items()返回的用户实例
:return: 用户注册时间
"""
return obj.date_joined

@ -0,0 +1,41 @@
import logging # 导入日志模块,用于记录通知过程中的信息和错误
import requests # 导入requests库用于发送HTTP请求与搜索引擎API交互
from django.conf import settings # 导入Django配置用于获取百度推送API地址等配置项
#wr 初始化日志记录器(指定记录器名称为当前模块)
logger = logging.getLogger(__name__)
class SpiderNotify():
"""
wr搜索引擎通知工具类用于向搜索引擎如百度推送网站新内容的URL
帮助搜索引擎快速发现并收录新页面提升SEO效率
"""
@staticmethod
def baidu_notify(urls):
"""
wr向百度搜索引擎推送URL使用百度链接提交API
百度会根据推送的URL优先抓取和收录页面
:param urls: 需要推送的URL列表如文章详情页链接
"""
try:
#wr 将URL列表转换为百度API要求的格式每行一个URL用换行符拼接
data = '\n'.join(urls)
#wr 发送POST请求到百度推送APIAPI地址从Django配置中获取
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
#wr 记录API返回结果便于监控推送是否成功
logger.info(result.text)
except Exception as e:
#wr 记录推送过程中的异常如网络错误、API地址错误等
logger.error(e)
@staticmethod
def notify(url):
"""
wr通用通知方法封装百度推送逻辑便于后续扩展其他搜索引擎
目前仅实现百度推送可根据需求添加谷歌必应等其他引擎的推送逻辑
:param url: 需要推送的单个URL或URL列表根据实际调用场景调整
"""
#wr 调用百度推送方法
SpiderNotify.baidu_notify(url)

@ -0,0 +1,42 @@
def setUp(self):
"""
wr测试前置初始化方法
会在每个测试方法以test_开头的方法执行前自动调用
通常用于创建测试数据初始化测试环境等
本案例暂无需初始化操作故为空实现
"""
pass
def test_utils(self):
"""
wr核心测试方法验证工具函数的基本功能
测试内容包括哈希计算Markdown转换字典转URL参数等工具函数
"""
# wr 测试SHA256哈希函数对字符串"test"进行哈希计算
md5 = get_sha256('test')
# wr 断言哈希结果不为None验证函数能正常生成哈希值无异常
self.assertIsNotNone(md5)
# wr 测试Markdown转换工具处理包含多级语法的Markdown文本
# wr 输入内容包括一级标题、Python代码块、两个不同链接
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,113 @@
"""djangoblog URL Configuration
Django博客项目的URL配置文件
The `urlpatterns` list routes URLs to views. For more information please see:
`urlpatterns`列表用于将URL路由到对应的视图更多信息请参考
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
函数视图示例
1. Add an import: from my_app import views
1. 导入视图from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
2. 添加URL到urlpatternsurl(r'^$', views.home, name='home')
Class-based views
类视图示例
1. Add an import: from other_app.views import Home
1. 导入类视图from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
2. 添加URL到urlpatternsurl(r'^$', Home.as_view(), name='home')
Including another URLconf
包含其他URL配置示例
1. Import the include() function: from django.conf.urls import url, include
1. 导入include()函数from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
2. 添加URL到urlpatternsurl(r'^blog/', include('blog.urls'))
"""
# wr导入Django配置
from django.conf import settings
#wr 导入国际化URL工具用于生成带语言前缀的URL
from django.conf.urls.i18n import i18n_patterns
#wr 导入静态文件URL配置工具
from django.conf.urls.static import static
# wr导入站点地图视图
from django.contrib.sitemaps.views import sitemap
# wr导入URL路径配置工具
from django.urls import path, include
from django.urls import re_path # wr兼容正则表达式的URL配置
#wr 导入Haystack搜索视图工厂
from haystack.views import search_view_factory
#wr 导入JSON响应工具
from django.http import JsonResponse
import time #wr 时间模块,用于健康检查的时间戳
from blog.views import EsSearchView
#wr 导入自定义管理员站点
from djangoblog.admin_site import admin_site
#wr 导入ElasticSearch搜索表单
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
#wr 导入RSS订阅源
from djangoblog.feeds import DjangoBlogFeed
#wr 导入站点地图配置类
from djangoblog.sitemap import (
ArticleSiteMap, CategorySiteMap,
StaticViewSitemap, TagSiteMap, UserSiteMap
)
#wr 站点地图配置:定义不同类型页面的站点地图
sitemaps = {
'blog': ArticleSiteMap, #wr文章页面站点地图
'Category': CategorySiteMap, #wr 分类页面站点地图
'Tag': TagSiteMap, #wr 标签页面站点地图
'User': UserSiteMap, #wr 用户页面站点地图
'static': StaticViewSitemap # wr静态页面如首页站点地图
}
# wr错误页面处理配置指定对应错误码的处理视图
handler404 = 'blog.views.page_not_found_view' # wr404页面不存在
handler500 = 'blog.views.server_error_view' # wr500服务器内部错误
handle403 = 'blog.views.permission_denied_view' # wr403权限拒绝
def health_check(request):
"""
wr健康检查接口
用于监控服务是否正常运行如容器化部署中的存活探针
"""
return JsonResponse({
'status': 'healthy', #wr 健康状态标识
'timestamp': time.time() # wr当前时间戳用于验证响应时效性
})
# wr基础URL配置不依赖语言前缀的URL
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # wr国际化配置入口语言切换等
path('health/', health_check, name='health_check'), # 健康检查接口
]
# wr国际化URL配置带语言前缀的URL如/en/blog/、/zh/blog/
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), # wr自定义管理员后台URL
re_path(r'', include('blog.urls', namespace='blog')), # wr博客主应用URL命名空间blog
re_path(r'mdeditor/', include('mdeditor.urls')), #wr Markdown编辑器URL
re_path(r'', include('comments.urls', namespace='comment')), # wr评论应用URL命名空间comment
re_path(r'', include('accounts.urls', namespace='account')), #wr 用户账户应用URL命名空间account
re_path(r'', include('oauth.urls', namespace='oauth')), #wr 第三方登录应用URL命名空间oauth
# wr站点地图XML文件URL搜索引擎会抓取此文件
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()), # wrRSS订阅源URL/feed/
re_path(r'^rss/$', DjangoBlogFeed()), #wr RSS订阅源URL/rss/与feed等价
# wr搜索功能URL使用自定义的ElasticSearch搜索视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), #wr 服务器管理应用URL
re_path(r'', include('owntracks.urls', namespace='owntracks')), # wr位置追踪应用URL
prefix_default_language=False #wr 不使用默认语言前缀(如默认语言为中文时,不强制添加/zh/
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) #wr 静态文件URL配置
#wr 开发环境下的媒体文件URL配置生产环境通常由Web服务器处理
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,408 @@
#!/usr/bin/env python
# encoding: utf-8
# 指定脚本解释器为Python编码格式为UTF-8
import logging #wr 日志模块,用于记录系统运行信息
import os #wr 操作系统交互模块,用于文件路径、目录操作
import random #wr 随机数模块,用于生成随机数据
import string #wr 字符串模块,包含字符串常量(如数字、字母等)
import uuid #wr 用于生成唯一标识符
from hashlib import sha256 #wr 哈希算法模块用于计算SHA-256哈希值
import bleach #wr HTML清理库用于过滤不安全的HTML标签和属性防止XSS攻击
import markdown #wr Markdown转换库用于将Markdown文本转为HTML
import requests #wr HTTP请求库用于发送网络请求如下载图片
from django.conf import settings #wr Django配置模块用于获取项目设置
from django.contrib.sites.models import Site #wr Django站点模型用于管理网站域名等信息
from django.core.cache import cache #wr Django缓存模块用于缓存数据提升性能
from django.templatetags.static import static #wr Django静态文件工具用于生成静态文件URL
#wr 初始化日志记录器(指定记录器名称为当前模块)
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""
wr获取文章和评论的最大ID
用于系统初始化或数据校验时获取当前最大的文章ID和评论ID
:return: 元组 (最大文章ID, 最大评论ID)
"""
from blog.models import Article # 导入文章模型
from comments.models import Comment # 导入评论模型
#wr 返回最新文章的ID和最新评论的ID假设模型有pk字段作为主键
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""
wr计算字符串的SHA-256哈希值
用于密码加密数据校验等场景哈希值不可逆确保数据安全
:param str: 待哈希的字符串
:return: 哈希后的十六进制字符串
"""
#wr 创建SHA-256哈希对象对字符串进行UTF-8编码后计算哈希
m = sha256(str.encode('utf-8'))
return m.hexdigest() #wr 返回十六进制格式的哈希结果
def cache_decorator(expiration=3 * 60):
"""
wr缓存装饰器用于缓存函数返回结果减少重复计算或数据库查询
:param expiration: 缓存过期时间默认3分钟
:return: 装饰器函数
"""
def wrapper(func):
def news(*args, **kwargs):
try:
#wr 尝试从视图对象中获取缓存键适用于Django视图函数
view = args[0]
key = view.get_cache_key()
except:
#wr 若无法从视图获取,则基于函数、参数生成唯一缓存键
key = None
if not key:
#wr 将函数和参数转换为字符串,确保唯一性
unique_str = repr((func, args, kwargs))
#wr 计算字符串的SHA-256哈希作为缓存键避免键过长
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
# wr尝试从缓存中获取数据
value = cache.get(key)
if value is not None:
#wr 缓存命中:返回缓存值(处理默认空值标记)
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
#wr 缓存未命中:执行原函数获取结果
logger.debug(f'cache_decorator set cache:{func.__name__} key:{key}')
value = func(*args, **kwargs)
# wr根据结果设置缓存空结果用特殊标记避免缓存穿透
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
wr刷新指定视图的缓存
用于在数据更新后主动清除对应视图的缓存确保用户获取最新数据
:param path: 视图对应的URL路径'/article/1/'
:param servername: 服务器域名'example.com'
:param serverport: 服务器端口'80'
:param key_prefix: 缓存键前缀可选
:return: 布尔值True表示缓存清除成功False表示未找到缓存
'''
from django.http import HttpRequest # wr导入HTTP请求类
from django.utils.cache import get_cache_key #wr 导入获取缓存键的工具
# wr构建模拟请求对象用于生成缓存键
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
# wr获取该请求对应的缓存键
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info(f'expire_view_cache:get key:{path}')
# wr若缓存存在则删除
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator() # wr应用缓存装饰器默认缓存3分钟
def get_current_site():
"""
wr 用于生成绝对URL网站标题等场景缓存减轻数据库压力
:return: Site模型实例
"""
site = Site.objects.get_current() # 获取当前站点Django默认功能
return site
class CommonMarkdown:
"""
wrMarkdown处理工具类提供Markdown文本到HTML的转换功能支持代码高亮和目录生成
"""
@staticmethod
def _convert_markdown(value):
"""
wr 内部方法执行Markdown转换
:param value: Markdown格式的文本
:return: 元组 (转换后的HTML内容, 目录HTML)
"""
# wr初始化Markdown转换器启用必要扩展
# - extra: 支持表格、脚注等扩展语法
# - codehilite: 代码高亮
# - toc: 生成目录
# - tables: 表格支持extra已包含这里冗余确保兼容性
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value) # 转换文本为HTML
toc = md.toc # 获取生成的目录HTML
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""
wr转换Markdown文本为HTML并返回内容和目录
:param value: Markdown文本
:return: 元组 (HTML内容, 目录HTML)
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""
wr转换Markdown文本为HTML仅返回内容忽略目录
:param value: Markdown文本
:return: HTML内容字符串
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""
wr 发送邮件通过Django信号机制解耦邮件发送逻辑
用于用户注册验证评论通知等场景
:param emailto: 收件人邮箱
:param title: 邮件标题
:param content: 邮件内容
"""
from djangoblog.blog_signals import send_email_signal # 导入邮件发送信号
#wr 发送信号实际发送逻辑由信号接收者实现如SMTP发送
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""
wr生成6位数字验证码
用于用户登录注册等场景的身份验证
:return: 6位数字字符串
"""
#wr 从数字字符集中随机选择6个字符并拼接
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""
wr将字典转换为URL查询参数字符串{'a':1, 'b':2} 'a=1&b=2'
用于构建带参数的URL
:param dict: 键值对字典
:return: URL查询参数字符串
"""
from urllib.parse import quote #wr 导入URL编码工具
#wr 对键和值进行URL编码处理特殊字符然后拼接为key=value&key=value格式
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
"""
wr获取博客系统设置如网站名称描述等带缓存机制
用于网站全局配置的统一管理
:return: BlogSettings模型实例
"""
#wr 尝试从缓存获取
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings #wr 导入博客设置模型
#wr 若数据库中无设置记录,创建默认设置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog' #wr 网站名称
setting.site_description = '基于Django的博客系统' #wr 网站描述
setting.site_seo_description = '基于Django的博客系统' #wr SEO描述
setting.site_keywords = 'Django,Python' #wr 网站关键词
setting.article_sub_length = 300 #wr 文章摘要长度
setting.sidebar_article_count = 10 #wr 侧边栏文章数量
setting.sidebar_comment_count = 5 #wr 侧边栏评论数量
setting.show_google_adsense = False #wr 是否显示谷歌广告
setting.open_site_comment = True #wr 是否开启评论
setting.analytics_code = '' #wr 统计代码如Google Analytics
setting.beian_code = '' #wr 备案号
setting.show_gongan_code = False #wr 是否显示公安备案号
setting.comment_need_review = False #wr 评论是否需要审核
setting.save() #wr 保存默认设置
#wr 从数据库获取设置
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value) #wr 缓存设置
return value
def save_user_avatar(url):
'''
wr下载并保存用户头像到本地静态文件目录
用于用户上传头像或第三方登录时获取头像
:param url: 头像图片的URL
:return: 本地头像的静态文件URL异常时返回默认头像
'''
logger.info(url) #wr 记录头像URL
try:
#wr 定义头像保存目录项目静态文件目录下的avatar文件夹
basedir = os.path.join(settings.STATICFILES, 'avatar')
#wr 发送GET请求下载图片超时2秒
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200: # 下载成功
#wr 若目录不存在则创建
if not os.path.exists(basedir):
os.makedirs(basedir)
#wr 支持的图片扩展名
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
#wr 判断URL是否指向图片文件
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
#wr 提取扩展名(非图片文件默认用.jpg
ext = os.path.splitext(url)[1] if isimage else '.jpg'
#wr 生成唯一文件名UUID避免重复
save_filename = str(uuid.uuid4().hex) + ext
logger.info(f'保存用户头像:{basedir}{save_filename}')
#wr 写入文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
#wr 返回静态文件URL如/static/avatar/xxx.jpg
return static('avatar/' + save_filename)
except Exception as e:
#wr 异常处理(如网络错误、文件写入失败等)
logger.error(e)
#wr 返回默认头像URL
return static('blog/img/avatar.png')
def delete_sidebar_cache():
"""
wr删除侧边栏相关缓存
用于侧边栏数据如热门文章最新评论更新后刷新缓存
"""
from blog.models import LinkShowType # 导入链接展示类型模型
#wr 生成所有侧边栏缓存键基于LinkShowType的取值
keys = ["sidebar" + x for x in LinkShowType.values]
#wr 逐个删除缓存
for k in keys:
logger.info(f'delete sidebar key:{k}')
cache.delete(k)
def delete_view_cache(prefix, keys):
"""
wr删除Django模板片段缓存
用于模板中通过{% cache %}标签缓存的内容更新后刷新
:param prefix: 缓存前缀对应{% cache %}的第一个参数
:param keys: 缓存键的动态部分对应{% cache %}的后续参数
"""
from django.core.cache.utils import make_template_fragment_key #wr 生成模板缓存键的工具
#wr 生成模板片段缓存键
key = make_template_fragment_key(prefix, keys)
cache.delete(key) #wr 删除缓存
def get_resource_url():
"""
wr获取静态资源的基础URL
用于统一管理静态文件路径如CSSJS图片等
:return: 静态资源基础URL字符串
"""
if settings.STATIC_URL:
#wr 若项目设置中定义了STATIC_URL直接使用
return settings.STATIC_URL
else:
#wr 否则使用当前站点域名拼接静态目录路径
site = get_current_site()
return 'http://' + site.domain + '/static/'
#wr HTML清理配置限制允许的HTML标签防止XSS攻击
#wr 只允许必要的标签,避免<script>、<iframe>等危险标签
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p', 'span', 'div']
# 允许的class属性值白名单仅包含代码高亮相关的class如codehilite、hll等
# 防止恶意注入样式类影响页面布局或触发XSS
ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""
wr自定义class属性过滤器仅保留白名单中的class值
用于bleach清理HTML时过滤不安全的class属性
:param tag: HTML标签名'span'
:param name: 属性名这里固定为'class'
:param value: 属性值'codehilite myclass'
:return: 过滤后的class值仅包含白名单中的类或False表示移除该属性
"""
if name == 'class':
#wr 拆分class值保留在白名单中的类
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
#wr 拼接允许的类若为空则返回False移除class属性
return ' '.join(allowed_classes) if allowed_classes else False
return value # 非class属性直接返回
#wr 允许的HTML属性白名单为每个标签指定允许的属性
#wr 如'a'标签允许'href'和'title',防止'onclick'等危险属性
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'span': class_filter, #wr 使用自定义过滤器处理span的class属性
'div': class_filter, #wr 使用自定义过滤器处理div的class属性
'pre': class_filter, #wr 使用自定义过滤器处理pre的class属性
'code': class_filter #wr 使用自定义过滤器处理code的class属性
}
#wr 允许的URL协议白名单限制链接只能使用安全协议
#wr 防止javascript:、data:等危险协议触发XSS
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
"""
wr安全清理HTML内容防止XSS攻击
使用bleach库按白名单过滤不安全的标签属性和协议
:param html: 原始HTML字符串
:return: 清理后的安全HTML字符串
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS, #wr 允许的标签
attributes=ALLOWED_ATTRIBUTES, #wr 允许的属性(含过滤器)
protocols=ALLOWED_PROTOCOLS, #wr 允许的URL协议
strip=True, #wr 移除不允许的标签(而非转义)
strip_comments=True #wr 移除HTML注释防止注释中的恶意代码
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
"""
WSGI config for djangoblog project.
WSGI配置文件用于djangoblog项目
It exposes the WSGI callable as a module-level variable named ``application``.
该文件将WSGI可调用对象暴露为模块级别的变量`application`供Web服务器如NginxGunicorn调用
For more information on this file, see
关于此文件的更多信息请参考
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
#wr 导入os模块用于处理操作系统环境变量
import os
#wr 从Django核心WSGI模块导入获取WSGI应用的函数
#wr get_wsgi_application()会返回Django项目的WSGI兼容应用实例
from django.core.wsgi import get_wsgi_application
#wr 设置Django使用的配置模块环境变量
#wr "DJANGO_SETTINGS_MODULE"是Django约定的环境变量用于指定项目配置文件路径
#wr 这里设置为"djangoblog.settings"即项目根目录下的settings.py文件
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
#wr 获取WSGI应用实例并赋值给application变量
#wr Web服务器如Gunicorn会通过这个变量与Django应用进行交互处理HTTP请求
application = get_wsgi_application()

@ -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 的部署!

@ -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 "文章歸檔"

@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

@ -0,0 +1,54 @@
import logging
from django.contrib import admin
# Register your models here.
from django.urls import reverse
from django.utils.html import format_html
logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin):
search_fields = ('nickname', 'email')
list_per_page = 20
list_display = (
'id',
'nickname',
'link_to_usermodel',
'show_user_image',
'type',
'email',
)
list_display_links = ('id', 'nickname')
list_filter = ('author', 'type',)
readonly_fields = []
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request):
return False
def link_to_usermodel(self, obj):
if obj.author:
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 show_user_image(self, obj):
img = obj.picture
return format_html(
u'<img src="%s" style="width:50px;height:50px"></img>' %
(img))
link_to_usermodel.short_description = '用户'
show_user_image.short_description = '用户头像'
class OAuthConfigAdmin(admin.ModelAdmin):
list_display = ('type', 'appkey', 'appsecret', 'is_enable')
list_filter = ('type',)

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

@ -0,0 +1,12 @@
from django.contrib.auth.forms import forms
from django.forms import widgets
class RequireEmailForm(forms.Form):
email = forms.EmailField(label='电子邮箱', required=True)
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
super(RequireEmailForm, self).__init__(*args, **kwargs)
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})

@ -0,0 +1,57 @@
# Generated by Django 4.1.7 on 2023-03-07 09:53
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 = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='OAuthConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, 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': 'oauth配置',
'verbose_name_plural': 'oauth配置',
'ordering': ['-created_time'],
},
),
migrations.CreateModel(
name='OAuthUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openid', models.CharField(max_length=50)),
('nickname', models.CharField(max_length=50, verbose_name='昵称')),
('token', models.CharField(blank=True, max_length=150, null=True)),
('picture', models.CharField(blank=True, max_length=350, null=True)),
('type', models.CharField(max_length=50)),
('email', models.CharField(blank=True, max_length=50, null=True)),
('metadata', models.TextField(blank=True, null=True)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': 'oauth用户',
'verbose_name_plural': 'oauth用户',
'ordering': ['-created_time'],
},
),
]

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

Loading…
Cancel
Save