Compare commits

..

7 Commits

8
.idea/.gitignore vendored

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

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

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

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

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

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/src" />
<option name="settingsModule" value="settings.py" />
<option name="manageScript" value="$MODULE_DIR$/src/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<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>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/templates" />
</list>
</option>
</component>
</module>

Binary file not shown.

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

@ -8,4 +8,5 @@ settings_production.py
*.md
docs/
logs/
static/
static/
.github/

@ -0,0 +1 @@
DJANGO_SECRET_KEY='f)jnngb81u_)*5!e%zclyr=zoh^61268qhm!kebtl8_c-pp_d^'

@ -38,10 +38,12 @@ jobs:
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: python
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
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

@ -19,55 +19,59 @@ on:
- '**/*.js'
jobs:
build-normal:
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
fail-fast: false
matrix:
python-version: ["3.10","3.11" ]
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: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
- name: Checkout代码
uses: actions/checkout@v4
- name: 设置测试信息
id: test-info
run: |
python manage.py makemigrations
python manage.py migrate
python manage.py test
build-with-es:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
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
@ -79,58 +83,289 @@ jobs:
mysql database: djangoblog
mysql user: root
mysql password: root
- name: Configure sysctl limits
# 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
- uses: miyataka/elasticsearch-github-actions@1
- 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'
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
# 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'
- name: Install Dependencies
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: |
python -m pip install --upgrade pip
echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
python -m pip install --upgrade pip setuptools wheel
# 安装基础依赖
pip install -r requirements.txt
- name: Run Tests
# 根据测试类型安装额外依赖
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: 127.0.0.1:9200
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
python manage.py makemigrations
python manage.py migrate
coverage run manage.py test
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
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:dev
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

@ -22,19 +22,19 @@ jobs:
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
context: .
push: true

3
src/.gitignore vendored

@ -60,11 +60,12 @@ target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html

@ -90,6 +90,7 @@ python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
--zyd202020 zbzydzfy123456
```
### 5. 运行项目
@ -146,7 +147,7 @@ python manage.py runserver
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs130 ls syj zyd164
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">

@ -9,21 +9,14 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""
用于在管理员界面创建BlogUser的表单
"""
# 密码输入字段
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',) # 表单包含的字段
fields = ('email',)
def clean_password2(self):
"""
验证两个密码字段是否匹配
"""
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
@ -32,39 +25,28 @@ class BlogUserCreationForm(forms.ModelForm):
return password2
def save(self, commit=True):
"""
以哈希格式保存密码
"""
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite' # 设置用户来源为管理站点
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""
用于在管理员界面修改BlogUser信息的表单
"""
class Meta:
model = BlogUser
fields = '__all__' # 包含所有字段
field_classes = {'username': UsernameField} # 指定username字段的类型
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""
BlogUser模型在管理员界面的配置类
"""
form = BlogUserChangeForm # 修改用户时使用的表单
add_form = BlogUserCreationForm # 创建用户时使用的表单
# 在列表中显示的字段
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
@ -73,10 +55,24 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
# 在列表中可以点击跳转到编辑页的字段
list_display_links = ('id', 'username')
# 默认排序方式
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')
# 修复字段集配置
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'nickname')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Additional info'), {'fields': ('source', 'creation_time', 'last_modify_time')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'email', 'password1', 'password2'),
}),
)

@ -2,8 +2,4 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
Django应用配置类用于配置accounts应用的基本信息
"""
name = 'accounts' # 定义应用的名称,与项目中的应用目录名一致
name = 'accounts'

@ -9,43 +9,28 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
"""
用户登录表单继承自Django内置的AuthenticationForm
"""
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# 自定义用户名输入框的样式和属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 自定义密码输入框的样式和属性
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""
用户注册表单继承自Django内置的UserCreationForm
"""
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# 自定义用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 自定义邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# 自定义密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 自定义确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""
验证邮箱是否已经被注册
"""
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
@ -53,14 +38,10 @@ class RegisterForm(UserCreationForm):
class Meta:
model = get_user_model()
fields = ("username", "email") # 定义表单包含的字段
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
"""
忘记密码表单用于用户重置密码
"""
# 新密码输入字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -71,7 +52,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -82,7 +62,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# 邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -93,7 +72,6 @@ class ForgetPasswordForm(forms.Form):
),
)
# 验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -105,9 +83,6 @@ class ForgetPasswordForm(forms.Form):
)
def clean_new_password2(self):
"""
验证两次输入的密码是否一致并验证密码强度
"""
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
@ -117,9 +92,6 @@ class ForgetPasswordForm(forms.Form):
return password2
def clean_email(self):
"""
验证邮箱是否存在
"""
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
@ -129,9 +101,6 @@ class ForgetPasswordForm(forms.Form):
return user_email
def clean_code(self):
"""
验证验证码是否正确
"""
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
@ -143,10 +112,6 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
"""
忘记密码时获取验证码的表单
"""
email = forms.EmailField(
label=_('Email'),
)

@ -7,66 +7,43 @@ import django.utils.timezone
class Migration(migrations.Migration):
# 标记这是一个初始迁移文件
initial = True
# 定义依赖关系依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
# 定义具体的操作
operations = [
# 创建BlogUser模型
migrations.CreateModel(
name='BlogUser',
fields=[
# 主键字段自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 密码字段最大长度128字符
('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', # 获取最新记录的字段
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
# 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -5,56 +5,42 @@ import django.utils.timezone
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系依赖于accounts应用的0001_initial迁移
dependencies = [
('accounts', '0001_initial'),
]
# 定义具体的迁移操作
operations = [
# 修改BlogUser模型的选项配置
migrations.AlterModelOptions(
name='bloguser',
options={
'get_latest_by': 'id', # 获取最新记录的字段
'ordering': ['-id'], # 默认排序方式
'verbose_name': 'user', # 单数名称(英文)
'verbose_name_plural': 'user' # 复数名称(英文)
},
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
# 移除BlogUser模型中的created_time字段
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
# 移除BlogUser模型中的last_mod_time字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
# 添加creation_time字段到BlogUser模型
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加last_modify_time字段到BlogUser模型
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# 修改BlogUser模型中的nickname字段
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
# 修改BlogUser模型中的source字段
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -9,93 +9,27 @@ from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
"""
<<<<<<< HEAD
博客用户模型类
继承自Django的AbstractUser扩展了额外的用户信息字段
Attributes:
nickname (CharField): 用户昵称最大长度100个字符可为空
creation_time (DateTimeField): 用户创建时间默认为当前时间
last_modify_time (DateTimeField): 用户信息最后修改时间默认为当前时间
source (CharField): 用户创建来源最大长度100个字符可为空
Meta:
ordering: 按照id倒序排列
verbose_name: 用户的可读性名称
verbose_name_plural: 用户的复数形式名称
get_latest_by: 指定用于latest()查询的字段
"""
=======
博客用户模型继承自Django的AbstractUser
扩展了用户的基本信息
"""
# 用户昵称,可为空
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 用户创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 用户最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 用户创建来源,可为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
<<<<<<< HEAD
获取用户详情页URL
通过reverse函数解析URL使用author_detail命名URL模式
Returns:
str: 用户详情页的相对URL路径
=======
获取用户详情页的绝对URL
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
"""
<<<<<<< HEAD
模型的字符串表示
Returns:
str: 用户邮箱地址
=======
定义对象的字符串表示返回用户的邮箱
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
return self.email
def get_full_url(self):
"""
<<<<<<< HEAD
获取用户页面的完整URL包含域名
用于构建完整的用户页面链接包含协议和域名
Returns:
str: 完整的用户页面URL格式为 https://{site}{path}
=======
获取用户的完整URL地址
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
<<<<<<< HEAD
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
=======
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数名称
verbose_name_plural = verbose_name # 复数名称
get_latest_by = 'id' # 获取最新记录的字段
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc

@ -12,26 +12,17 @@ from . import utils
# Create your tests here.
class AccountTest(TestCase):
"""
账户相关功能的测试类
"""
def setUp(self):
"""
测试前的准备工作创建测试客户端请求工厂和测试用户
"""
self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
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--=" # 测试用的新密码
self.new_test = "xxx123--="
def test_validate_account(self):
"""
测试账户验证功能
"""
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
@ -39,7 +30,6 @@ class AccountTest(TestCase):
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# 测试登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
@ -47,14 +37,12 @@ class AccountTest(TestCase):
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -64,26 +52,21 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# 测试访问文章管理URL
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""
测试用户注册功能
"""
self.assertEquals(
self.assertEqual(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 测试用户注册
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
self.assertEqual(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
@ -95,7 +78,6 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 测试登录后操作
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
@ -121,14 +103,12 @@ class AccountTest(TestCase):
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
@ -139,26 +119,18 @@ class AccountTest(TestCase):
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""
测试邮箱验证码验证功能
"""
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
# 测试正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# 测试错误邮箱
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""
测试忘记密码时成功获取验证码
"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
@ -168,17 +140,12 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""
测试忘记密码时获取验证码失败的情况
"""
# 测试空邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试无效邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
@ -186,9 +153,6 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""
测试通过邮箱成功重置密码
"""
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@ -211,9 +175,6 @@ class AccountTest(TestCase):
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""
测试为不存在的用户重置密码
"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -227,10 +188,8 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""
测试使用错误验证码重置密码
"""
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@ -246,3 +205,31 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
def test_favorites_page(self):
user = BlogUser.objects.create_superuser(
email="fav@user.com",
username="favuser",
password="favpass")
category = Category()
category.name = "favcategory"
category.save()
article = Article()
article.title = "favorite title"
article.body = "favorite body"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# 收藏
article.favorite_users.add(user)
# 登录并访问收藏页
self.client.login(username='favuser', password='favpass')
resp = self.client.get(reverse('account:favorites'))
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, "favorite title")

@ -4,38 +4,26 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts" # 定义应用的命名空间
app_name = "accounts"
urlpatterns = [
# 登录URL使用自定义的LoginForm表单
re_path(r'^login/$',
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
# 注册URL
re_path(r'^register/$',
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
# 登出URL
re_path(r'^logout/$',
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
# 账户操作结果页面URL
path(r'account/result.html',
path(r'account/result.html',
views.account_result,
name='result'),
# 忘记密码页面URL
re_path(r'^forget_password/$',
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
# 忘记密码验证码发送URL
re_path(r'^forget_password_code/$',
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
path('favorites/', views.FavoriteArticlesView.as_view(), name='favorites'),
]

@ -4,52 +4,23 @@ from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端允许用户使用用户名或邮箱进行登录
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
重写authenticate方法支持邮箱或用户名登录
Args:
request: HTTP请求对象
username: 用户名或邮箱
password: 密码
**kwargs: 其他参数
Returns:
用户对象或None
"""
# 判断输入的是邮箱还是用户名
if '@' in username:
kwargs = {'email': username} # 使用邮箱查询
kwargs = {'email': username}
else:
kwargs = {'username': username} # 使用用户名查询
kwargs = {'username': username}
try:
# 获取用户对象
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
# 用户不存在时返回None
return None
def get_user(self, username):
"""
根据用户ID获取用户对象
Args:
username: 用户ID
Returns:
用户对象或None
"""
try:
# 根据主键获取用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
# 用户不存在时返回None
return None

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5) # 验证码有效期为5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
@ -17,7 +17,6 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
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}
@ -31,32 +30,20 @@ def verify(email: str, code: str) -> typing.Optional[str]:
code: 验证码
Return:
如果有错误就返回错误str
Note:
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email) # 从缓存中获取存储的验证码
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error") # 验证码不匹配时返回错误信息
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code
将验证码存储到缓存中设置过期时间
Args:
email: 邮箱地址作为缓存的键
code: 验证码作为缓存的值
"""
cache.set(email, code, _code_ttl.seconds) # 使用邮箱作为键存储验证码
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code
从缓存中获取指定邮箱的验证码
Args:
email: 邮箱地址
Returns:
验证码字符串或None如果不存在或已过期
"""
return cache.get(email) # 从缓存中获取验证码
"""获取code"""
return cache.get(email)

@ -20,62 +20,42 @@ 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 django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
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__) # 创建日志记录器
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
"""
用户注册视图类
<<<<<<< HEAD
处理用户注册表单提交和验证邮箱功能
"""
form_class = RegisterForm
template_name = 'account/registration_form.html'
=======
"""
form_class = RegisterForm # 使用的表单类
template_name = 'account/registration_form.html' # 模板文件
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
@method_decorator(csrf_protect) # 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
调度方法添加CSRF保护装饰器
"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
<<<<<<< HEAD
处理有效的注册表单
保存用户信息发送验证邮件
=======
表单验证成功时的处理方法
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
if form.is_valid():
user = form.save(False) # 保存表单但不提交到数据库
user.is_active = False # 设置用户为非活跃状态,需要邮箱验证
user.source = 'Register' # 设置用户来源
user.save(True) # 提交到数据库
site = get_current_site().domain # 获取当前站点域名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000' # 调试模式下使用本地地址
path = reverse('account:result') # 获取验证结果页面URL
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构造验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -86,7 +66,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[
user.email,
@ -96,7 +75,7 @@ class RegisterView(FormView):
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url) # 重定向到注册结果页面
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
@ -104,80 +83,33 @@ class RegisterView(FormView):
class LogoutView(RedirectView):
"""
用户登出视图类
<<<<<<< HEAD
处理用户登出逻辑并重定向到登录页面
"""
url = '/login/'
=======
"""
url = '/login/' # 登出后重定向的URL
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
@method_decorator(never_cache) # 添加不缓存装饰器
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
调度方法添加不缓存装饰器
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""
处理GET请求执行登出操作
"""
<<<<<<< HEAD
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
=======
logout(request) # 执行登出
delete_sidebar_cache() # 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs) # 重定向到登录页
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
class LoginView(FormView):
"""
用户登录视图类
<<<<<<< HEAD
处理用户登录表单和认证逻辑
"""
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
=======
"""
form_class = LoginForm # 使用的表单类
template_name = 'account/login.html' # 模板文件
success_url = '/' # 登录成功后重定向的URL
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 登录会话保持时间(一个月)
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
@method_decorator(sensitive_post_parameters('password')) # 敏感参数保护
@method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 不缓存
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
<<<<<<< HEAD
"""
调度方法添加敏感参数保护CSRF保护和不缓存装饰器
"""
=======
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
<<<<<<< HEAD
获取上下文数据处理重定向URL
=======
获取上下文数据添加重定向URL
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
@ -186,39 +118,25 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""
<<<<<<< HEAD
处理有效的登录表单
进行用户认证并登录
=======
表单验证成功时的处理方法
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() # 删除侧边栏缓存
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) # 执行登录
if self.request.POST.get("remember"): # 如果选择了记住登录
self.request.session.set_expiry(self.login_ttl) # 设置会话过期时间
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
"""
获取登录成功后的重定向URL
"""
<<<<<<< HEAD
=======
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@ -227,117 +145,79 @@ class LoginView(FormView):
def account_result(request):
"""
<<<<<<< HEAD
账户操作结果视图函数
处理注册和邮箱验证的结果页面显示
"""
type = request.GET.get('type')
id = request.GET.get('id')
=======
账户操作结果页面
"""
type = request.GET.get('type') # 获取操作类型
id = request.GET.get('id') # 获取用户ID
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
user = get_object_or_404(get_user_model(), id=id) # 获取用户对象
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
return HttpResponseRedirect('/') # 如果用户已激活,重定向到首页
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功提示
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden() # 签名验证失败
user.is_active = True # 激活用户
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/') # 重定向到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""
忘记密码视图类
<<<<<<< HEAD
处理用户忘记密码的重置操作
"""
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
"""
处理有效的忘记密码表单
更新用户密码
=======
"""
form_class = ForgetPasswordForm # 使用的表单类
template_name = 'account/forget_password.html' # 模板文件
def form_valid(self, form):
"""
表单验证成功时的处理方法
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
if form.is_valid():
# 根据邮箱查找用户并更新密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"]) # 加密新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
<<<<<<< HEAD
忘记密码邮箱验证码视图类
处理通过邮箱发送验证码的请求
=======
忘记密码时发送验证码视图类
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
def post(self, request: HttpRequest):
"""
<<<<<<< HEAD
处理POST请求发送验证码到用户邮箱
"""
form = ForgetPasswordCodeForm(request.POST)
=======
处理POST请求发送验证码邮件
"""
form = ForgetPasswordCodeForm(request.POST) # 验证表单
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
if not form.is_valid():
return HttpResponse("错误的邮箱") # 表单验证失败返回错误信息
to_email = form.cleaned_data["email"] # 获取邮箱
code = generate_code() # 生成验证码
utils.send_verify_email(to_email, code) # 发送验证码邮件
utils.set_code(to_email, code) # 将验证码存储到缓存
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
return HttpResponse("ok") # 返回成功信息
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
<<<<<<< HEAD
return HttpResponse("ok")
=======
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
class FavoriteArticlesView(LoginRequiredMixin, ListView):
template_name = 'blog/article_index.html'
context_object_name = 'article_list'
paginate_by = settings.PAGINATE_BY if hasattr(settings, 'PAGINATE_BY') else 10
def get_queryset(self):
return self.request.user.favorite_articles.filter(status='p').order_by('-pub_time')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['page_type'] = '我的收藏'
ctx['tag_name'] = None
ctx['linktype'] = 'i'
ctx['sort'] = 'latest'
return ctx

@ -6,49 +6,33 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
class ArticleForm(forms.ModelForm):
"""
文章表单类用于在管理界面编辑文章
"""
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__' # 包含所有字段
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
"""
批量发布文章操作
"""
queryset.update(status='p') # 将选中的文章状态设置为已发布
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
"""
批量将文章设为草稿操作
"""
queryset.update(status='d') # 将选中的文章状态设置为草稿
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
"""
批量关闭文章评论功能
"""
queryset.update(comment_status='c') # 将选中的文章评论状态设置为关闭
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
"""
批量开启文章评论功能
"""
queryset.update(comment_status='o') # 将选中的文章评论状态设置为开启
queryset.update(comment_status='o')
# 为批量操作设置显示名称
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
@ -56,13 +40,10 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
"""
文章模型在管理界面的配置类
"""
list_per_page = 20 # 每页显示20条记录
search_fields = ('body', 'title') # 设置搜索字段
form = ArticleForm # 使用自定义表单
list_display = ( # 列表页显示的字段
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
@ -72,46 +53,36 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
list_display_links = ('id', 'title') # 列表页中可点击进入编辑页的字段
list_filter = ('status', 'type', 'category') # 设置过滤器
filter_horizontal = ('tags',) # 标签字段使用水平过滤器
exclude = ('creation_time', 'last_modify_time') # 在表单中排除这些字段
view_on_site = True # 显示"在站点上查看"链接
actions = [ # 注册批量操作
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
date_hierarchy = 'creation_time'
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
"""
在列表页显示分类的链接
"""
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category') # 设置列名
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
"""
获取表单限制作者字段只能选择超级用户
"""
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
"""
保存模型实例
"""
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
"""
获取在站点上查看的URL
"""
if obj:
url = obj.get_full_url()
return url
@ -122,38 +93,22 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
"""
标签模型在管理界面的配置类
"""
exclude = ('slug', 'last_mod_time', 'creation_time') # 排除字段
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
"""
分类模型在管理界面的配置类
"""
list_display = ('name', 'parent_category', 'index') # 列表页显示字段
exclude = ('slug', 'last_mod_time', 'creation_time') # 排除字段
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
"""
友情链接模型在管理界面的配置类
"""
exclude = ('last_mod_time', 'creation_time') # 排除字段
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
"""
侧边栏模型在管理界面的配置类
"""
list_display = ('name', 'content', 'is_enable', 'sequence') # 列表页显示字段
exclude = ('last_mod_time', 'creation_time') # 排除字段
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
"""
博客设置模型在管理界面的配置类
"""
pass

@ -2,7 +2,4 @@ from django.apps import AppConfig
class BlogConfig(AppConfig):
"""
Django应用配置类用于配置blog应用的基本信息
"""
name = 'blog' # 定义应用的名称,与项目中的应用目录名一致
name = 'blog'

@ -5,55 +5,39 @@ from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__) # 创建日志记录器
logger = logging.getLogger(__name__)
def seo_processor(requests):
"""
SEO上下文处理器为模板提供SEO相关数据
Args:
requests: HTTP请求对象
Returns:
dict: 包含SEO和网站配置信息的字典
"""
key = 'seo_processor' # 缓存键名
value = cache.get(key) # 从缓存中获取数据
key = 'seo_processor'
value = cache.get(key)
if value:
# 如果缓存中存在数据,直接返回
return value
else:
# 如果缓存中没有数据,记录日志并生成新数据
logger.info('set processor cache.')
setting = get_blog_setting() # 获取博客设置
# 构造返回值字典包含网站SEO和配置信息
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name, # 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
'nav_category_list': Category.objects.all(), # 导航分类列表
'nav_pages': Article.objects.filter( # 导航页面列表
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
'BEIAN_CODE': setting.beian_code, # 备案号
'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, # 当前年份
"GLOBAL_HEADER": setting.global_header, # 公共头部内容
"GLOBAL_FOOTER": setting.global_footer, # 公共尾部内容
"COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
# 将数据缓存10小时
cache.set(key, value, 60 * 60 * 10)
return value

@ -7,11 +7,9 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# 检查是否在Django设置中配置了Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -21,10 +19,8 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
# 尝试获取geoip管道
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果geoip管道不存在则创建一个用于添加地理位置信息的管道
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -38,81 +34,57 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
"""
地理IP信息内部文档类
"""
continent_name = Keyword() # 大洲名称
country_iso_code = Keyword() # 国家ISO代码
country_name = Keyword() # 国家名称
location = GeoPoint() # 地理位置坐标
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
"""
用户代理浏览器信息内部文档类
"""
Family = Keyword() # 浏览器家族
Version = Keyword() # 浏览器版本
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
"""
用户代理操作系统信息内部文档类
"""
pass
class UserAgentDevice(InnerDoc):
"""
用户代理设备信息内部文档类
"""
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
"""
用户代理完整信息内部文档类
"""
browser = Object(UserAgentBrowser, required=False) # 浏览器信息
os = Object(UserAgentOS, required=False) # 操作系统信息
device = Object(UserAgentDevice, required=False) # 设备信息
string = Text() # 完整的User-Agent字符串
is_bot = Boolean() # 是否为爬虫
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document):
"""
页面响应时间文档类用于记录网站性能数据
"""
url = Keyword() # 请求URL
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 记录时间
ip = Keyword() # 访问者IP
geoip = Object(GeoIp, required=False) # 地理位置信息
useragent = Object(UserAgent, required=False) # 用户代理信息
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance' # 索引名称
name = 'performance'
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime' # 文档类型
doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager:
"""
页面响应时间文档管理器类
"""
@staticmethod
def build_index():
"""
构建性能监控索引
"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -121,20 +93,13 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
"""
删除性能监控索引
"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""
创建并保存页面响应时间记录
"""
ElaspedTimeDocumentManager.build_index()
# 构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -151,125 +116,98 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# 创建文档实例
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000)) # 使用当前时间戳作为文档ID
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua,
ip=ip)
# 保存文档并使用geoip管道处理IP地理位置信息
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
"""
文章文档类用于Elasticsearch全文搜索
"""
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章正文使用IK分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章标题使用IK分词器
author = Object(properties={ # 作者信息
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
category = Object(properties={ # 分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
tags = Object(properties={ # 标签信息
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date() # 发布时间
status = Text() # 文章状态
comment_status = Text() # 评论状态
type = Text() # 文章类型
views = Integer() # 浏览量
article_order = Integer() # 文章排序
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog' # 索引名称
name = 'blog'
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article' # 文档类型
doc_type = 'Article'
class ArticleDocumentManager():
"""
文章文档管理器类用于管理文章在Elasticsearch中的索引
"""
def __init__(self):
self.create_index()
def create_index(self):
"""
创建文章索引
"""
ArticleDocument.init()
def delete_index(self):
"""
删除文章索引
"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
"""
将Django文章对象转换为Elasticsearch文档对象
"""
return [
ArticleDocument(
meta={
'id': article.id}, # 文档ID
body=article.body, # 文章正文
title=article.title, # 文章标题
author={ # 作者信息
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={ # 分类信息
category={
'name': article.category.name,
'id': article.category.id},
tags=[ # 标签信息
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time, # 发布时间
status=article.status, # 文章状态
comment_status=article.comment_status, # 评论状态
type=article.type, # 文章类型
views=article.views, # 浏览量
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
"""
重新构建文章索引
"""
ArticleDocument.init()
# 如果没有提供文章列表,则获取所有文章
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
# 逐个保存文档
for doc in docs:
doc.save()
def update_docs(self, docs):
"""
更新文档
"""
for doc in docs:
doc.save()

@ -3,33 +3,17 @@ import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__) # 创建日志记录器
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
"""
博客搜索表单类继承自Haystack的SearchForm
"""
# 定义搜索查询字段,设置为必填
querydata = forms.CharField(required=True)
def search(self):
"""
执行搜索操作
Returns:
搜索结果数据
"""
# 调用父类的搜索方法
datas = super(BlogSearchForm, self).search()
# 验证表单数据
if not self.is_valid():
return self.no_query_found() # 如果表单无效,返回无查询结果
return self.no_query_found()
# 如果查询数据存在,记录查询日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas # 返回搜索结果
return datas

@ -6,39 +6,13 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
# TODO 参数化
class Command(BaseCommand):
"""
<<<<<<< HEAD
Django管理命令用于构建Elasticsearch搜索索引
"""
help = 'build search index'
def handle(self, *args, **options):
"""
执行命令时的处理逻辑
如果启用了Elasticsearch则构建性能和文章的索引
"""
=======
Django管理命令用于构建搜索引擎索引
"""
help = 'build search index' # 命令帮助信息
def handle(self, *args, **options):
"""
命令处理函数执行索引构建操作
"""
# 检查是否启用了Elasticsearch
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
if ELASTICSEARCH_ENABLED:
# 构建耗时文档索引
ElaspedTimeDocumentManager.build_index()
# 初始化耗时文档
manager = ElapsedTimeDocument()
manager.init()
# 重新构建文章文档索引
manager = ArticleDocumentManager()
manager.delete_index() # 删除现有索引
manager.rebuild() # 重新构建索引
else:
# 如果未启用Elasticsearch可以添加提示信息或处理逻辑
pass
manager.delete_index()
manager.rebuild()

@ -5,19 +5,9 @@ from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
"""
Django管理命令用于构建搜索词列表
"""
help = 'build search words' # 命令帮助信息
help = 'build search words'
def handle(self, *args, **options):
"""
命令处理函数生成标签和分类名称的搜索词列表
"""
# 收集所有标签和分类的名称使用set去重
datas = set([t.name for t in Tag.objects.all()] + # 获取所有标签名称
[t.name for t in Category.objects.all()]) # 获取所有分类名称
# 将搜索词列表按行打印输出
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))

@ -4,17 +4,8 @@ from djangoblog.utils import cache
class Command(BaseCommand):
"""
Django管理命令用于清除整个缓存
"""
help = 'clear the whole cache' # 命令帮助信息
help = 'clear the whole cache'
def handle(self, *args, **options):
"""
命令处理函数执行缓存清除操作
"""
cache.clear() # 清除所有缓存
# 输出成功信息到控制台
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -2,163 +2,52 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category, BlogSettings
from blog.models import Article, Tag, Category
class Command(BaseCommand):
"""
Django管理命令用于创建测试数据
"""
help = 'create test datas' # 命令帮助信息
help = 'create test datas'
def handle(self, *args, **options):
<<<<<<< HEAD
"""
命令处理函数创建测试用的用户分类标签和文章数据
"""
# 创建或获取测试用户
user = get_user_model().objects.get_or_create(
email='test@test.com',
username='测试用户',
password=make_password('test!q@w#eTYU'))[0]
# 先尝试通过用户名查找用户
try:
user = get_user_model().objects.get(username='测试用户')
except get_user_model().DoesNotExist:
# 如果用户不存在,则创建新用户
user = get_user_model().objects.create(
email='test@test.com',
username='测试用户',
password=make_password('test!q@w#eTYU')
)
# 创建父分类
pcategory = Category.objects.get_or_create(
name='我是父类目',
parent_category=None)[0]
name='我是父类目', parent_category=None)[0]
# 创建子分类
category = Category.objects.get_or_create(
name='子类目',
parent_category=pcategory)[0]
name='子类目', parent_category=pcategory)[0]
category.save()
# 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 创建20篇文章及其对应的标签
# 使用 get_or_create 处理基础标签
basetag, created = Tag.objects.get_or_create(name="标签")
for i in range(1, 20):
# 创建文章
article = Article.objects.get_or_create(
article, article_created = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# 为每篇文章创建专属标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 将标签关联到文章
article.tags.add(tag)
article.tags.add(basetag)
=======
# Ensure BlogSettings reflect movie discussion theme
setting = BlogSettings.objects.first()
if not setting:
setting = BlogSettings()
setting.site_name = '电影交流'
setting.site_description = '一个专注电影资讯、影评、演员导演与类型讨论的社区'
setting.site_seo_description = '电影交流社区,分享最新电影资讯、深度影评、演员与导演作品讨论,涵盖科幻、动作、剧情、喜剧等类型'
setting.site_keywords = '电影, 影评, 电影资讯, 演员, 导演, 科幻, 动作, 剧情, 喜剧'
setting.save()
# Make test user idempotent (avoid unique constraint on username)
User = get_user_model()
user, _ = User.objects.get_or_create(username='测试用户', defaults={
'email': 'test@test.com',
'password': make_password('test!q@w#eTYU')
})
# Update email/password if needed (in case user already existed with different email)
changed = False
if not user.email:
user.email = 'test@test.com'
changed = True
# ensure password is set to known value for demo
if not user.has_usable_password():
user.password = make_password('test!q@w#eTYU')
changed = True
if changed:
user.save()
# Movie-themed categories
pcategory = Category.objects.get_or_create(
name='电影', parent_category=None)[0]
if not pcategory.slug or pcategory.slug == 'no-slug':
pcategory.slug = 'dianying'
pcategory.save()
category = Category.objects.get_or_create(
name='影评', parent_category=pcategory)[0]
if not category.slug or category.slug == 'no-slug':
category.slug = 'yingping'
category.save()
category.save()
# Base tags for movie domain
base_tags = ['科幻', '动作', '剧情', '喜剧', '悬疑', '动画', '纪录片']
basetag_objects = []
slug_map = {
'科幻': 'kehuan',
'动作': 'dongzuo',
'剧情': 'juqing',
'喜剧': 'xiju',
'悬疑': 'xuanyi',
'动画': 'donghua',
'纪录片': 'jilupian',
}
for t in base_tags:
tag_obj = Tag.objects.get_or_create(name=t)[0]
if not tag_obj.slug or tag_obj.slug == 'no-slug':
tag_obj.slug = slug_map.get(t, 'tag')
tag_obj.save()
basetag_objects.append(tag_obj)
movie_samples = [
('星际穿越', '一次关于爱与时空的伟大旅程,诺兰的硬核科幻经典。'),
('盗梦空间', '层层梦境之间的较量,现实与幻觉的边界。'),
('蝙蝠侠:黑暗骑士', '混沌与秩序的对决,小丑与蝙蝠侠的哲学博弈。'),
('阿甘正传', '傻人有傻福,一部跨越时代的人生史诗。'),
('寄生虫', '阶层冲突下的人性与荒诞,奥斯卡最佳影片。'),
('千与千寻', '成长与记忆的奇幻旅程,宫崎骏的温柔。'),
('霸王别姬', '半世纪浮沉与爱恨,戏里戏外都是人生。'),
('这个杀手不太冷', '冷酷与温情的相遇,一朵花和一个女孩。'),
('当幸福来敲门', '逆境中不放弃的父爱与坚持。'),
('少年的你', '校园与成长的伤痛与守护。'),
('航站情缘', '在机场“漂泊”的温柔喜剧。'),
('海上钢琴师', '一生在海上的天才与自由的代价。'),
('美丽人生', '荒诞中的爱与幽默,生命的顽强。'),
('摔跤吧!爸爸', '不服输的父亲与女儿,梦想与性别刻板印象。'),
('你的名字。', '命运与时间的交错,青春与思念。'),
('流浪地球', '家园与牺牲的宏大叙事,中国科幻新里程。'),
('让子弹飞', '黑色幽默与权力游戏,姜文的快意江湖。'),
('釜山行', '人性与灾难中的生存与选择。'),
('无间道', '身份与信念的撕裂与救赎。')
]
for title, summary in movie_samples:
article = Article.objects.get_or_create(
category=category,
title=title,
defaults={
'body': summary,
'author': user,
}
)[0]
# attach 2-3 random base tags
from random import sample
for tag in sample(basetag_objects, k=min(3, len(basetag_objects))):
author=user
)
# 使用 get_or_create 处理每个标签
tag, tag_created = Tag.objects.get_or_create(name="标签" + str(i))
# 只有在文章是新创建的时候才添加标签
if article_created:
article.tags.add(tag)
>>>>>>> 9a6c41e696594d7c9488468755f726a36b5d45bb
article.save()
article.tags.add(basetag)
article.save()
# 清除缓存
from djangoblog.utils import cache
cache.clear()
# 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -4,73 +4,47 @@ from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain # 获取当前站点域名
site = get_current_site().domain
class Command(BaseCommand):
"""
Django管理命令用于向百度搜索引擎提交URL进行收录通知
"""
help = 'notify baidu url' # 命令帮助信息
help = 'notify baidu url'
def add_arguments(self, parser):
"""
添加命令行参数
"""
parser.add_argument(
'data_type', # 参数名称
type=str, # 参数类型
choices=[ # 参数可选值
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these') # 参数帮助信息
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
"""
根据相对路径生成完整URL
Args:
path: 相对路径
Returns:
完整的HTTPS URL
"""
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
"""
命令处理函数执行百度URL提交操作
"""
type = options['data_type'] # 获取数据类型参数
self.stdout.write('start get %s' % type) # 输出开始信息
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = [] # 存储需要提交的URL列表
# 根据数据类型收集相应的URL
urls = []
if type == 'article' or type == 'all':
# 收集所有已发布文章的URL
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
# 收集所有标签页面的URL
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
# 收集所有分类页面的URL
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# 输出准备提交的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' % len(urls)))
# 向百度提交URL
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
# 输出完成信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -8,68 +8,40 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
"""
Django管理命令用于同步用户头像
"""
help = 'sync user avatar' # 命令帮助信息
help = 'sync user avatar'
def test_picture(self, url):
"""
测试图片URL是否有效
Args:
url: 图片URL
Returns:
bool: URL是否有效
"""
try:
# 发送GET请求测试图片URL超时时间为2秒
if requests.get(url, timeout=2).status_code == 200:
return True
except:
# 出现异常说明URL无效
pass
def handle(self, *args, **options):
"""
命令处理函数执行用户头像同步操作
"""
static_url = static("../") # 获取静态文件基础URL
users = OAuthUser.objects.all() # 获取所有OAuth用户
self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出开始信息
# 遍历所有用户进行头像同步
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户
url = u.picture # 获取用户当前头像URL
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
# 如果URL以静态URL开头说明是本地头像
if url.startswith(static_url):
# 测试图片是否有效
if self.test_picture(url):
continue # 有效则跳过
continue
else:
# 无效则尝试从元数据重新获取
if u.metadata:
manage = get_manager_by_type(u.type) # 获取对应的OAuth管理器
url = manage.get_picture(u.metadata) # 从元数据获取新图片URL
url = save_user_avatar(url) # 保存用户头像
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
# 没有元数据则使用默认头像
url = static('blog/img/avatar.png')
else:
# 非本地头像则保存新头像
url = save_user_avatar(url)
else:
# 没有头像URL则使用默认头像
url = static('blog/img/avatar.png')
# 如果获取到有效的头像URL则更新用户头像
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}') # 输出同步结果
u.picture = url # 更新用户头像URL
u.save() # 保存到数据库
self.stdout.write('结束同步') # 输出结束信息
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -6,66 +6,37 @@ from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__) # 创建日志记录器
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""
在线中间件类用于记录页面加载时间和用户访问信息
"""
def __init__(self, get_response=None):
"""
初始化中间件
Args:
get_response: Django的响应获取函数
"""
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
中间件调用方法处理请求和响应
Args:
request: HTTP请求对象
Returns:
HTTP响应对象
"""
''' page render time '''
start_time = time.time() # 记录请求开始时间
response = self.get_response(request) # 获取响应
# 获取用户IP地址
ip, _ = get_client_ip(request)
# 解析用户代理信息
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
# 如果响应不是流式响应,则处理性能数据
if not response.streaming:
try:
# 计算页面渲染耗时
cast_time = time.time() - start_time
# 如果启用了Elasticsearch则记录性能数据
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) # 转换为毫秒
url = request.path # 获取请求路径
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
# 创建并保存性能文档
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# 在响应内容中替换加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
# 记录错误日志
logger.error("Error OnlineMiddleware: %s" % e)
return response # 返回响应
return response

@ -8,17 +8,14 @@ import mdeditor.fields
class Migration(migrations.Migration):
# 标记这是一个初始迁移文件
initial = True
# 定义依赖关系
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义具体的操作
operations = [
# 创建BlogSettings模型网站配置
migrations.CreateModel(
name='BlogSettings',
fields=[
@ -44,8 +41,6 @@ class Migration(migrations.Migration):
'verbose_name_plural': '网站配置',
},
),
# 创建Links模型友情链接
migrations.CreateModel(
name='Links',
fields=[
@ -64,8 +59,6 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# 创建SideBar模型侧边栏
migrations.CreateModel(
name='SideBar',
fields=[
@ -83,8 +76,6 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# 创建Tag模型标签
migrations.CreateModel(
name='Tag',
fields=[
@ -100,8 +91,6 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
# 创建Category模型分类
migrations.CreateModel(
name='Category',
fields=[
@ -119,8 +108,6 @@ class Migration(migrations.Migration):
'ordering': ['-index'],
},
),
# 创建Article模型文章
migrations.CreateModel(
name='Article',
fields=[
@ -128,7 +115,7 @@ class Migration(migrations.Migration):
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')), # 使用Markdown编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
@ -143,9 +130,8 @@ class Migration(migrations.Migration):
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'], # 按排序和发布时间降序排列
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -4,24 +4,20 @@ from django.db import migrations, models
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系依赖于blog应用的0001_initial迁移
dependencies = [
('blog', '0001_initial'),
]
# 定义具体的迁移操作
operations = [
# 为BlogSettings模型添加global_footer字段
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# 为BlogSettings模型添加global_header字段
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -4,18 +4,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# 定义具体的迁移操作
operations = [
# 为BlogSettings模型添加comment_need_review字段
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -4,30 +4,24 @@ from django.db import migrations
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# 定义具体的迁移操作
operations = [
# 将BlogSettings模型中的analyticscode字段重命名为analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
# 将BlogSettings模型中的beiancode字段重命名为beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
# 将BlogSettings模型中的sitename字段重命名为site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -8,357 +8,293 @@ import mdeditor.fields
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
# 定义具体的迁移操作
operations = [
# 修改Article模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# 修改Category模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# 修改Links模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# 修改SideBar模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# 修改Tag模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 移除Article模型中的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
# 移除Article模型中的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# 移除Category模型中的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
# 移除Category模型中的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# 移除Links模型中的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# 移除SideBar模型中的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# 移除Tag模型中的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
# 移除Tag模型中的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# 添加Article模型中的creation_time字段
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加Article模型中的last_modify_time字段
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 添加Category模型中的creation_time字段
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加Category模型中的last_modify_time字段
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 添加Links模型中的creation_time字段
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加SideBar模型中的creation_time字段
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加Tag模型中的creation_time字段
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 添加Tag模型中的last_modify_time字段
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型中的article_order字段显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
# 修改Article模型中的author字段显示名称
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
# 修改Article模型中的body字段显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
# 修改Article模型中的category字段显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
# 修改Article模型中的comment_status字段显示名称和选项
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
# 修改Article模型中的pub_time字段显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
# 修改Article模型中的show_toc字段显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
# 修改Article模型中的status字段显示名称和选项
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
# 修改Article模型中的tags字段显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
# 修改Article模型中的title字段显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
# 修改Article模型中的type字段显示名称和选项
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
# 修改Article模型中的views字段显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型中的article_comment_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
# 修改BlogSettings模型中的article_sub_length字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
# 修改BlogSettings模型中的google_adsense_codes字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
# 修改BlogSettings模型中的open_site_comment字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
# 修改BlogSettings模型中的show_google_adsense字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
# 修改BlogSettings模型中的sidebar_article_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
# 修改BlogSettings模型中的sidebar_comment_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
# 修改BlogSettings模型中的site_description字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
# 修改BlogSettings模型中的site_keywords字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
# 修改BlogSettings模型中的site_name字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
# 修改BlogSettings模型中的site_seo_description字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型中的index字段显示名称
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
# 修改Category模型中的name字段显示名称
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
# 修改Category模型中的parent_category字段显示名称
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型中的is_enable字段显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
# 修改Links模型中的last_mod_time字段显示名称
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Links模型中的link字段显示名称
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
# 修改Links模型中的name字段显示名称
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
# 修改Links模型中的sequence字段显示名称
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Links模型中的show_type字段显示名称和选项
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# 修改SideBar模型中的content字段显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
# 修改SideBar模型中的is_enable字段显示名称
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
# 修改SideBar模型中的last_mod_time字段显示名称
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改SideBar模型中的name字段显示名称
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
# 修改SideBar模型中的sequence字段显示名称
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型中的name字段显示名称
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -4,17 +4,14 @@ from django.db import migrations
class Migration(migrations.Migration):
# 定义该迁移文件的依赖关系依赖于blog应用的0005_alter_article_options_alter_category_options_and_more迁移
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# 定义具体的迁移操作
operations = [
# 修改BlogSettings模型的选项配置将显示名称改为英文
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -0,0 +1,33 @@
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0006_alter_blogsettings_options'),
]
operations = [
migrations.AddField(
model_name='article',
name='like_count',
field=models.PositiveIntegerField(default=0, verbose_name='likes'),
),
migrations.AddField(
model_name='article',
name='favorite_count',
field=models.PositiveIntegerField(default=0, verbose_name='favorites'),
),
migrations.AddField(
model_name='article',
name='like_users',
field=models.ManyToManyField(blank=True, related_name='liked_articles', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='article',
name='favorite_users',
field=models.ManyToManyField(blank=True, related_name='favorite_articles', to=settings.AUTH_USER_MODEL),
),
]

@ -14,41 +14,29 @@ from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__) # 创建日志记录器
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""
链接显示类型枚举类
"""
I = ('i', _('index')) # 首页
L = ('l', _('list')) # 列表页
P = ('p', _('post')) # 文章页面
A = ('a', _('all')) # 全站
S = ('s', _('slide')) # 幻灯片
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model):
"""
基础模型类提供通用字段和方法
"""
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
"""
重写保存方法处理slug生成和视图计数更新
"""
# 判断是否是仅更新浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 如果是更新浏览量,直接更新数据库避免触发其他逻辑
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 如果有slug字段自动生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
@ -57,42 +45,35 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
"""
获取完整的URL地址
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True # 声明为抽象类
abstract = True
@abstractmethod
def get_absolute_url(self):
"""
抽象方法子类必须实现获取绝对URL的方法
"""
pass
class Article(BaseModel):
"""文章模型"""
"""文章"""
STATUS_CHOICES = (
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = (
('o', _('Open')), # 开启评论
('c', _('Close')), # 关闭评论
('o', _('Open')),
('c', _('Close')),
)
TYPE = (
('a', _('Article')), # 文章
('p', _('Page')), # 页面
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body')) # 使用Markdown编辑器字段
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
status = models.CharField(
@ -107,6 +88,18 @@ class Article(BaseModel):
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
like_count = models.PositiveIntegerField('likes', default=0)
favorite_count = models.PositiveIntegerField('favorites', default=0)
like_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='liked_articles',
blank=True
)
favorite_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='favorite_articles',
blank=True
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
@ -125,24 +118,18 @@ class Article(BaseModel):
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
"""
将文章正文转换为字符串
"""
return self.body
def __str__(self):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] # 按排序和发布时间降序排列
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
"""
获取文章绝对URL
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -152,31 +139,39 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
获取分类树结构
"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""
保存文章
"""
super().save(*args, **kwargs)
def viewed(self):
"""
增加文章浏览量
"""
self.views += 1
self.save(update_fields=['views'])
def like_once_by_user(self, user):
if not user.is_authenticated:
return False
if self.like_users.filter(pk=user.pk).exists():
return False
self.like_users.add(user)
self.like_count += 1
self.save(update_fields=['like_count'])
return True
def favorite_once_by_user(self, user):
if not user.is_authenticated:
return False
if self.favorite_users.filter(pk=user.pk).exists():
return False
self.favorite_users.add(user)
self.favorite_count += 1
self.save(update_fields=['favorite_count'])
return True
def comment_list(self):
"""
获取文章评论列表带缓存
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -189,33 +184,24 @@ class Article(BaseModel):
return comments
def get_admin_url(self):
"""
获取管理后台的文章编辑URL
"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
"""
获取下一篇已发布的文章
"""
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
获取上一篇已发布的文章
"""
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从文章正文中获取第一张图片的URL
:return: 图片URL或空字符串
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
@ -224,7 +210,7 @@ class Article(BaseModel):
class Category(BaseModel):
"""文章分类模型"""
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
@ -236,14 +222,11 @@ class Category(BaseModel):
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
ordering = ['-index'] # 按索引降序排列
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
获取分类绝对URL
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
@ -254,8 +237,8 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级分类树
:return: 分类列表
递归获得分类目录的父级
:return:
"""
categorys = []
@ -270,8 +253,8 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子分类
:return: 子分类列表
获得当前分类目录所有子
:return:
"""
categorys = []
all_categorys = Category.objects.all()
@ -290,7 +273,7 @@ class Category(BaseModel):
class Tag(BaseModel):
"""文章标签模型"""
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
@ -298,26 +281,20 @@ class Tag(BaseModel):
return self.name
def get_absolute_url(self):
"""
获取标签绝对URL
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
获取该标签下的文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name'] # 按名称升序排列
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
"""友情链接模型"""
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
@ -333,7 +310,7 @@ class Links(models.Model):
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # 按顺序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
@ -342,7 +319,7 @@ class Links(models.Model):
class SideBar(models.Model):
"""侧边栏模型,可以展示一些html内容"""
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
@ -351,7 +328,7 @@ class SideBar(models.Model):
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # 按顺序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
@ -360,7 +337,7 @@ class SideBar(models.Model):
class BlogSettings(models.Model):
"""博客配置模型"""
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
@ -422,631 +399,10 @@ class BlogSettings(models.Model):
return self.site_name
def clean(self):
"""
验证模型数据确保只存在一个配置实例
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
保存配置并清除缓存
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""
链接显示类型枚举类
I: 首页显示
L: 列表页显示
P: 文章页显示
A: 所有页面显示
S: 幻灯片显示
"""
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model):
"""
基础模型类
提供所有模型共有的字段和方法
包含主键创建时间和最后修改时间字段
Attributes:
id (AutoField): 主键字段
creation_time (DateTimeField): 创建时间
last_modify_time (DateTimeField): 最后修改时间
"""
id = models.AutoField(
primary_key=True,
help_text='主键ID')
creation_time = models.DateTimeField(
_('creation time'),
default=now,
help_text='记录创建时间')
last_modify_time = models.DateTimeField(
_('modify time'),
default=now,
help_text='记录最后修改时间')
def save(self, *args, **kwargs):
"""
重写保存方法
如果是更新文章浏览量则只更新浏览量字段
否则处理slug字段并调用父类保存方法
"""
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
"""
获取完整URL地址
:return: 完整的URL地址包含域名
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
@abstractmethod
def get_absolute_url(self):
"""
抽象方法子类必须实现
返回模型对象的绝对URL路径
"""
pass
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(
_('title'),
max_length=200,
unique=True,
help_text='文章标题,必须唯一')
body = MDTextField(
_('body'),
help_text='文章正文内容支持Markdown语法')
pub_time = models.DateTimeField(
_('publish time'),
blank=False,
null=False,
default=now,
help_text='文章发布时间')
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p',
help_text='文章状态:草稿或已发布')
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o',
help_text='评论状态:开放或关闭')
type = models.CharField(
_('type'),
max_length=1,
choices=TYPE,
default='a',
help_text='文章类型:普通文章或页面')
views = models.PositiveIntegerField(
_('views'),
default=0,
help_text='文章浏览量')
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE,
help_text='文章作者')
article_order = models.IntegerField(
_('order'),
blank=False,
null=False,
default=0,
help_text='文章排序')
show_toc = models.BooleanField(
_('show toc'),
blank=False,
null=False,
default=False,
help_text='是否显示目录')
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False,
help_text='文章分类')
tags = models.ManyToManyField(
'Tag',
verbose_name=_('tag'),
blank=True,
help_text='文章标签')
def body_to_string(self):
"""
将文章内容转换为字符串
:return: 文章内容字符串
"""
return self.body
def __str__(self):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
"""
获取文章详情页的URL
:return: 文章详情页URL
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
获取分类目录树
:return: 分类目录树列表
"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""
重写保存方法调用父类保存方法
"""
super().save(*args, **kwargs)
def viewed(self):
"""
增加文章浏览量
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
获取文章的评论列表
使用缓存机制提高性能
:return: 评论列表
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""
获取文章在管理后台的编辑URL
:return: 管理后台编辑URL
"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
"""
获取下一篇文章
:return: 下一篇文章对象
"""
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
获取上一篇文章
:return: 上一篇文章对象
"""
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从文章正文中获取第一张图片的URL
使用正则表达式匹配Markdown格式的图片
:return: 第一张图片的URL如果没有找到则返回空字符串
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""文章分类"""
name = models.CharField(
_('category name'),
max_length=30,
unique=True,
help_text='分类名称,必须唯一')
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE,
help_text='父级分类')
slug = models.SlugField(
default='no-slug',
max_length=60,
blank=True,
help_text='分类slug用于生成URL')
index = models.IntegerField(
default=0,
verbose_name=_('index'),
help_text='分类索引,用于排序')
class Meta:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
获取分类详情页URL
:return: 分类详情页URL
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return: 包含当前分类及其所有父级分类的列表
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return: 包含当前分类及其所有子分类的列表
"""
categorys = []
all_categorys = Category.objects.all()
def parse(category):
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(
_('tag name'),
max_length=30,
unique=True,
help_text='标签名称,必须唯一')
slug = models.SlugField(
default='no-slug',
max_length=60,
blank=True,
help_text='标签slug用于生成URL')
def __str__(self):
return self.name
def get_absolute_url(self):
"""
获取标签详情页URL
:return: 标签详情页URL
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
获取使用该标签的文章数量
:return: 文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
"""友情链接"""
name = models.CharField(
_('link name'),
max_length=30,
unique=True,
help_text='链接名称')
link = models.URLField(
_('link'),
help_text='链接地址')
sequence = models.IntegerField(
_('order'),
unique=True,
help_text='链接排序')
is_enable = models.BooleanField(
_('is show'),
default=True,
blank=False,
null=False,
help_text='是否显示')
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I,
help_text='链接显示类型')
creation_time = models.DateTimeField(
_('creation time'),
default=now,
help_text='链接创建时间')
last_mod_time = models.DateTimeField(
_('modify time'),
default=now,
help_text='链接最后修改时间')
class Meta:
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(
_('title'),
max_length=100,
help_text='侧边栏标题')
content = models.TextField(
_('content'),
help_text='侧边栏内容支持HTML')
sequence = models.IntegerField(
_('order'),
unique=True,
help_text='侧边栏排序')
is_enable = models.BooleanField(
_('is enable'),
default=True,
help_text='是否启用')
creation_time = models.DateTimeField(
_('creation time'),
default=now,
help_text='侧边栏创建时间')
last_mod_time = models.DateTimeField(
_('modify time'),
default=now,
help_text='侧边栏最后修改时间')
class Meta:
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='',
help_text='网站名称')
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='',
help_text='网站描述')
site_seo_description = models.TextField(
_('site seo description'),
max_length=1000,
null=False,
blank=False,
default='',
help_text='网站SEO描述')
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='',
help_text='网站关键词')
article_sub_length = models.IntegerField(
_('article sub length'),
default=300,
help_text='文章摘要长度')
sidebar_article_count = models.IntegerField(
_('sidebar article count'),
default=10,
help_text='侧边栏文章显示数量')
sidebar_comment_count = models.IntegerField(
_('sidebar comment count'),
default=5,
help_text='侧边栏评论显示数量')
article_comment_count = models.IntegerField(
_('article comment count'),
default=5,
help_text='文章评论显示数量')
show_google_adsense = models.BooleanField(
_('show adsense'),
default=False,
help_text='是否显示Google广告')
google_adsense_codes = models.TextField(
_('adsense code'),
max_length=2000,
null=True,
blank=True,
default='',
help_text='Google广告代码')
open_site_comment = models.BooleanField(
_('open site comment'),
default=True,
help_text='是否开放站点评论')
global_header = models.TextField(
"公共头部",
null=True,
blank=True,
default='',
help_text='公共头部代码')
global_footer = models.TextField(
"公共尾部",
null=True,
blank=True,
default='',
help_text='公共尾部代码')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='',
help_text='网站备案号')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='',
help_text='网站统计代码,如百度统计')
show_gongan_code = models.BooleanField(
'是否显示公安备案号',
default=False,
null=False,
help_text='是否显示公安备案号')
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='',
help_text='公安备案号')
comment_need_review = models.BooleanField(
'评论是否需要审核',
default=False,
null=False,
help_text='评论是否需要审核')
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
return self.site_name
def clean(self):
"""
验证模型数据
确保只存在一个配置实例
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
重写保存方法
保存后清除缓存
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -4,25 +4,10 @@ from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""
文章搜索索引类用于Haystack全文搜索
"""
# 定义文档字段use_template=True表示使用模板来构建索引内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
返回索引对应的模型类
"""
return Article
def index_queryset(self, using=None):
"""
返回需要建立索引的查询集
Args:
using: 数据库别名
Returns:
已发布文章的查询集
"""
return self.get_model().objects.filter(status='p') # 只对已发布的文章建立索引
return self.get_model().objects.filter(status='p')

@ -0,0 +1,9 @@
.button {
border: none;
padding: 4px 80px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
}

@ -0,0 +1,47 @@
let wait = 60;
function time(o) {
if (wait == 0) {
o.removeAttribute("disabled");
o.value = "获取验证码";
wait = 60
return false
} else {
o.setAttribute("disabled", true);
o.value = "重新发送(" + wait + ")";
wait--;
setTimeout(function () {
time(o)
},
1000)
}
}
document.getElementById("btn").onclick = function () {
let id_email = $("#id_email")
let token = $("*[name='csrfmiddlewaretoken']").val()
let ts = this
let myErr = $("#myErr")
$.ajax(
{
url: "/forget_password_code/",
type: "POST",
data: {
"email": id_email.val(),
"csrfmiddlewaretoken": token
},
success: function (result) {
if (result != "ok") {
myErr.remove()
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
return
}
myErr.remove()
time(ts)
},
error: function (e) {
alert("发送失败,请重试")
}
}
);
}

@ -0,0 +1,58 @@
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #fff;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin-heading {
margin: 0 0 15px;
font-size: 18px;
font-weight: 400;
color: #555;
}
.form-signin .checkbox {
margin-bottom: 10px;
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: 10px;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
}
.card {
width: 304px;
padding: 20px 25px 30px;
margin: 0 auto 25px;
background-color: #f7f7f7;
border-radius: 2px;
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
}
.card-signin {
width: 354px;
padding: 40px;
}
.card-signin .profile-img {
display: block;
width: 96px;
height: 96px;
margin: 0 auto 10px;
}

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 221 B

@ -1,18 +1,13 @@
/*
IEIE9
Styles for older IE versions (previous to IE9).
*/
/* 页面主体背景色 */
body {
background-color: #e6e6e6;
}
/* 自定义背景为空时的样式 */
body.custom-background-empty {
background-color: #fff;
}
/* 自定义背景为空或白色时站点样式 */
body.custom-background-empty .site,
body.custom-background-white .site {
box-shadow: none;
@ -20,20 +15,14 @@ body.custom-background-white .site {
margin-top: 0;
padding: 0;
}
/* 辅助文本和屏幕阅读器文本 */
.assistive-text,
.site .screen-reader-text {
clip: rect(1px 1px 1px 1px);
}
/* 全宽页面内容 */
.full-width .site-content {
float: none;
width: 100%;
}
/* 防止IE8中具有高度和宽度属性的完整尺寸和大尺寸图像被拉伸 */
img.size-full,
img.size-large,
img.header-image,
@ -41,23 +30,17 @@ img.wp-post-image,
img[class*="align"],
img[class*="wp-image-"],
img[class*="attachment-"] {
width: auto; /* 防止在IE8中拉伸完整尺寸和大尺寸图像 */
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
}
/* 作者头像 */
.author-avatar {
float: left;
margin-top: 8px;
margin-top: 0.571428571rem;
}
/* 作者描述 */
.author-description {
float: right;
width: 80%;
}
/* 站点容器 */
.site {
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
margin: 48px auto;
@ -65,39 +48,27 @@ img[class*="attachment-"] {
overflow: hidden;
padding: 0 40px;
}
/* 站点内容 */
.site-content {
float: left;
width: 65.104166667%;
}
/* 不同页面模板的站点内容宽度 */
body.template-front-page .site-content,
body.attachment .site-content,
body.full-width .site-content {
width: 100%;
}
/* 小工具区域 */
.widget-area {
float: right;
width: 26.041666667%;
}
/* 站点头部标题 */
.site-header h1,
.site-header h2 {
text-align: left;
}
/* 站点头部一级标题 */
.site-header h1 {
font-size: 26px;
line-height: 1.846153846;
}
/* 主导航菜单 */
.main-navigation ul.nav-menu,
.main-navigation div.nav-menu > ul {
border-bottom: 1px solid #ededed;
@ -106,46 +77,32 @@ body.full-width .site-content {
text-align: left;
width: 100%;
}
/* 主导航菜单列表 */
.main-navigation ul {
margin: 0;
text-indent: 0;
}
/* 主导航菜单项和链接 */
.main-navigation li a,
.main-navigation li {
display: inline-block;
text-decoration: none;
}
/* IE7浏览器特殊处理 */
.ie7 .main-navigation li a,
.ie7 .main-navigation li {
display: inline;
}
/* 主导航菜单链接 */
.main-navigation li a {
border-bottom: 0;
color: #6a6a6a;
line-height: 3.692307692;
text-transform: uppercase;
}
/* 主导航菜单链接悬停效果 */
.main-navigation li a:hover {
color: #000;
}
/* 主导航菜单项 */
.main-navigation li {
margin: 0 40px 0 0;
position: relative;
}
/* 主导航菜单下拉列表 */
.main-navigation li ul {
margin: 0;
padding: 0;
@ -157,23 +114,17 @@ body.full-width .site-content {
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
/* IE7浏览器下拉列表特殊处理 */
.ie7 .main-navigation li ul {
clip: inherit;
display: none;
left: 0;
overflow: visible;
}
/* 多级下拉菜单 */
.main-navigation li ul ul,
.ie7 .main-navigation li ul ul {
top: 0;
left: 100%;
}
/* 下拉菜单显示 */
.main-navigation ul li:hover > ul,
.main-navigation ul li:focus > ul,
.main-navigation .focus > ul {
@ -183,14 +134,10 @@ body.full-width .site-content {
height: inherit;
width: inherit;
}
/* IE7浏览器下拉菜单显示特殊处理 */
.ie7 .main-navigation ul li:hover > ul,
.ie7 .main-navigation ul li:focus > ul {
display: block;
}
/* 下拉菜单项链接 */
.main-navigation li ul li a {
background: #efefef;
border-bottom: 1px solid #ededed;
@ -200,14 +147,10 @@ body.full-width .site-content {
padding: 8px 10px;
width: 180px;
}
/* 下拉菜单项链接悬停效果 */
.main-navigation li ul li a:hover {
background: #e3e3e3;
color: #444;
}
/* 当前菜单项样式 */
.main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a,
.main-navigation .current_page_item > a,
@ -215,58 +158,39 @@ body.full-width .site-content {
color: #636363;
font-weight: bold;
}
/* 菜单切换按钮 */
.main-navigation .menu-toggle {
display: none;
}
/* 文章标题 */
.entry-header .entry-title {
font-size: 22px;
}
/* 评论表单输入框 */
#respond form input[type="text"] {
width: 46.333333333%;
}
/* 评论表单文本域 */
#respond form textarea.blog-textarea {
width: 79.666666667%;
}
/* 首页模板内容 */
.template-front-page .site-content,
.template-front-page article {
overflow: hidden;
}
/* 首页模板带缩略图文章 */
.template-front-page.has-post-thumbnail article {
float: left;
width: 47.916666667%;
}
/* 页面图像 */
.entry-page-image {
float: right;
margin-bottom: 0;
width: 47.916666667%;
}
/* IE首页模板小工具修复 */
/* IE Front Page Template Widget fix */
.template-front-page .widget-area {
clear: both;
}
/* 首页模板小工具 */
.template-front-page .widget {
width: 100% !important;
border: none;
}
/* 首页模板小工具区域 */
.template-front-page .widget-area .widget,
.template-front-page .first.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets {
@ -274,14 +198,10 @@ body.full-width .site-content {
margin-bottom: 24px;
width: 51.875%;
}
/* 首页模板第二组小工具 */
.template-front-page .second.front-widgets,
.template-front-page .widget-area .widget:nth-child(odd) {
clear: right;
}
/* 首页模板第一组和第二组小工具 */
.template-front-page .first.front-widgets,
.template-front-page .second.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
@ -289,88 +209,64 @@ body.full-width .site-content {
margin: 0 0 24px;
width: 39.0625%;
}
/* 双侧边栏模板小工具 */
.template-front-page.two-sidebars .widget,
.template-front-page.two-sidebars .widget:nth-child(even) {
float: none;
width: auto;
}
/* 为IE9之前的密码框添加字体以使圆点显示 */
/* add input font for <IE9 Password Box to make the bullets show up */
input[type="password"] {
font-family: Helvetica, Arial, sans-serif;
}
/* IE7IE8RTL
/* RTL overrides for IE7 and IE8
-------------------------------------------------------------- */
/* RTL布局站点标题 */
.rtl .site-header h1,
.rtl .site-header h2 {
text-align: right;
}
/* RTL布局小工具区域和作者描述 */
.rtl .widget-area,
.rtl .author-description {
float: left;
}
/* RTL布局作者头像和站点内容 */
.rtl .author-avatar,
.rtl .site-content {
float: right;
}
/* RTL布局导航菜单 */
.rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul {
text-align: right;
}
/* RTL布局多级导航菜单项 */
.rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li {
margin-left: 40px;
margin-right: auto;
}
/* RTL布局三级导航菜单 */
.rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* IE7浏览器RTL布局三级导航菜单特殊处理 */
.ie7 .rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* IE7浏览器RTL布局导航菜单项 */
.ie7 .rtl .main-navigation ul li {
z-index: 99;
}
/* IE7浏览器RTL布局下拉菜单 */
.ie7 .rtl .main-navigation li ul {
position: absolute;
bottom: 100%;
right: 0;
z-index: 1;
}
/* IE7浏览器RTL布局导航菜单项外边距 */
.ie7 .rtl .main-navigation li {
margin-right: auto;
margin-left: 40px;
}
/* IE7浏览器RTL布局四级导航菜单 */
.ie7 .rtl .main-navigation li ul ul ul {
position: relative;
z-index: 1;

@ -1,11 +1,10 @@
/* 使点击事件穿透 */
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
/* 进度条样式 */
#nprogress .bar {
background: red; /* 进度条颜色 */
background: red;
position: fixed;
z-index: 1031;
@ -16,7 +15,7 @@
height: 2px;
}
/* 花哨的模糊效果 */
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
@ -31,7 +30,7 @@
transform: rotate(3deg) translate(0px, -4px);
}
/* 移除这些来去掉旋转器 */
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
@ -40,7 +39,6 @@
right: 15px;
}
/* 旋转器图标样式 */
#nprogress .spinner-icon {
width: 18px;
height: 18px;
@ -55,26 +53,22 @@
animation: nprogress-spinner 400ms linear infinite;
}
/* 自定义父容器样式 */
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
/* 自定义父容器中的旋转器和进度条样式 */
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
/* Webkit内核浏览器的旋转器动画 */
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
/* 标准旋转器动画 */
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}

@ -1,212 +1,173 @@
/* Google图标样式 */
.icon-sn-google {
background-position: 0 -28px;
}
/* Google背景图标样式 */
.icon-sn-bg-google {
background-color: #4285f4;
background-position: 0 0;
}
/* Google字体图标颜色 */
.fa-sn-google {
color: #4285f4;
}
/* GitHub图标样式 */
.icon-sn-github {
background-position: -28px -28px;
}
/* GitHub背景图标样式 */
.icon-sn-bg-github {
background-color: #333;
background-position: -28px 0;
}
/* GitHub字体图标颜色 */
.fa-sn-github {
color: #333;
}
/* 微博图标样式 */
.icon-sn-weibo {
background-position: -56px -28px;
}
/* 微博背景图标样式 */
.icon-sn-bg-weibo {
background-color: #e90d24;
background-position: -56px 0;
}
/* 微博字体图标颜色 */
.fa-sn-weibo {
color: #e90d24;
}
/* QQ图标样式 */
.icon-sn-qq {
background-position: -84px -28px;
}
/* QQ背景图标样式 */
.icon-sn-bg-qq {
background-color: #0098e6;
background-position: -84px 0;
}
/* QQ字体图标颜色 */
.fa-sn-qq {
color: #0098e6;
}
/* Twitter图标样式 */
.icon-sn-twitter {
background-position: -112px -28px;
}
/* Twitter背景图标样式 */
.icon-sn-bg-twitter {
background-color: #50abf1;
background-position: -112px 0;
}
/* Twitter字体图标颜色 */
.fa-sn-twitter {
color: #50abf1;
}
/* Facebook图标样式 */
.icon-sn-facebook {
background-position: -140px -28px;
}
/* Facebook背景图标样式 */
.icon-sn-bg-facebook {
background-color: #4862a3;
background-position: -140px 0;
}
/* Facebook字体图标颜色 */
.fa-sn-facebook {
color: #4862a3;
}
/* 人人网图标样式 */
.icon-sn-renren {
background-position: -168px -28px;
}
/* 人人网背景图标样式 */
.icon-sn-bg-renren {
background-color: #197bc8;
background-position: -168px 0;
}
/* 人人网字体图标颜色 */
.fa-sn-renren {
color: #197bc8;
}
/* 腾讯微博图标样式 */
.icon-sn-tqq {
background-position: -196px -28px;
}
/* 腾讯微博背景图标样式 */
.icon-sn-bg-tqq {
background-color: #1f9ed2;
background-position: -196px 0;
}
/* 腾讯微博字体图标颜色 */
.fa-sn-tqq {
color: #1f9ed2;
}
/* 豆瓣图标样式 */
.icon-sn-douban {
background-position: -224px -28px;
}
/* 豆瓣背景图标样式 */
.icon-sn-bg-douban {
background-color: #279738;
background-position: -224px 0;
}
/* 豆瓣字体图标颜色 */
.fa-sn-douban {
color: #279738;
}
/* 微信图标样式 */
.icon-sn-weixin {
background-position: -252px -28px;
}
/* 微信背景图标样式 */
.icon-sn-bg-weixin {
background-color: #00b500;
background-position: -252px 0;
}
/* 微信字体图标颜色 */
.fa-sn-weixin {
color: #00b500;
}
/* 虚线图标样式 */
.icon-sn-dotted {
background-position: -280px -28px;
}
/* 虚线背景图标样式 */
.icon-sn-bg-dotted {
background-color: #eee;
background-position: -280px 0;
}
/* 虚线字体图标颜色 */
.fa-sn-dotted {
color: #eee;
}
/* 网站图标样式 */
.icon-sn-site {
background-position: -308px -28px;
}
/* 网站背景图标样式 */
.icon-sn-bg-site {
background-color: #00b500;
background-position: -308px 0;
}
/* 网站字体图标颜色 */
.fa-sn-site {
color: #00b500;
}
/* LinkedIn图标样式 */
.icon-sn-linkedin {
background-position: -336px -28px;
}
/* LinkedIn背景图标样式 */
.icon-sn-bg-linkedin {
background-color: #0077b9;
background-position: -336px 0;
}
/* LinkedIn字体图标颜色 */
.fa-sn-linkedin {
color: #0077b9;
}
/* 社交网络图标通用样式 */
[class*=icon-sn-] {
display: inline-block;
background-image: url('../img/icon-sn.svg');
@ -217,159 +178,128 @@
background-size: auto 56px;
}
/* 社交网络图标悬停效果 */
[class*=icon-sn-]:hover {
opacity: .8;
filter: alpha(opacity=80);
}
/* Google按钮样式 */
.btn-sn-google {
background: #4285f4;
}
/* Google按钮激活、聚焦、悬停样式 */
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3;
}
/* GitHub按钮样式 */
.btn-sn-github {
background: #333;
}
/* GitHub按钮激活、聚焦、悬停样式 */
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
background: #262626;
}
/* 微博按钮样式 */
.btn-sn-weibo {
background: #e90d24;
}
/* 微博按钮激活、聚焦、悬停样式 */
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
background: #d10c20;
}
/* QQ按钮样式 */
.btn-sn-qq {
background: #0098e6;
}
/* QQ按钮激活、聚焦、悬停样式 */
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd;
}
/* Twitter按钮样式 */
.btn-sn-twitter {
background: #50abf1;
}
/* Twitter按钮激活、聚焦、悬停样式 */
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef;
}
/* Facebook按钮样式 */
.btn-sn-facebook {
background: #4862a3;
}
/* Facebook按钮激活、聚焦、悬停样式 */
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
background: #405791;
}
/* 人人网按钮样式 */
.btn-sn-renren {
background: #197bc8;
}
/* 人人网按钮激活、聚焦、悬停样式 */
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
background: #166db1;
}
/* 腾讯微博按钮样式 */
.btn-sn-tqq {
background: #1f9ed2;
}
/* 腾讯微博按钮激活、聚焦、悬停样式 */
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
background: #1c8dbc;
}
/* 豆瓣按钮样式 */
.btn-sn-douban {
background: #279738;
}
/* 豆瓣按钮激活、聚焦、悬停样式 */
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
background: #228330;
}
/* 微信按钮样式 */
.btn-sn-weixin {
background: #00b500;
}
/* 微信按钮激活、聚焦、悬停样式 */
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
background: #009c00;
}
/* 虚线按钮样式 */
.btn-sn-dotted {
background: #eee;
}
/* 虚线按钮激活、聚焦、悬停样式 */
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
background: #e1e1e1;
}
/* 网站按钮样式 */
.btn-sn-site {
background: #00b500;
}
/* 网站按钮激活、聚焦、悬停样式 */
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
background: #009c00;
}
/* LinkedIn按钮样式 */
.btn-sn-linkedin {
background: #0077b9;
}
/* LinkedIn按钮激活、聚焦、悬停样式 */
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
background: #0067a0;
}
/* 社交网络按钮通用样式 */
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
border: none;
color: #fff;
}
/* 更多按钮样式 */
.btn-sn-more {
padding: 0;
}
/* 更多按钮及激活、悬停样式 */
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
box-shadow: none;
}
/* 社交网络按钮内的图标样式 */
[class*=btn-sn-] [class*=icon-sn-] {
background-color: transparent;
}

@ -0,0 +1,600 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@ -1,62 +1,40 @@
/**
* 博客前端交互功能
* Created by liangliang on 2016/11/20.
*/
/**
* 回复评论功能
* @param {string} parentid - 父评论ID
*/
function do_reply(parentid) {
console.log(parentid);
// 设置父评论ID
$("#id_parent_comment_id").val(parentid)
// 将评论表单移动到对应评论下
$("#commentform").appendTo($("#div-comment-" + parentid));
// 隐藏回复标题,显示取消回复按钮
$("#reply-title").hide();
$("#cancel_comment").show();
}
/**
* 取消回复功能
*/
function cancel_reply() {
// 显示回复标题,隐藏取消回复按钮
$("#reply-title").show();
$("#cancel_comment").hide();
// 清空父评论ID
$("#id_parent_comment_id").val('')
// 将评论表单移回原位置
$("#commentform").appendTo($("#respond"));
}
// 页面加载进度条控制
NProgress.start();
NProgress.set(0.4);
// 增量更新进度条
//Increment
var interval = setInterval(function () {
NProgress.inc();
}, 1000);
// 文档加载完成后停止进度条
$(document).ready(function () {
NProgress.done();
clearInterval(interval);
});
/** 侧边栏回到顶部功能 */
/** 侧边栏回到顶部 */
var rocket = $('#rocket');
// 监听窗口滚动事件,防抖处理
$(window).on('scroll', debounce(slideTopSet, 300));
/**
* 防抖函数
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 等待时间毫秒
* @returns {Function} 防抖后的函数
*/
function debounce(func, wait) {
var timeout;
return function () {
@ -65,54 +43,45 @@ function debounce(func, wait) {
};
}
/**
* 设置回到顶部按钮显示状态
*/
function slideTopSet() {
var top = $(document).scrollTop();
if (top > 200) {
rocket.addClass('show'); // 滚动超过200px时显示回到顶部按钮
rocket.addClass('show');
} else {
rocket.removeClass('show'); // 否则隐藏
rocket.removeClass('show');
}
}
// 点击回到顶部按钮事件
$(document).on('click', '#rocket', function (event) {
rocket.addClass('move'); // 添加动画类
// 页面平滑滚动到顶部
rocket.addClass('move');
$('body, html').animate({
scrollTop: 0
}, 800);
});
// 动画结束事件处理
$(document).on('animationEnd', function () {
setTimeout(function () {
rocket.removeClass('move'); // 移除动画类
rocket.removeClass('move');
}, 400);
});
// Webkit内核浏览器动画结束事件处理
});
$(document).on('webkitAnimationEnd', function () {
setTimeout(function () {
rocket.removeClass('move'); // 移除动画类
rocket.removeClass('move');
}, 400);
});
// 页面加载完成后初始化评论回复功能
window.onload = function () {
var replyLinks = document.querySelectorAll(".comment-reply-link");
for (var i = 0; i < replyLinks.length; i++) {
replyLinks[i].onclick = function () {
var pk = this.getAttribute("data-pk");
do_reply(pk); // 点击回复链接时调用回复功能
do_reply(pk);
};
}
};
// 国际化语言切换功能(已注释)
// $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');

@ -0,0 +1,142 @@
/**
* MathJax 智能加载器
* 检测页面是否包含数学公式如果有则动态加载和配置MathJax
*/
(function() {
'use strict';
/**
* 检测页面是否包含数学公式
* @returns {boolean} 是否包含数学公式
*/
function hasMathFormulas() {
const content = document.body.textContent || document.body.innerText || '';
// 检测常见的数学公式语法
return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content);
}
/**
* 配置MathJax
*/
function configureMathJax() {
window.MathJax = {
tex: {
// 行内公式和块级公式分隔符
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
// 处理转义字符和LaTeX环境
processEscapes: true,
processEnvironments: true,
// 自动换行
tags: 'ams'
},
options: {
// 跳过这些HTML标签避免处理代码块等
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
// CSS类控制
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
},
// 启动配置
startup: {
ready() {
console.log('MathJax配置完成开始初始化...');
MathJax.startup.defaultReady();
// 处理特定区域的数学公式
const contentEl = document.getElementById('content');
const commentsEl = document.getElementById('comments');
const promises = [];
if (contentEl) {
promises.push(MathJax.typesetPromise([contentEl]));
}
if (commentsEl) {
promises.push(MathJax.typesetPromise([commentsEl]));
}
// 等待所有渲染完成
Promise.all(promises).then(() => {
console.log('MathJax渲染完成');
// 触发自定义事件通知其他脚本MathJax已就绪
document.dispatchEvent(new CustomEvent('mathjaxReady'));
}).catch(error => {
console.error('MathJax渲染失败:', error);
});
}
},
// 输出配置
chtml: {
scale: 1,
minScale: 0.5,
matchFontHeight: false,
displayAlign: 'center',
displayIndent: '0'
}
};
}
/**
* 加载MathJax库
*/
function loadMathJax() {
console.log('检测到数学公式开始加载MathJax...');
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
script.async = true;
script.defer = true;
script.onload = function() {
console.log('MathJax库加载成功');
};
script.onerror = function() {
console.error('MathJax库加载失败尝试备用CDN...');
// 备用CDN
const fallbackScript = document.createElement('script');
fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
fallbackScript.onload = function() {
const mathJaxScript = document.createElement('script');
mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
mathJaxScript.async = true;
document.head.appendChild(mathJaxScript);
};
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
}
/**
* 初始化函数
*/
function init() {
// 等待DOM完全加载
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
// 检测是否需要加载MathJax
if (hasMathFormulas()) {
// 先配置,再加载
configureMathJax();
loadMathJax();
} else {
console.log('未检测到数学公式跳过MathJax加载');
}
}
// 提供重新渲染的全局方法,供动态内容使用
window.rerenderMathJax = function(element) {
if (window.MathJax && window.MathJax.typesetPromise) {
const target = element || document.body;
return window.MathJax.typesetPromise([target]);
}
return Promise.resolve();
};
// 启动初始化
init();
})();

@ -1,5 +1,6 @@
/**
* 处理小屏幕上的导航菜单切换和子菜单项的可访问性
* Handles toggling the navigation menu for small screens and
* accessibility for submenu items.
*/
( function() {
var nav = document.getElementById( 'site-navigation' ), button, menu;
@ -13,19 +14,17 @@
return;
}
// 如果菜单不存在或为空则隐藏按钮
// Hide button if menu is missing or empty.
if ( ! menu || ! menu.childNodes.length ) {
button.style.display = 'none';
return;
}
// 按钮点击事件处理
button.onclick = function() {
if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
menu.className = 'nav-menu';
}
// 切换菜单展开/收起状态
if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
button.className = button.className.replace( ' toggled-on', '' );
menu.className = menu.className.replace( ' toggled-on', '' );
@ -36,14 +35,12 @@
};
} )();
// 为隐藏的子菜单项提供更好的焦点支持以提高可访问性
// Better focus for hidden submenu items for accessibility.
( function( $ ) {
// 为导航菜单中的链接添加焦点和失焦事件
$( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
$( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
} );
// 移动端触摸事件处理
if ( 'ontouchstart' in window ) {
$('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
var el = $( this ).parent( 'li' );
@ -55,4 +52,4 @@
}
} );
}
} )( jQuery );
} )( jQuery );

@ -1,352 +1,293 @@
/* 代码高亮样式 */
.codehilite .hll {
background-color: #ffffcc
}
/* 代码块背景 */
.codehilite {
background: #ffffff;
}
/* 注释 */
.codehilite .c {
color: #177500
}
/* 注释 */
/* Error */
/* Comment */
.codehilite .err {
color: #000000
}
/* 错误 */
/* Keyword */
/* Error */
.codehilite .k {
color: #A90D91
}
/* 关键字 */
/* Literal */
/* Keyword */
.codehilite .l {
color: #1C01CE
}
/* 字面量 */
/* Name */
/* Literal */
.codehilite .n {
color: #000000
}
/* 名称 */
/* Operator */
/* Name */
.codehilite .o {
color: #000000
}
/* 操作符 */
/* Comment.Hashbang */
/* Operator */
.codehilite .ch {
color: #177500
}
/* Hashbang注释 */
/* Comment.Multiline */
/* Comment.Hashbang */
.codehilite .cm {
color: #177500
}
/* 多行注释 */
/* Comment.Preproc */
/* Comment.Multiline */
.codehilite .cp {
color: #633820
}
/* 预处理注释 */
/* Comment.PreprocFile */
/* Comment.Preproc */
.codehilite .cpf {
color: #177500
}
/* 文件预处理注释 */
/* Comment.Single */
/* Comment.PreprocFile */
.codehilite .c1 {
color: #177500
}
/* 单行注释 */
/* Comment.Special */
/* Comment.Single */
.codehilite .cs {
color: #177500
}
/* 特殊注释 */
/* Keyword.Constant */
/* Comment.Special */
.codehilite .kc {
color: #A90D91
}
/* 常量关键字 */
/* Keyword.Declaration */
/* Keyword.Constant */
.codehilite .kd {
color: #A90D91
}
/* 声明关键字 */
/* Keyword.Namespace */
/* Keyword.Declaration */
.codehilite .kn {
color: #A90D91
}
/* 命名空间关键字 */
/* Keyword.Pseudo */
/* Keyword.Namespace */
.codehilite .kp {
color: #A90D91
}
/* 伪关键字 */
/* Keyword.Reserved */
/* Keyword.Pseudo */
.codehilite .kr {
color: #A90D91
}
/* 保留关键字 */
/* Keyword.Type */
/* Keyword.Reserved */
.codehilite .kt {
color: #A90D91
}
/* 类型关键字 */
/* Literal.Date */
/* Keyword.Type */
.codehilite .ld {
color: #1C01CE
}
/* 日期字面量 */
/* Literal.Number */
/* Literal.Date */
.codehilite .m {
color: #1C01CE
}
/* 数字字面量 */
/* Literal.String */
/* Literal.Number */
.codehilite .s {
color: #C41A16
}
/* 字符串字面量 */
/* Name.Attribute */
/* Literal.String */
.codehilite .na {
color: #836C28
}
/* 属性名称 */
/* Name.Builtin */
/* Name.Attribute */
.codehilite .nb {
color: #A90D91
}
/* 内置名称 */
/* Name.Class */
/* Name.Builtin */
.codehilite .nc {
color: #3F6E75
}
/* 类名 */
/* Name.Constant */
/* Name.Class */
.codehilite .no {
color: #000000
}
/* 常量名称 */
/* Name.Decorator */
/* Name.Constant */
.codehilite .nd {
color: #000000
}
/* 装饰器名称 */
/* Name.Entity */
/* Name.Decorator */
.codehilite .ni {
color: #000000
}
/* 实体名称 */
/* Name.Exception */
/* Name.Entity */
.codehilite .ne {
color: #000000
}
/* 异常名称 */
/* Name.Function */
/* Name.Exception */
.codehilite .nf {
color: #000000
}
/* 函数名称 */
/* Name.Label */
/* Name.Function */
.codehilite .nl {
color: #000000
}
/* 标签名称 */
/* Name.Namespace */
/* Name.Label */
.codehilite .nn {
color: #000000
}
/* 命名空间名称 */
/* Name.Other */
/* Name.Namespace */
.codehilite .nx {
color: #000000
}
/* 其他名称 */
/* Name.Property */
/* Name.Other */
.codehilite .py {
color: #000000
}
/* 属性名称 */
/* Name.Tag */
/* Name.Property */
.codehilite .nt {
color: #000000
}
/* 标签名称 */
/* Name.Variable */
/* Name.Tag */
.codehilite .nv {
color: #000000
}
/* 变量名称 */
/* Operator.Word */
/* Name.Variable */
.codehilite .ow {
color: #000000
}
/* 操作符单词 */
/* Literal.Number.Bin */
/* Operator.Word */
.codehilite .mb {
color: #1C01CE
}
/* 二进制数字 */
/* Literal.Number.Float */
/* Literal.Number.Bin */
.codehilite .mf {
color: #1C01CE
}
/* 浮点数 */
/* Literal.Number.Hex */
/* Literal.Number.Float */
.codehilite .mh {
color: #1C01CE
}
/* 十六进制数 */
/* Literal.Number.Integer */
/* Literal.Number.Hex */
.codehilite .mi {
color: #1C01CE
}
/* 整数 */
/* Literal.Number.Oct */
/* Literal.Number.Integer */
.codehilite .mo {
color: #1C01CE
}
/* 八进制数 */
/* Literal.String.Backtick */
/* Literal.Number.Oct */
.codehilite .sb {
color: #C41A16
}
/* 反引号字符串 */
/* Literal.String.Char */
/* Literal.String.Backtick */
.codehilite .sc {
color: #2300CE
}
/* 字符 */
/* Literal.String.Doc */
/* Literal.String.Char */
.codehilite .sd {
color: #C41A16
}
/* 文档字符串 */
/* Literal.String.Double */
/* Literal.String.Doc */
.codehilite .s2 {
color: #C41A16
}
/* 双引号字符串 */
/* Literal.String.Escape */
/* Literal.String.Double */
.codehilite .se {
color: #C41A16
}
/* 转义字符串 */
/* Literal.String.Heredoc */
/* Literal.String.Escape */
.codehilite .sh {
color: #C41A16
}
/* Heredoc字符串 */
/* Literal.String.Interpol */
/* Literal.String.Heredoc */
.codehilite .si {
color: #C41A16
}
/* 插值字符串 */
/* Literal.String.Other */
/* Literal.String.Interpol */
.codehilite .sx {
color: #C41A16
}
/* 其他字符串 */
/* Literal.String.Regex */
/* Literal.String.Other */
.codehilite .sr {
color: #C41A16
}
/* 正则表达式 */
/* Literal.String.Single */
/* Literal.String.Regex */
.codehilite .s1 {
color: #C41A16
}
/* 单引号字符串 */
/* Literal.String.Symbol */
/* Literal.String.Single */
.codehilite .ss {
color: #C41A16
}
/* 符号 */
/* Name.Builtin.Pseudo */
/* Literal.String.Symbol */
.codehilite .bp {
color: #5B269A
}
/* 伪内置名称 */
/* Name.Variable.Class */
/* Name.Builtin.Pseudo */
.codehilite .vc {
color: #000000
}
/* 类变量 */
/* Name.Variable.Global */
/* Name.Variable.Class */
.codehilite .vg {
color: #000000
}
/* 全局变量 */
/* Name.Variable.Instance */
/* Name.Variable.Global */
.codehilite .vi {
color: #000000
}
/* 实例变量 */
/* Literal.Number.Integer.Long */
/* Name.Variable.Instance */
.codehilite .il {
color: #1C01CE
}
/* 长整数 */
/* Literal.Number.Integer.Long */

@ -20,38 +20,18 @@ from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__) # 创建日志记录器
logger = logging.getLogger(__name__)
register = template.Library() # 创建模板标签注册器
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
"""
<<<<<<< HEAD
头部元数据标签
通过插件系统应用过滤器生成头部元数据
:param context: 模板上下文
:return: 头部元数据HTML
=======
生成页面头部meta信息
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
"""
<<<<<<< HEAD
时间格式化标签
将时间数据格式化为设置中指定的时间格式
=======
格式化时间显示
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:param data: 时间数据
:return: 格式化后的时间字符串
"""
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
@ -61,17 +41,6 @@ def timeformat(data):
@register.simple_tag
def datetimeformat(data):
"""
<<<<<<< HEAD
日期时间格式化标签
将时间数据格式化为设置中指定的日期时间格式
:param data: 时间数据
=======
格式化日期时间显示
:param data: 日期时间数据
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:return: 格式化后的日期时间字符串
"""
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
@ -83,33 +52,78 @@ def datetimeformat(data):
@stringfilter
def custom_markdown(content):
"""
<<<<<<< HEAD
自定义Markdown转换过滤器
将Markdown格式的内容转换为HTML
:param content: Markdown格式的内容
:return: 转换后的HTML内容
=======
将内容转换为markdown格式
:param content: 原始内容
:return: markdown格式的内容
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
通用markdown过滤器应用文章内容插件
主要用于文章内容处理
"""
return mark_safe(CommonMarkdown.get_markdown(content))
html_content = CommonMarkdown.get_markdown(content)
# 然后应用插件过滤器优化HTML
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
return mark_safe(optimized_html)
@register.filter()
@stringfilter
def sidebar_markdown(content):
html_content = CommonMarkdown.get_markdown(content)
return mark_safe(html_content)
@register.simple_tag(takes_context=True)
def render_article_content(context, article, is_summary=False):
"""
渲染文章内容包含完整的上下文信息供插件使用
Args:
context: 模板上下文
article: 文章对象
is_summary: 是否为摘要模式首页使用
"""
if not article or not hasattr(article, 'body'):
return ''
# 先转换Markdown为HTML
html_content = CommonMarkdown.get_markdown(article.body)
# 如果是摘要模式,先截断内容再应用插件
if is_summary:
# 截断HTML内容到合适的长度约300字符
from django.utils.html import strip_tags
from django.template.defaultfilters import truncatechars
# 先去除HTML标签截断纯文本然后重新转换为HTML
plain_text = strip_tags(html_content)
truncated_text = truncatechars(plain_text, 300)
# 重新转换截断后的文本为HTML简化版避免复杂的插件处理
html_content = CommonMarkdown.get_markdown(truncated_text)
# 然后应用插件过滤器,传递完整的上下文
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 获取request对象
request = context.get('request')
# 应用所有文章内容相关的插件
# 注意:摘要模式下某些插件(如版权声明)可能不适用
optimized_html = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME,
html_content,
article=article,
request=request,
context=context,
is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
)
return mark_safe(optimized_html)
@register.simple_tag
def get_markdown_toc(content):
"""
<<<<<<< HEAD
获取Markdown目录标签
从Markdown内容中提取目录结构
:param content: Markdown格式的内容
=======
获取markdown内容的目录
:param content: markdown内容
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:return: 目录HTML
"""
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@ -118,18 +132,6 @@ def get_markdown_toc(content):
@register.filter()
@stringfilter
def comment_markdown(content):
"""
<<<<<<< HEAD
评论Markdown转换过滤器
将Markdown格式的评论内容转换为HTML并清理不安全标签
:param content: Markdown格式的评论内容
:return: 转换并清理后的HTML内容
=======
将评论内容转换为markdown格式并清理HTML
:param content: 评论内容
:return: 清理后的markdown格式内容
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@ -139,14 +141,8 @@ def comment_markdown(content):
def truncatechars_content(content):
"""
获得文章内容的摘要
<<<<<<< HEAD
根据博客设置中的文章摘要长度截取内容
:param content: 原始内容
:return: 截取后的内容
=======
:param content: 文章内容
:return: 截取后的摘要内容
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:param content:
:return:
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
@ -157,33 +153,17 @@ def truncatechars_content(content):
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
"""
<<<<<<< HEAD
内容截取过滤器
移除HTML标签并截取前150个字符
:param content: 原始内容
:return: 截取后的内容
=======
截取内容前150个字符去除HTML标签
:param content: 原始内容
:return: 截取后的纯文本内容
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
<<<<<<< HEAD
加载面包屑导航标签
生成文章的面包屑导航信息
=======
获得文章面包屑导航
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:param article: 文章对象
:return: 面包屑导航数据
获得文章面包屑
:param article:
:return:
"""
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
@ -202,16 +182,9 @@ def load_breadcrumb(article):
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
<<<<<<< HEAD
加载文章标签列表标签
生成文章标签的显示数据
:param article: 文章对象
:return: 文章标签列表数据
=======
加载文章标签列表
:param article: 文章对象
:return: 标签列表数据
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
文章标签
:param article:
:return:
"""
tags = article.tags.all()
tags_list = []
@ -229,18 +202,8 @@ def load_articletags(article):
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
<<<<<<< HEAD
加载侧边栏标签
生成侧边栏显示数据包括文章分类标签等信息
使用缓存提高性能
:param user: 当前用户
:param linktype: 链接类型
=======
加载侧边栏
:param user: 当前用户
:param linktype: 链接显示类型
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:return: 侧边栏数据
:return:
"""
value = cache.get("sidebar" + linktype)
if value:
@ -250,38 +213,28 @@ def load_sidebar(user, linktype):
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
# 获取最新文章
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
# 获取所有分类
sidebar_categorys = Category.objects.all()
# 获取启用的额外侧边栏
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
# 获取阅读量最高的文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
# 获取文章发布日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
# 获取友情链接
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
# 获取最新评论
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
# 获取每个标签的文章数量
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
# 计算标签字体大小
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
@ -300,7 +253,6 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
# 缓存侧边栏数据3小时
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
@ -310,18 +262,9 @@ def load_sidebar(user, linktype):
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
<<<<<<< HEAD
加载文章元信息标签
生成文章元信息显示数据
:param article: 文章对象
:param user: 当前用户
:return: 文章元信息数据
=======
获得文章meta信息
:param article: 文章对象
:param user: 当前用户
:return: 文章meta信息数据
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:param article:
:return:
"""
return {
'article': article,
@ -330,27 +273,9 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
"""
<<<<<<< HEAD
加载分页信息标签
根据页面类型生成分页导航链接
:param page_obj: 分页对象
:param page_type: 页面类型
:param tag_name: 标签名称
:return: 分页信息数据
=======
加载分页信息
:param page_obj: 分页对象
:param page_type: 页面类型
:param tag_name: 标签名/分类名/作者名
:return: 分页链接数据
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
"""
def load_pagination_info(page_obj, page_type, tag_name, sort=None):
previous_url = ''
next_url = ''
# 首页分页
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -360,8 +285,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
# 标签页面分页
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
@ -378,8 +301,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'tag_name': tag.slug})
# 作者文章页面分页
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -396,7 +317,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number,
'author_name': tag_name})
# 分类目录页面分页
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
@ -414,6 +334,12 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number,
'category_name': category.slug})
if sort:
if next_url:
next_url = f"{next_url}?sort={sort}"
if previous_url:
previous_url = f"{previous_url}?sort={sort}"
return {
'previous_url': previous_url,
'next_url': next_url,
@ -421,105 +347,228 @@ def load_pagination_info(page_obj, page_type, tag_name):
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
@register.inclusion_tag('blog/tags/article_info.html', takes_context=True)
def load_article_detail(context, article, isindex, user):
"""
<<<<<<< HEAD
加载文章详情标签
生成文章详情显示数据
=======
加载文章详情
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
:param article: 文章对象
:param isindex: 是否为列表页
:param user: 当前用户
:return: 文章详情数据
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
request = context.get('request')
liked_ids = request.session.get('liked_articles', []) if request else []
favorited_ids = request.session.get('favorited_articles', []) if request else []
has_liked = False
has_favorited = False
if request and hasattr(request, 'user') and request.user.is_authenticated:
has_liked = article.like_users.filter(pk=request.user.pk).exists()
has_favorited = article.favorite_users.filter(pk=request.user.pk).exists()
else:
has_liked = article.id in liked_ids
has_favorited = article.id in favorited_ids
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
'has_liked': has_liked,
'has_favorited': has_favorited,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
# 返回用户头像URL
# 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""
获得gravatar头像URL
:param email: 邮箱地址
:param size: 头像大小
:return: gravatar头像URL
"""
cachekey = 'gravatat/' + email
"""获得用户头像 - 优先使用OAuth头像否则使用默认头像"""
cachekey = 'avatar/' + email
url = cache.get(cachekey)
if url:
return url
else:
# 查找OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
# 生成gravatar URL
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
# 缓存头像URL 10小时
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
# 检查OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
# 过滤出有头像的用户
users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
if users_with_picture:
# 获取默认头像路径用于比较
default_avatar_path = static('blog/img/avatar.png')
# 优先选择非默认头像的用户,否则选择第一个
non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
avatar_type = 'non-default' if non_default_users else 'default'
logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
return url
# 使用默认头像
url = static('blog/img/avatar.png')
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
logger.info('Using default avatar for {}'.format(email))
return url
@register.filter
def gravatar(email, size=40):
"""
获得gravatar头像HTML标签
:param email: 邮箱地址
:param size: 头像大小
:return: 头像img标签
"""
"""获得用户头像HTML标签"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' % (url, size, size))
'<img src="%s" height="%d" width="%d" class="avatar" alt="用户头像">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
"""
模板标签允许查询集过滤用法:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
:param qs: 查询集
:param kwargs: 过滤条件
:return: 过滤后的查询集
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
<<<<<<< HEAD
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
=======
"""
连接两个字符串
:param arg1: 第一个字符串
:param arg2: 第二个字符串
:return: 连接后的字符串
"""
return str(arg1) + str(arg2)
>>>>>>> ff8be05a63440d45c1cc36269ce152191eac8fdc
# === 插件系统模板标签 ===
@register.simple_tag(takes_context=True)
def render_plugin_widgets(context, position, **kwargs):
"""
渲染指定位置的所有插件组件
Args:
context: 模板上下文
position: 位置标识
**kwargs: 传递给插件的额外参数
Returns:
按优先级排序的所有插件HTML内容
"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
widgets = []
for plugin in get_loaded_plugins():
try:
widget_data = plugin.render_position_widget(
position=position,
context=context,
**kwargs
)
if widget_data:
widgets.append(widget_data)
except Exception as e:
logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}")
# 按优先级排序(数字越小优先级越高)
widgets.sort(key=lambda x: x['priority'])
# 合并HTML内容
html_parts = [widget['html'] for widget in widgets]
return mark_safe(''.join(html_parts))
@register.simple_tag(takes_context=True)
def plugin_head_resources(context):
"""渲染所有插件的head资源仅自定义HTMLCSS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义head HTMLCSS文件已通过压缩系统处理
head_html = plugin.get_head_html(context)
if head_html:
resources.append(head_html)
except Exception as e:
logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.simple_tag(takes_context=True)
def plugin_body_resources(context):
"""渲染所有插件的body资源仅自定义HTMLJS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义body HTMLJS文件已通过压缩系统处理
body_html = plugin.get_body_html(context)
if body_html:
resources.append(body_html)
except Exception as e:
logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.inclusion_tag('plugins/css_includes.html')
def plugin_compressed_css():
"""插件CSS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
css_files = []
for plugin in get_loaded_plugins():
for css_file in plugin.get_css_files():
css_url = plugin.get_static_url(css_file)
css_files.append(css_url)
return {'css_files': css_files}
@register.inclusion_tag('plugins/js_includes.html')
def plugin_compressed_js():
"""插件JS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
js_files = []
for plugin in get_loaded_plugins():
for js_file in plugin.get_js_files():
js_url = plugin.get_static_url(js_file)
js_files.append(js_url)
return {'js_files': js_files}
@register.simple_tag(takes_context=True)
def plugin_widget(context, plugin_name, widget_type='default', **kwargs):
"""
渲染指定插件的组件
使用方式
{% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %}
"""
from djangoblog.plugin_manage.loader import get_plugin_by_slug
plugin = get_plugin_by_slug(plugin_name)
if plugin and hasattr(plugin, 'render_template'):
try:
widget_context = {**context.flatten(), **kwargs}
template_name = f"{widget_type}.html"
return mark_safe(plugin.render_template(template_name, widget_context))
except Exception as e:
logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}")
return ""

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

Loading…
Cancel
Save