合并 #3

Open
pqkpsr3gu wants to merge 16 commits from hz_branch into develop

@ -0,0 +1,211 @@
# "DjangoBlog"软件系统的界面设计
组号19
小组成员:张恒祺,胡振,李裕祥,杜英硕,王浩
## 软件系统概述
DjangoBlog 是一款基于 Django 框架开发的现代化博客系统,旨在为个人博主、技术团队和企业提供功能完善的内容发布平台。系统通过用户友好的界面设计和清晰的模板架构,实现了从文章发布、内容管理到读者互动的全流程服务。系统解决了传统博客系统界面单一、功能有限、维护困难等问题,为作者提供便捷的内容创作体验,同时为读者提供优质的阅读体验。
## 功能分析
系统主要实现以下核心功能模块:
### 1. 用户端功能
**文章展示模块**:以列表和详情形式展示所有文章,支持分类浏览和标签筛选
**智能搜索模块**:支持全文搜索,提供精准的搜索结果和高亮显示
**文章归档模块**:按时间维度组织文章,支持年月日多级归档浏览
**评论互动模块**:读者评论、点赞、分享等社交功能
**用户认证模块**:用户注册、登录、密码重置等账户管理功能
### 2. 管理端功能
**文章管理模块**:文章的增删改查、发布状态控制、分类标签管理
**评论管理模块**:评论审核、回复、垃圾评论过滤
**数据统计模块**:访问统计、热门文章分析、读者行为分析
**系统设置模块**博客信息配置、主题样式管理、SEO设置
## 软件主要的界面设计
### 1. 系统首页界面
**界面功能**:文章列表展示、分类导航、搜索入口
**界面元素**
- 顶部导航栏包含Logo、主导航菜单首页、分类、归档、关于
- 侧边栏区域:搜索框、分类列表、热门文章、标签云
- 文章列表区:采用卡片式布局展示文章标题、摘要、发布时间、阅读量
- 分页导航:支持多页文章浏览
### 2. 文章详情界面
**界面功能**:完整文章内容展示、评论系统、相关文章推荐
**界面组件**
- 面包屑导航:显示当前位置和导航路径
- 文章标题区:文章标题、作者信息、发布时间、阅读统计
- 文章内容区支持Markdown渲染、代码高亮、目录导航
- 文章元信息:标签列表、分类信息、社交分享按钮
- 评论区域:评论列表、评论表单、评论分页
### 3. 搜索界面
**界面功能**:全文搜索、结果展示、高级筛选
**界面元素**
- 搜索框:支持关键词联想和搜索历史
- 搜索结果:以列表形式展示,支持按相关度、时间排序
- 高亮显示:搜索关键词在结果中高亮标记
- 筛选选项:支持按分类、标签、时间范围筛选
### 4. 归档界面
**界面功能**:按时间浏览历史文章
**界面组件**
- 时间树形结构:按年-月-日层次展示文章分布
- 文章链接列表:每个时间节点下的文章标题链接
- 统计信息:各时间段文章数量统计
**界面关系图**
![图片描述](https://code.educoder.net/api/pqkpsr3gu/DjangoBlog/raw/src%2Fimg%2Ff5b2b176a93286ba173c053e60bb4b0d.png?ref=hz_branch)
## 模板设计
### 模板架构设计
本系统采用Django模板引擎基于MTV架构设计了一套完整的模板系统实现了界面元素的高度复用和统一管理。模板系统采用分层设计理念从基础布局到具体功能页面形成了清晰的继承和包含关系。
### 模板继承关系
![归档界面时间树结构](https://code.educoder.net/api/pqkpsr3gu/DjangoBlog/raw/src%2Fimg%2Fbb816f59d4732648c2ce554565975e06.png?ref=hz_branch)
### 模板继承机制说明
#### 1. 基础模板层次
**base.html (基础布局)**
- 提供网站整体HTML结构
- 包含全局CSS和JavaScript资源
- 定义主要的内容区块header、content、footer、sidebar
**base_account.html (用户认证页面布局)**
- 继承base.html但采用简化布局
- 专门用于登录、注册等认证页面
- 移除侧边栏,专注于表单功能
**各功能页面模板**
- article_index.html (文章列表):展示分页文章列表
- article_detail.html (文章详情):完整文章内容和评论系统
- article_archives.html (文章归档):时间维度的文章组织
- error_page.html (错误页面):友好的错误提示
- search.html (搜索页面):搜索结果展示
#### 2. 模板包含关系
**base.html 包含关系**
- nav.html (导航菜单):主导航和响应式菜单
- footer.html (页脚):版权信息、友情链接、备案信息
- adsense.html (广告):广告位和统计代码
**内容区块结构**
- content (主要内容):各页面特有的内容区域
- sidebar (侧边栏)通过sidebar.html加载辅助功能组件
#### 3. 模板标签系统
**核心模板标签 (blog_tags.py)**
- `load_article_detail`:加载文章详情数据
- `load_sidebar`:动态生成侧边栏内容
- `load_breadcrumb`:生成面包屑导航
- `load_article_metas`:加载文章元信息(作者、时间、阅读量)
- `load_pagination_info`:生成分页导航
**模板标签文件结构**
```
templates/blog/tags/
├── article_info.html (文章基本信息展示)
├── article_meta_info.html (文章元信息展示)
├── article_pagination.html (分页控件)
├── article_tag_list.html (标签云和标签列表)
├── breadcrumb.html (面包屑导航)
└── sidebar.html (侧边栏组件集合)
```
#### 4. 模板功能分析
**基础模板 (base.html) 功能**
- HTML5 文档结构和语义化标签
- SEO 元标签优化title、description、keywords
- OpenGraph 和 Twitter Card 社交媒体支持
- 响应式设计支持和移动端适配
- 全局样式表和JavaScript资源管理
- 网站头部导航和底部信息统一管理
**文章列表模板 (article_index.html) 特点**
- 支持多种布局模式(列表、网格)
- 智能分页机制,优化大量数据展示
- 多维度筛选(分类、标签、时间)
- 集成搜索功能和排序选项
- SEO友好的URL结构和标题层次
**文章详情模板 (article_detail.html) 功能**
- Markdown内容渲染和代码高亮
- 自动生成文章目录TOC导航
- 多级评论系统和回复功能
- 相关文章推荐算法
- 社交分享按钮和统计代码
- 阅读进度指示器和阅读时间估算
**侧边栏模板 (sidebar.html) 组件**
- 智能搜索框(支持自动补全)
- 分类树形结构和文章计数
- 热门文章排行榜(按阅读量、评论数)
- 最近评论列表和作者回复
- 标签云可视化展示
- 友情链接管理和展示
- 管理员功能快捷入口
### 界面设计特点
#### 1. 响应式设计
- 采用移动优先的响应式设计理念
- 支持从手机到桌面的全设备适配
- 自适应侧边栏布局(桌面端显示,移动端可隐藏)
- 触摸友好的交互元素设计
#### 2. SEO 优化
- 完整的meta标签体系和结构化数据
- JSON-LD格式的富片段标记
- 面包屑导航和规范的URL结构
- 语义化HTML5标签使用
- 图片alt属性和延迟加载
#### 3. 用户体验
- 清晰的视觉层次和信息架构
- 快速的页面加载和流畅的交互
- 智能的内容推荐和个性化展示
- 无障碍访问支持ARIA标签
- 一致的操作反馈和状态提示
#### 4. 性能优化
- 模板片段缓存和数据库查询优化
- 静态资源压缩和CDN加速
- 图片懒加载和响应式图片
- 渐进式加载和骨架屏技术
- 服务端渲染和客户端增强
## 总结
DjangoBlog 系统采用了清晰的模板继承和包含关系,通过基础模板提供统一布局,通过模板标签实现组件化开发。整个系统具有良好的可维护性和扩展性,适合作为博客系统的基础框架进行二次开发。
界面设计注重用户体验和 SEO 优化,通过响应式布局和丰富的交互功能,为用户提供了良好的浏览体验。模板系统的模块化设计使得系统易于维护和扩展,同时为后续功能迭代提供了良好的基础架构。
系统的界面流转逻辑清晰,用户可以从多个入口访问内容,最终都汇聚到核心的文章详情页面。这种设计既保证了内容的可发现性,又提供了连贯的用户体验。模板标签系统的细粒度组件设计,使得界面元素的复用和定制变得简单高效。

@ -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,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

@ -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

@ -8,7 +8,7 @@ on:
branches:
- 'master'
- 'dev'
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
@ -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

1
src/.gitignore vendored

@ -62,7 +62,6 @@ target/
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/

@ -1,164 +0,0 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */

@ -1,528 +0,0 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
# SIG # Begin signature block
# MII0CQYJKoZIhvcNAQcCoIIz+jCCM/YCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnL745ElCYk8vk
# dBtMuQhLeWJ3ZGfzKW4DHCYzAn+QB6CCG9IwggXMMIIDtKADAgECAhBUmNLR1FsZ
# lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu
# dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy
# MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv
# c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo
# b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD
# Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH
# or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD
# PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV
# qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70
# ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1
# vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3
# W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK
# xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg
# trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj
# O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7
# swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE
# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ
# T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn
# LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c
# Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF
# xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx
# AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8
# FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY
# lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2
# 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY
# uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al
# GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk
# ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T
# 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggb+MIIE5qADAgECAhMzAAM/y2Wy
# WWnFfpZcAAAAAz/LMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ
# RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDEwHhcNMjUwNDA4MDEwNzI0WhcNMjUwNDEx
# MDEwNzI0WjB8MQswCQYDVQQGEwJVUzEPMA0GA1UECBMGT3JlZ29uMRIwEAYDVQQH
# EwlCZWF2ZXJ0b24xIzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9u
# MSMwIQYDVQQDExpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjCCAaIwDQYJKoZI
# hvcNAQEBBQADggGPADCCAYoCggGBAI0elXEcbTdGLOszMU2fzimHGM9Y4EjwFgC2
# iGPdieHc0dK1DyEIdtnvjKxnG/KICC3J2MrhePGzMEkie3yQjx05B5leG0q8YoGU
# m9z9K67V6k3DSXX0vQe9FbaNVuyXed31MEf/qek7Zo4ELxu8n/LO3ibURBLRHNoW
# Dz9zr4DcU+hha0bdIL6SnKMLwHqRj59gtFFEPqXcOVO7kobkzQS3O1T5KNL/zGuW
# UGQln7fS4YI9bj24bfrSeG/QzLgChVYScxnUgjAANfT1+SnSxrT4/esMtfbcvfID
# BIvOWk+FPPj9IQWsAMEG/LLG4cF/pQ/TozUXKx362GJBbe6paTM/RCUTcffd83h2
# bXo9vXO/roZYk6H0ecd2h2FFzLUQn/0i4RQQSOp6zt1eDf28h6F8ev+YYKcChph8
# iRt32bJPcLQVbUzhehzT4C0pz6oAqPz8s0BGvlj1G6r4CY1Cs2YiMU09/Fl64pWf
# IsA/ReaYj6yNsgQZNUcvzobK2mTxMwIDAQABo4ICGTCCAhUwDAYDVR0TAQH/BAIw
# ADAOBgNVHQ8BAf8EBAMCB4AwPAYDVR0lBDUwMwYKKwYBBAGCN2EBAAYIKwYBBQUH
# AwMGGysGAQQBgjdhgqKNuwqmkohkgZH0oEWCk/3hbzAdBgNVHQ4EFgQU4Y4Xr/Xn
# zEXblXrNC0ZLdaPEJYUwHwYDVR0jBBgwFoAU6IPEM9fcnwycdpoKptTfh6ZeWO4w
# ZwYDVR0fBGAwXjBcoFqgWIZWaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0El
# MjAwMS5jcmwwgaUGCCsGAQUFBwEBBIGYMIGVMGQGCCsGAQUFBzAChlhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIw
# VmVyaWZpZWQlMjBDUyUyMEFPQyUyMENBJTIwMDEuY3J0MC0GCCsGAQUFBzABhiFo
# dHRwOi8vb25lb2NzcC5taWNyb3NvZnQuY29tL29jc3AwZgYDVR0gBF8wXTBRBgwr
# BgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAEEATANBgkqhkiG
# 9w0BAQwFAAOCAgEAKTeVGPXsDKqQLe1OuKx6K6q711FPxNQyLOOqeenH8zybHwNo
# k05cMk39HQ7u+R9BQIL0bWexb7wa3XeKaX06p7aY/OQs+ycvUi/fC6RGlaLWmQ9D
# YhZn2TBz5znimvSf3P+aidCuXeDU5c8GpBFog6fjEa/k+n7TILi0spuYZ4yC9R48
# R63/VvpLi2SqxfJbx5n92bY6driNzAntjoravF25BSejXVrdzefbnqbQnZPB39g8
# XHygGPb0912fIuNKPLQa/uCnmYdXJnPb0ZgMxxA8fyxvL2Q30Qf5xpFDssPDElvD
# DoAbvR24CWvuHbu+CMMr2SJUpX4RRvDioO7JeB6wZb+64MXyPUSSf6QwkKNsHPIa
# e9tSfREh86sYn5bOA0Wd+Igk0RpA5jDRTu3GgPOPWbm1PU+VoeqThtHt6R3l17pr
# aQ5wIuuLXgxi1K4ZWgtvXw8BtIXfZz24qCtoo0+3kEGUpEHBgkF1SClbRb8uAzx+
# 0ROGniLPJRU20Xfn7CgipeKLcNn33JPFwQHk1zpbGS0090mi0erOQCz0S47YdHmm
# RJcbkNIL9DeNAglTZ/TFxrYUM1NRS1Cp4e63MgBKcWh9VJNokInzzmS+bofZz+u1
# mm8YNtiJjdT8fmizXdUEk68EXQhOs0+HBNvc9nMRK6R28MZu/J+PaUcPL84wggda
# MIIFQqADAgECAhMzAAAABzeMW6HZW4zUAAAAAAAHMA0GCSqGSIb3DQEBDAUAMGMx
# CzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNDAy
# BgNVBAMTK01pY3Jvc29mdCBJRCBWZXJpZmllZCBDb2RlIFNpZ25pbmcgUENBIDIw
# MjEwHhcNMjEwNDEzMTczMTU0WhcNMjYwNDEzMTczMTU0WjBaMQswCQYDVQQGEwJV
# UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSswKQYDVQQDEyJNaWNy
# b3NvZnQgSUQgVmVyaWZpZWQgQ1MgQU9DIENBIDAxMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAt/fAAygHxbo+jxA04hNI8bz+EqbWvSu9dRgAawjCZau1
# Y54IQal5ArpJWi8cIj0WA+mpwix8iTRguq9JELZvTMo2Z1U6AtE1Tn3mvq3mywZ9
# SexVd+rPOTr+uda6GVgwLA80LhRf82AvrSwxmZpCH/laT08dn7+Gt0cXYVNKJORm
# 1hSrAjjDQiZ1Jiq/SqiDoHN6PGmT5hXKs22E79MeFWYB4y0UlNqW0Z2LPNua8k0r
# bERdiNS+nTP/xsESZUnrbmyXZaHvcyEKYK85WBz3Sr6Et8Vlbdid/pjBpcHI+Hyt
# oaUAGE6rSWqmh7/aEZeDDUkz9uMKOGasIgYnenUk5E0b2U//bQqDv3qdhj9UJYWA
# DNYC/3i3ixcW1VELaU+wTqXTxLAFelCi/lRHSjaWipDeE/TbBb0zTCiLnc9nmOjZ
# PKlutMNho91wxo4itcJoIk2bPot9t+AV+UwNaDRIbcEaQaBycl9pcYwWmf0bJ4IF
# n/CmYMVG1ekCBxByyRNkFkHmuMXLX6PMXcveE46jMr9syC3M8JHRddR4zVjd/FxB
# nS5HOro3pg6StuEPshrp7I/Kk1cTG8yOWl8aqf6OJeAVyG4lyJ9V+ZxClYmaU5yv
# tKYKk1FLBnEBfDWw+UAzQV0vcLp6AVx2Fc8n0vpoyudr3SwZmckJuz7R+S79BzMC
# AwEAAaOCAg4wggIKMA4GA1UdDwEB/wQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADAd
# BgNVHQ4EFgQU6IPEM9fcnwycdpoKptTfh6ZeWO4wVAYDVR0gBE0wSzBJBgRVHSAA
# MEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# RG9jcy9SZXBvc2l0b3J5Lmh0bTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAS
# BgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFNlBKbAPD2Ns72nX9c0pnqRI
# ajDmMHAGA1UdHwRpMGcwZaBjoGGGX2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
# a2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDb2RlJTIwU2ln
# bmluZyUyMFBDQSUyMDIwMjEuY3JsMIGuBggrBgEFBQcBAQSBoTCBnjBtBggrBgEF
# BQcwAoZhaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNy
# b3NvZnQlMjBJRCUyMFZlcmlmaWVkJTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAy
# MDIxLmNydDAtBggrBgEFBQcwAYYhaHR0cDovL29uZW9jc3AubWljcm9zb2Z0LmNv
# bS9vY3NwMA0GCSqGSIb3DQEBDAUAA4ICAQB3/utLItkwLTp4Nfh99vrbpSsL8NwP
# Ij2+TBnZGL3C8etTGYs+HZUxNG+rNeZa+Rzu9oEcAZJDiGjEWytzMavD6Bih3nEW
# FsIW4aGh4gB4n/pRPeeVrK4i1LG7jJ3kPLRhNOHZiLUQtmrF4V6IxtUFjvBnijaZ
# 9oIxsSSQP8iHMjP92pjQrHBFWHGDbkmx+yO6Ian3QN3YmbdfewzSvnQmKbkiTibJ
# gcJ1L0TZ7BwmsDvm+0XRsPOfFgnzhLVqZdEyWww10bflOeBKqkb3SaCNQTz8nsha
# UZhrxVU5qNgYjaaDQQm+P2SEpBF7RolEC3lllfuL4AOGCtoNdPOWrx9vBZTXAVdT
# E2r0IDk8+5y1kLGTLKzmNFn6kVCc5BddM7xoDWQ4aUoCRXcsBeRhsclk7kVXP+zJ
# GPOXwjUJbnz2Kt9iF/8B6FDO4blGuGrogMpyXkuwCC2Z4XcfyMjPDhqZYAPGGTUI
# NMtFbau5RtGG1DOWE9edCahtuPMDgByfPixvhy3sn7zUHgIC/YsOTMxVuMQi/bga
# memo/VNKZrsZaS0nzmOxKpg9qDefj5fJ9gIHXcp2F0OHcVwe3KnEXa8kqzMDfrRl
# /wwKrNSFn3p7g0b44Ad1ONDmWt61MLQvF54LG62i6ffhTCeoFT9Z9pbUo2gxlyTF
# g7Bm0fgOlnRfGDCCB54wggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJ
# KoZIhvcNAQEMBQAwdzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjFIMEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNh
# dGlvbiBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIw
# MDUyMFoXDTM2MDQwMTIwMTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlm
# aWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBALLwwK8ZiCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3
# qrZilYKJNqztA9OQacr1AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrw
# eLoQ71lzCHkD7A4As1Bs076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9
# rGQ//h890Q8JdjLLw0nV+ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8s
# ZU6ZE7iRrRZywRmrKDp5+TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfW
# o5tTgkoSMoayqoDpHOLJs+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC6
# 9oKpRQXY9bGH6jn9NEJv9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjk
# Vd6CGUAf0K+Jw+GE/5VpIVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHg
# YYjHs1NbOc6brH+eKpWLtr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu
# 0t+gCwLdN0A6cFtSRtR8bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeL
# zdpt32cdYKp+dhr2ogc+qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGj
# ggI1MIICMTAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0O
# BBYEFNlBKbAPD2Ns72nX9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8G
# CCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3Mv
# UmVwb3NpdG9yeS5odG0wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0T
# AQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYD
# VR0fBH0wezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIw
# Q2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEE
# gbYwgbMwgYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL2NlcnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIw
# Um9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYB
# BQUHMAGGIWh0dHA6Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG
# 9w0BAQwFAAOCAgEAfyUqnv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdP
# Q4H55v7VOInnmezQEeW4LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVI
# E1CRFfNF4XKO8XYEa3oW8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T
# 8FG7/VEURIVWArbAzPt9ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4
# e0ejRA7mB2IoX5hNh3UEauY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaB
# jhWmirYyANb0RJSLWjinMLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/t
# uLjAsKP6SzYZ+1Ry358ZTFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQw
# K8VtsBdekBmdTbQVoCgPCqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX
# 3mccFgx6UsQeRSdVVVNSyALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQL
# Uz6ZTl65yPUiOh3k7Yww94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i
# 3IpaDcR+iuGjH2TdaC1ZOmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxgheN
# MIIXiQIBATBxMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBBT0Mg
# Q0EgMDECEzMAAz/LZbJZacV+llwAAAADP8swDQYJYIZIAWUDBAIBBQCggcowGQYJ
# KoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQB
# gjcCARUwLwYJKoZIhvcNAQkEMSIEIGcBno/ti9PCrR9sXrajsTvlHQvGxbk63JiI
# URJByQuGMF4GCisGAQQBgjcCAQwxUDBOoEiARgBCAHUAaQBsAHQAOgAgAFIAZQBs
# AGUAYQBzAGUAXwB2ADMALgAxADIALgAxADAAXwAyADAAMgA1ADAANAAwADgALgAw
# ADKhAoAAMA0GCSqGSIb3DQEBAQUABIIBgE9xMVem4h5iAbvBzmB1pTdA4LYNkvd/
# hSbYmJRt5oJqBR0RGbUmcfYAgTlhdb/S84aGvI3N62I8qeMApnH89q+UF0i8p6+U
# Qza6Mu1cAHCq0NkHH6+N8g7nIfe5Cn+BBCBJ6kuYfQm9bx1JwEm5/yVCwG9I6+XV
# 3WonOeA8djuZFfB9OIW6N9ubX7X+nYqWaeT6w6/lDs8mL+s0Fumy4mJ8B15pd9mr
# N6dIRFokzhuALq6G0USKFzYf3qJQ4GyCos/Luez3cr8sE/78ds6vah5IlLP6qXMM
# ETwAdoymIYSm3Dly3lflodd4d7/nkMhfHITOxSUDoBbCP6MO1rhChX591rJy/omK
# 0RdM9ZpMl6VXHhzZ+lB8U/6j7xJGlxJSJHet7HFEuTnJEjY9dDy2bUgzk0vK1Rs2
# l7VLOP3X87p9iVz5vDAOQB0fcsMDJvhIzJlmIb5z2uZ6hqD4UZdTDMLIBWe9H7Kv
# rhmGDPHPRboFKtTrKoKcWaf4fJJ2NUtYlKGCFKAwghScBgorBgEEAYI3AwMBMYIU
# jDCCFIgGCSqGSIb3DQEHAqCCFHkwghR1AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFh
# BgsqhkiG9w0BCRABBKCCAVAEggFMMIIBSAIBAQYKKwYBBAGEWQoDATAxMA0GCWCG
# SAFlAwQCAQUABCAY3nVyqXzzboHwsVGd+j5FjG9eaMv+O3mJKpX+3EJ43AIGZ9gU
# uyvYGBMyMDI1MDQwODEyNDEyMi40MTNaMASAAgH0oIHgpIHdMIHaMQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQg
# QW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozREE1
# LTk2M0ItRTFGNDE1MDMGA1UEAxMsTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBT
# dGFtcGluZyBBdXRob3JpdHmggg8gMIIHgjCCBWqgAwIBAgITMwAAAAXlzw//Zi7J
# hwAAAAAABTANBgkqhkiG9w0BAQwFADB3MQswCQYDVQQGEwJVUzEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMUgwRgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRp
# dHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAw
# HhcNMjAxMTE5MjAzMjMxWhcNMzUxMTE5MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
# ZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAJ5851Jj/eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZH
# d5HBXXBvf7KrQ5cMSqFSHGqg2/qJhYqOQxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv
# 3lYkuLDsfMuIEqvGYOPURAH+Ybl4SJEESnt0MbPEoKdNihwM5xGv0rGofJ1qOYST
# Ncc55EbBT7uq3wx3mXhtVmtcCEr5ZKTkKKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgC
# sJ+sozf5EeH5KrlFnxpjKKTavwfFP6XaGZGWUG8TZaiTogRoAlqcevbiqioUz1Yt
# 4FRK53P6ovnUfANjIgM9JDdJ4e0qiDRm5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5r
# O9yhv2fjJHrmlQ0EIXmp4DhDBieKUGR+eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEf
# yTxgGjOsRpeexIveR1MPTVf7gt8hY64XNPO6iyUGsEgt8c2PxF87E+CO7A28TpjN
# q5eLiiunhKbq0XbjkNoU5JhtYUrlmAbpxRjb9tSreDdtACpm3rkpxp7AQndnI0Sh
# u/fk1/rE3oWsDqMX3jjv40e8KN5YsJBnczyWB4JyeeFMW3JBfdeAKhzohFe8U5w9
# WuvcP1E8cIxLoKSDzCCBOu0hWdjzKNu8Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3
# rV69AgMBAAGjggIbMIICFzAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMC
# AQAwHQYDVR0OBBYEFGtpKDo1L0hjQM972K9J6T7ZPdshMFQGA1UdIARNMEswSQYE
# VR0gADBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJ
# KwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME
# GDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHegdYZzaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRl
# bnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRo
# b3JpdHklMjAyMDIwLmNybDCBlAYIKwYBBQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAC
# hnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29m
# dCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRl
# JTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwDQYJKoZIhvcNAQEMBQADggIBAF+Idsd+
# bbVaFXXnTHho+k7h2ESZJRWluLE0Oa/pO+4ge/XEizXvhs0Y7+KVYyb4nHlugBes
# nFqBGEdC2IWmtKMyS1OWIviwpnK3aL5JedwzbeBF7POyg6IGG/XhhJ3UqWeWTO+C
# zb1c2NP5zyEh89F72u9UIw+IfvM9lzDmc2O2END7MPnrcjWdQnrLn1Ntday7JSyr
# DvBdmgbNnCKNZPmhzoa8PccOiQljjTW6GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1
# I64XNGqst8S+w+RUdie8fXC1jKu3m9KGIqF4aldrYBamyh3g4nJPj/LR2CBaLyD+
# 2BuGZCVmoNR/dSpRCxlot0i79dKOChmoONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG
# 89LDKbRSSvijmwJwxRxLLpMQ/u4xXxFfR4f/gksSkbJp7oqLwliDm/h+w0aJ/U5c
# cnYhYb7vPKNMN+SZDWycU5ODIRfyoGl59BsXR/HpRGtiJquOYGmvA/pk5vC1lcnb
# eMrcWD/26ozePQ/TWfNXKBOmkFpvPE8CH+EeGGWzqTCjdAsno2jzTeNSxlx3glDG
# Jgcdz5D/AAxw9Sdgq/+rY7jjgs7X6fqPTXPmaCAJKVHAP19oEjJIBwD1LyHbaEgB
# xFCogYSOiUIr0Xqcr1nJfiWG2GwYe6ZoAF1bMIIHljCCBX6gAwIBAgITMwAAAEYX
# 5HV6yv3a5QAAAAAARjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg
# UHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAeFw0yNDExMjYxODQ4NDla
# Fw0yNTExMTkxODQ4NDlaMIHaMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYw
# JAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozREE1LTk2M0ItRTFGNDE1MDMGA1UEAxMs
# TWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHkwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwlXzoj/MNL1BfnV+gg4d0fZum
# 1HdUJidSNTcDzpHJvmIBqH566zBYcV0TyN7+3qOnJjpoTx6JBMgNYnL5BmTX9Hrm
# X0WdNMLf74u7NtBSuAD2sf6n2qUUrz7i8f7r0JiZixKJnkvA/1akLHppQMDCug1o
# C0AYjd753b5vy1vWdrHXE9hL71BZe5DCq5/4LBny8aOQZlzvjewgONkiZm+Sfctk
# Jjh9LxdkDlq5EvGE6YU0uC37XF7qkHvIksD2+XgBP0lEMfmPJo2fI9FwIA9YMX7K
# IINEM5OY6nkvKryM9s5bK6LV4z48NYpiI1xvH15YDps+19nHCtKMVTZdB4cYhA0d
# VqJ7dAu4VcxUwD1AEcMxWbIOR1z6OFkVY9GX5oH8k17d9t35PWfn0XuxW4SG/rim
# gtFgpE/shRsy5nMCbHyeCdW0He1plrYQqTsSHP2n/lz2DCgIlnx+uvPLVf5+JG/1
# d1i/LdwbC2WH6UEEJyZIl3a0YwM4rdzoR+P4dO9I/2oWOxXCYqFytYdCy9ljELUw
# byLjrjRddteR8QTxrCfadKpKfFY6Ak/HNZPUHaAPak3baOIvV7Q8axo3DWQy2ib3
# zXV6hMPNt1v90pv+q9daQdwUzUrgcbwThdrRhWHwlRIVg2sR668HPn4/8l9ikGok
# rL6gAmVxNswEZ9awCwIDAQABo4IByzCCAccwHQYDVR0OBBYEFBE20NSvdrC6Z6cm
# 6RPGP8YbqIrxMB8GA1UdIwQYMBaAFGtpKDo1L0hjQM972K9J6T7ZPdshMGwGA1Ud
# HwRlMGMwYaBfoF2GW2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFB1YmxpYyUyMFJTQSUyMFRpbWVzdGFtcGluZyUyMENBJTIw
# MjAyMC5jcmwweQYIKwYBBQUHAQEEbTBrMGkGCCsGAQUFBzAChl1odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFB1YmxpYyUy
# MFJTQSUyMFRpbWVzdGFtcGluZyUyMENBJTIwMjAyMC5jcnQwDAYDVR0TAQH/BAIw
# ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwZgYDVR0g
# BF8wXTBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAEE
# AjANBgkqhkiG9w0BAQwFAAOCAgEAFIW5L+gGzX4gyHorS33YKXuK9iC91iZTpm30
# x/EdHG6U8NAu2qityxjZVq6MDq300gspG0ntzLYqVhjfku7iNzE78k6tNgFCr9wv
# GkIHeK+Q2RAO9/s5R8rhNC+lywOB+6K5Zi0kfO0agVXf7Nk2O6F6D9AEzNLijG+c
# Oe5Ef2F5l4ZsVSkLFCI5jELC+r4KnNZjunc+qvjSz2DkNsXfrjFhyk+K7v7U7+JF
# Z8kZ58yFuxEX0cxDKpJLxiNh/ODCOL2UxYkhyfI3AR0EhfxX9QZHVgxyZwnavR35
# FxqLSiGTeAJsK7YN3bIxyuP6eCcnkX8TMdpu9kPD97sHnM7po0UQDrjaN7etviLD
# xnax2nemdvJW3BewOLFrD1nSnd7ZHdPGPB3oWTCaK9/3XwQERLi3Xj+HZc89RP50
# Nt7h7+3G6oq2kXYNidI9iWd+gL+lvkQZH9YTIfBCLWjvuXvUUUU+AvFI00Utqrvd
# rIdqCFaqE9HHQgSfXeQ53xLWdMCztUP/YnMXiJxNBkc6UE2px/o6+/LXJDIpwIXR
# 4HSodLfkfsNQl6FFrJ1xsOYGSHvcFkH8389RmUvrjr1NBbdesc4Bu4kox+3cabOZ
# c1zm89G+1RRL2tReFzSMlYSGO3iKn3GGXmQiRmFlBb3CpbUVQz+fgxVMfeL0j4Lm
# KQfT1jIxggPUMIID0AIBATB4MGExCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBQdWJsaWMgUlNB
# IFRpbWVzdGFtcGluZyBDQSAyMDIwAhMzAAAARhfkdXrK/drlAAAAAABGMA0GCWCG
# SAFlAwQCAQUAoIIBLTAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZI
# hvcNAQkEMSIEIHgwQkiMhul6IrfEKmPaCFR+R91oZOlPqVgP/9PPcfn+MIHdBgsq
# hkiG9w0BCRACLzGBzTCByjCBxzCBoAQgEid2SJpUPj5xQm73M4vqDmVh1QR6TiuT
# UVkL3P8Wis4wfDBlpGMwYTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGlt
# ZXN0YW1waW5nIENBIDIwMjACEzMAAABGF+R1esr92uUAAAAAAEYwIgQgVp6I1YBM
# Mni0rCuD57vEK/tzWZypHqWFikWLFVY11RwwDQYJKoZIhvcNAQELBQAEggIAnRBH
# voM5+wbJp+aOwrrL8fi8Rv/eFV820Nhr+jMny73UscN60OWdcdcZDbjDlnDX1KEP
# sNcEOFvaruHHrF4kDK8N0yemElNz63IgqhUoGoXXQKT2RgVg7T/kiQJH7zuaEjgB
# YNniAZdXXJJ1C+uv2ZQzkGIEVIEA6pB5/xo4kFhrfkOrdGzqL8HXT/RZQDMn5Uzk
# W+Sl2JmsyYBS4sgI9Ay3qT5nv+frzngbWlqx1dre21uj37Fgk5mWHJEdmY1nqTTd
# 25j6oDLGPC8AS9wtgZBXggemKAXwyeOFFahXUFN7X7cbwTALy5aWjE/rqp+N5J7M
# +YApl3aknUZ13KTXz9pfAF0uhmZimngvBHjijyctleF8HUP2RNAhS/l68OqW7oKi
# Dqvb7tSHJbcnYkxo7dUq6ppfN51ah61ZsyMVG6SaH015+5QO1k50ohXcFff2GOuZ
# d3Z9JOoAjIkeiVTNeRlPDlHtS0CSYu4ZKsWsst+0VY2R9rJBeoii9Xa0oiIggkYL
# 1pHAPH0B1uLlvFcI6B+fAXe0OiCJodbO5lk8ZpvCG5WWYbjzp2c3B8PZGSBgEpSf
# KYlVavvBAvaJCORUO7j8PyzzDINuzQorP9+i399ORjOnqeC92Cb0V12LcoqqtJaf
# 7oSB86VOI0lfHnPUlLWvoiLHrFR5PsYkltOuPqU=
# SIG # End signature block

@ -1,76 +0,0 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past locations. Without forgetting
# past locations the $PATH changes we made may not be respected.
# See "man bash" for more details. hash is usually a builtin of your shell
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
case "$(uname)" in
CYGWIN*|MSYS*|MINGW*)
# transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
# and to /cygdrive/d/path/to/venv on Cygwin
VIRTUAL_ENV=$(cygpath 'D:\pycharm\djangoProject\DjangoBlog-master\.venv')
export VIRTUAL_ENV
;;
*)
# use the path as-is
export VIRTUAL_ENV='D:\pycharm\djangoProject\DjangoBlog-master\.venv'
;;
esac
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"Scripts":$PATH"
export PATH
VIRTUAL_ENV_PROMPT='(.venv) '
export VIRTUAL_ENV_PROMPT
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1="("'(.venv) '") ${PS1:-}"
export PS1
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

@ -1,34 +0,0 @@
@echo off
rem This file is UTF-8 encoded, so we need to update the current code page while executing it
for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do (
set _OLD_CODEPAGE=%%a
)
if defined _OLD_CODEPAGE (
"%SystemRoot%\System32\chcp.com" 65001 > nul
)
set "VIRTUAL_ENV=D:\pycharm\djangoProject\DjangoBlog-master\.venv"
if not defined PROMPT set PROMPT=$P$G
if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT%
if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%
set _OLD_VIRTUAL_PROMPT=%PROMPT%
set PROMPT=(.venv) %PROMPT%
if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%
set PYTHONHOME=
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%"
set "VIRTUAL_ENV_PROMPT=(.venv) "
:END
if defined _OLD_CODEPAGE (
"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
set _OLD_CODEPAGE=
)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

@ -1,22 +0,0 @@
@echo off
if defined _OLD_VIRTUAL_PROMPT (
set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
)
set _OLD_VIRTUAL_PROMPT=
if defined _OLD_VIRTUAL_PYTHONHOME (
set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%"
set _OLD_VIRTUAL_PYTHONHOME=
)
if defined _OLD_VIRTUAL_PATH (
set "PATH=%_OLD_VIRTUAL_PATH%"
)
set _OLD_VIRTUAL_PATH=
set VIRTUAL_ENV=
set VIRTUAL_ENV_PROMPT=
:END

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,5 +0,0 @@
home = C:\Users\86137\AppData\Local\Programs\Python\Python312
include-system-site-packages = false
version = 3.12.10
executable = C:\Users\86137\AppData\Local\Programs\Python\Python312\python.exe
command = C:\Users\86137\AppData\Local\Programs\Python\Python312\python.exe -m venv D:\pycharm\djangoProject\DjangoBlog-master\.venv

@ -47,6 +47,13 @@ class BlogUserChangeForm(UserChangeForm):
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
# Django 5.x 兼容性覆盖add_fieldsets以排除usable_password字段
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
list_display = (
'id',
'nickname',
@ -57,3 +64,4 @@ class BlogUserAdmin(UserAdmin):
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')

@ -6,9 +6,9 @@ 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
#zhq: 文章表单类 - 可用于定制文章编辑表单
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
@ -16,29 +16,29 @@ class ArticleForm(forms.ModelForm):
model = Article
fields = '__all__'
#zhq: Admin动作函数 - 批量发布文章
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
#zhq: Admin动作函数 - 批量设为草稿
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
#zhq: Admin动作函数 - 批量关闭评论
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
#zhq: Admin动作函数 - 批量开启评论
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
#zhq: 设置动作的描述信息
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
#zhq: 文章管理类 - 定制文章在Admin中的显示和行为
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
@ -52,9 +52,11 @@ class ArticlelAdmin(admin.ModelAdmin):
'views',
'status',
'type',
'article_order')
'article_order',
'version_count') # 添加版本数量显示
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
@ -62,16 +64,30 @@ class ArticlelAdmin(admin.ModelAdmin):
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
open_article_commentstatus] #zhq: 注册批量动作
raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
# zhq: 自定义方法,生成分类的管理后台链接
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def version_count(self, obj):
"""显示文章版本数量和链接"""
from blog.models_version import ArticleVersion
count = ArticleVersion.objects.filter(article=obj).count()
if count > 0:
url = reverse('admin:blog_articleversion_changelist') + f'?article__id__exact={obj.id}'
return format_html('<a href="{}">{} 个版本</a>', url, count)
return '无版本'
version_count.short_description = _('Versions')
def get_form(self, request, obj=None, **kwargs):
# zhq: 限制作者字段只能选择超级用户
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
@ -81,6 +97,7 @@ class ArticlelAdmin(admin.ModelAdmin):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
# zhq: 获取文章在前台的访问链接
if obj:
url = obj.get_full_url()
return url
@ -89,24 +106,37 @@ class ArticlelAdmin(admin.ModelAdmin):
site = get_current_site().domain
return site
class Media:
"""引入草稿自动保存的 JavaScript"""
js = ('blog/js/article-draft-autosave.js',)
#zhq: 标签管理类 - 简化管理界面
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
#zhq: 分类管理类 - 显示父级分类信息
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
#zhq: 友情链接管理类
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
#zhq: 侧边栏管理类
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
#zhq: 博客设置管理类 - 使用默认管理界面
class BlogSettingsAdmin(admin.ModelAdmin):
pass
# 导入文章版本管理(导入模块以触发装饰器注册)
import blog.admin_version
# 导入文章草稿管理(导入模块以触发装饰器注册)
import blog.admin_draft
# 导入社交功能管理(导入模块以触发装饰器注册)
import blog.admin_social
# 导入多媒体管理(导入模块以触发装饰器注册)
import blog.admin_media

@ -0,0 +1,110 @@
# 文章草稿 Admin 管理界面
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from blog.models_draft import ArticleDraft
@admin.register(ArticleDraft)
class ArticleDraftAdmin(admin.ModelAdmin):
"""文章草稿管理界面"""
list_display = [
'id',
'title_display',
'author',
'article_link',
'preview',
'last_update_time',
'is_published',
'action_buttons'
]
list_filter = [
'is_published',
'author',
'last_update_time',
'creation_time'
]
search_fields = [
'title',
'body',
'author__username'
]
readonly_fields = [
'author',
'article',
'creation_time',
'last_update_time',
'session_id'
]
fields = [
'author',
'article',
'title',
'body',
'category_id',
'tags_data',
'status',
'comment_status',
'type',
'session_id',
'is_published',
'creation_time',
'last_update_time'
]
list_per_page = 50
def has_add_permission(self, request):
"""禁止手动添加草稿"""
return False
def title_display(self, obj):
"""显示标题"""
title = obj.title or '(无标题)'
if len(title) > 40:
return title[:37] + '...'
return title
title_display.short_description = '标题'
def article_link(self, obj):
"""文章链接"""
if obj.article:
url = reverse('admin:blog_article_change', args=[obj.article.id])
return format_html('<a href="{}">{}</a>', url, obj.article.title)
return '-'
article_link.short_description = '文章'
def preview(self, obj):
"""预览文本"""
preview_text = obj.get_preview_text(30)
return format_html('<span style="color: #666;">{}</span>', preview_text)
preview.short_description = '预览'
def action_buttons(self, obj):
"""操作按钮"""
if not obj.is_published:
apply_url = f'/blog/api/draft/apply/'
delete_url = f'/blog/api/draft/delete/'
return format_html(
'<button class="button" onclick="applyDraft({})">应用到文章</button> '
'<button class="button" onclick="deleteDraft({})">删除</button>',
obj.id, obj.id
)
return '已发布'
action_buttons.short_description = '操作'
class Media:
js = ('admin/js/draft_actions.js',)

@ -0,0 +1,210 @@
# 多媒体管理 Admin 界面
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from blog.models_media import MediaFile, MediaFolder, MediaFileFolder
@admin.register(MediaFile)
class MediaFileAdmin(admin.ModelAdmin):
"""媒体文件管理界面"""
list_display = [
'id',
'thumbnail_preview',
'original_filename',
'file_type',
'file_size_display',
'uploader_link',
'upload_time',
'reference_count',
'is_public'
]
list_filter = [
'file_type',
'is_public',
'upload_time'
]
search_fields = [
'original_filename',
'description',
'uploader__username'
]
readonly_fields = [
'stored_filename',
'file_hash',
'file_size',
'mime_type',
'file_path',
'thumbnail_path',
'width',
'height',
'upload_time',
'thumbnail_preview_large'
]
fieldsets = (
('基本信息', {
'fields': ('original_filename', 'file_type', 'description', 'is_public')
}),
('文件详情', {
'fields': ('stored_filename', 'file_size', 'file_hash', 'mime_type', 'file_path')
}),
('图片信息', {
'fields': ('width', 'height', 'thumbnail_path', 'thumbnail_preview_large'),
'classes': ('collapse',)
}),
('用户信息', {
'fields': ('uploader', 'upload_time', 'reference_count')
}),
)
list_per_page = 50
def thumbnail_preview(self, obj):
"""缩略图预览(列表)"""
if obj.is_image():
return format_html(
'<img src="{}" style="max-width: 50px; max-height: 50px; object-fit: cover;" />',
obj.get_thumbnail_url()
)
return '📄'
thumbnail_preview.short_description = '预览'
def thumbnail_preview_large(self, obj):
"""缩略图预览(详情)"""
if obj.is_image():
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px;" />',
obj.get_absolute_url()
)
return '非图片文件'
thumbnail_preview_large.short_description = '图片预览'
def file_size_display(self, obj):
"""文件大小显示"""
size = obj.file_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
file_size_display.short_description = '文件大小'
def uploader_link(self, obj):
"""上传者链接"""
if obj.uploader:
url = reverse('admin:accounts_bloguser_change', args=[obj.uploader.id])
return format_html('<a href="{}">{}</a>', url, obj.uploader.username)
return '-'
uploader_link.short_description = '上传者'
def has_add_permission(self, request):
"""禁止手动添加(应通过上传功能)"""
return False
@admin.register(MediaFolder)
class MediaFolderAdmin(admin.ModelAdmin):
"""媒体文件夹管理界面"""
list_display = [
'id',
'name',
'full_path_display',
'owner_link',
'files_count_display',
'created_time'
]
list_filter = [
'created_time'
]
search_fields = [
'name',
'description',
'owner__username'
]
readonly_fields = [
'created_time',
'full_path_display'
]
fieldsets = (
('文件夹信息', {
'fields': ('name', 'parent', 'owner', 'description')
}),
('详细信息', {
'fields': ('created_time', 'full_path_display')
}),
)
def full_path_display(self, obj):
"""完整路径显示"""
return obj.get_full_path()
full_path_display.short_description = '完整路径'
def owner_link(self, obj):
"""所有者链接"""
url = reverse('admin:accounts_bloguser_change', args=[obj.owner.id])
return format_html('<a href="{}">{}</a>', url, obj.owner.username)
owner_link.short_description = '所有者'
def files_count_display(self, obj):
"""文件数量显示"""
return obj.file_relations.count()
files_count_display.short_description = '文件数量'
@admin.register(MediaFileFolder)
class MediaFileFolderAdmin(admin.ModelAdmin):
"""媒体文件-文件夹关联管理界面"""
list_display = [
'id',
'file_link',
'folder_link',
'added_time'
]
list_filter = [
'added_time'
]
search_fields = [
'file__original_filename',
'folder__name'
]
readonly_fields = [
'added_time'
]
def file_link(self, obj):
"""文件链接"""
url = reverse('admin:blog_mediafile_change', args=[obj.file.id])
return format_html('<a href="{}">{}</a>', url, obj.file.original_filename)
file_link.short_description = '文件'
def folder_link(self, obj):
"""文件夹链接"""
url = reverse('admin:blog_mediafolder_change', args=[obj.folder.id])
return format_html('<a href="{}">{}</a>', url, obj.folder.name)
folder_link.short_description = '文件夹'

@ -0,0 +1,168 @@
# 用户关注和收藏 Admin 管理界面
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from blog.models_social import UserFollow, ArticleFavorite, ArticleLike
@admin.register(UserFollow)
class UserFollowAdmin(admin.ModelAdmin):
"""用户关注管理界面"""
list_display = [
'id',
'follower_link',
'following_link',
'creation_time'
]
list_filter = [
'creation_time'
]
search_fields = [
'follower__username',
'following__username'
]
readonly_fields = [
'follower',
'following',
'creation_time'
]
list_per_page = 50
def has_add_permission(self, request):
"""禁止手动添加"""
return False
def follower_link(self, obj):
"""关注者链接"""
url = reverse('admin:accounts_bloguser_change', args=[obj.follower.id])
return format_html('<a href="{}">{}</a>', url, obj.follower.username)
follower_link.short_description = '关注者'
def following_link(self, obj):
"""被关注者链接"""
url = reverse('admin:accounts_bloguser_change', args=[obj.following.id])
return format_html('<a href="{}">{}</a>', url, obj.following.username)
following_link.short_description = '被关注者'
@admin.register(ArticleFavorite)
class ArticleFavoriteAdmin(admin.ModelAdmin):
"""文章收藏管理界面"""
list_display = [
'id',
'user_link',
'article_link',
'note_display',
'creation_time'
]
list_filter = [
'creation_time'
]
search_fields = [
'user__username',
'article__title',
'note'
]
readonly_fields = [
'user',
'article',
'creation_time'
]
list_per_page = 50
def has_add_permission(self, request):
"""禁止手动添加"""
return False
def user_link(self, obj):
"""用户链接"""
url = reverse('admin:accounts_bloguser_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = '用户'
def article_link(self, obj):
"""文章链接"""
url = reverse('admin:blog_article_change', args=[obj.article.id])
title = obj.article.title
if len(title) > 50:
title = title[:47] + '...'
return format_html('<a href="{}">{}</a>', url, title)
article_link.short_description = '文章'
def note_display(self, obj):
"""显示备注"""
if obj.note:
if len(obj.note) > 30:
return obj.note[:27] + '...'
return obj.note
return '-'
note_display.short_description = '备注'
@admin.register(ArticleLike)
class ArticleLikeAdmin(admin.ModelAdmin):
"""文章点赞管理界面"""
list_display = [
'id',
'user_link',
'article_link',
'creation_time'
]
list_filter = [
'creation_time'
]
search_fields = [
'user__username',
'article__title'
]
readonly_fields = [
'user',
'article',
'creation_time'
]
list_per_page = 50
def has_add_permission(self, request):
"""禁止手动添加"""
return False
def user_link(self, obj):
"""用户链接"""
url = reverse('admin:accounts_bloguser_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = '用户'
def article_link(self, obj):
"""文章链接"""
url = reverse('admin:blog_article_change', args=[obj.article.id])
title = obj.article.title
if len(title) > 50:
title = title[:47] + '...'
return format_html('<a href="{}">{}</a>', url, title)
article_link.short_description = '文章'

@ -0,0 +1,217 @@
# 文章版本管理 Admin 界面
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.shortcuts import redirect, render
from django.contrib import messages
from django.template.response import TemplateResponse
from blog.models_version import ArticleVersion
@admin.register(ArticleVersion)
class ArticleVersionAdmin(admin.ModelAdmin):
"""文章版本管理界面"""
list_display = [
'version_number',
'article_link',
'title_display',
'created_by',
'creation_time',
'change_summary_display',
'is_auto_save',
'action_buttons'
]
list_filter = [
'is_auto_save',
'creation_time',
'created_by'
]
search_fields = [
'title',
'change_summary',
'article__title'
]
readonly_fields = [
'article',
'version_number',
'title',
'body',
'pub_time',
'status',
'comment_status',
'type',
'category_id',
'category_name',
'created_by',
'creation_time',
'is_auto_save'
]
fields = [
'article',
'version_number',
'title',
'body',
'pub_time',
'status',
'comment_status',
'type',
'category_name',
'created_by',
'creation_time',
'change_summary',
'is_auto_save'
]
list_per_page = 50
def has_add_permission(self, request):
"""禁止手动添加版本"""
return False
def has_delete_permission(self, request, obj=None):
"""禁止删除版本(保持历史完整性)"""
return False
def article_link(self, obj):
"""文章链接"""
url = reverse('admin:blog_article_change', args=[obj.article.id])
return format_html('<a href="{}">{}</a>', url, obj.article.title)
article_link.short_description = '文章'
def title_display(self, obj):
"""显示标题(截断)"""
if len(obj.title) > 50:
return obj.title[:47] + '...'
return obj.title
title_display.short_description = '标题'
def change_summary_display(self, obj):
"""显示变更说明"""
if obj.change_summary:
return obj.change_summary
return format_html('<span style="color: #999;">自动保存</span>')
change_summary_display.short_description = '变更说明'
def action_buttons(self, obj):
"""操作按钮"""
restore_url = reverse('admin:restore_article_version', args=[obj.id])
compare_url = reverse('admin:compare_article_version', args=[obj.id])
return format_html(
'<a class="button" href="{}">恢复此版本</a> '
'<a class="button" href="{}">对比</a>',
restore_url,
compare_url
)
action_buttons.short_description = '操作'
def get_urls(self):
"""添加自定义URL"""
from django.urls import path
urls = super().get_urls()
custom_urls = [
path(
'<int:version_id>/restore/',
self.admin_site.admin_view(self.restore_version),
name='restore_article_version',
),
path(
'<int:version_id>/compare/',
self.admin_site.admin_view(self.compare_version),
name='compare_article_version',
),
]
return custom_urls + urls
def restore_version(self, request, version_id):
"""恢复版本"""
try:
version = ArticleVersion.objects.get(id=version_id)
except ArticleVersion.DoesNotExist:
messages.error(request, '版本不存在')
return redirect('admin:blog_articleversion_changelist')
if request.method == 'POST':
# 在恢复前先保存当前版本
ArticleVersion.create_version(
article=version.article,
user=request.user,
change_summary=f'恢复前的版本(准备恢复到 v{version.version_number}',
is_auto_save=False
)
if version.restore_to_article():
messages.success(
request,
f'已成功将文章恢复到版本 v{version.version_number}'
)
return redirect('admin:blog_article_change', version.article.id)
else:
messages.error(request, '恢复版本失败')
return redirect('admin:blog_articleversion_changelist')
# 显示确认页面
context = {
**self.admin_site.each_context(request),
'version': version,
'current': version.article,
'opts': self.model._meta,
'title': f'恢复版本 v{version.version_number}',
}
return TemplateResponse(
request,
'admin/blog/articleversion/restore_confirmation.html',
context
)
def compare_version(self, request, version_id):
"""对比版本"""
try:
version = ArticleVersion.objects.get(id=version_id)
except ArticleVersion.DoesNotExist:
messages.error(request, '版本不存在')
return redirect('admin:blog_articleversion_changelist')
current = version.article
diff = version.get_diff_with_current()
# 生成正文差异(如果有变化)
body_diff_html = None
if diff['body_changed']:
import difflib
d = difflib.HtmlDiff()
body_diff_html = d.make_table(
version.body.splitlines(),
current.body.splitlines(),
f'版本 v{version.version_number}',
'当前版本',
context=True,
numlines=3
)
context = {
**self.admin_site.each_context(request),
'version': version,
'current': current,
'diff': diff,
'body_diff_html': body_diff_html,
'opts': self.model._meta,
'title': f'对比版本 v{version.version_number}',
}
return TemplateResponse(
request,
'admin/blog/articleversion/compare.html',
context
)

@ -1,5 +1,20 @@
from django.apps import AppConfig
#zhq: 博客应用配置类 - 继承自Django的AppConfig基类
class BlogConfig(AppConfig):
name = 'blog'
name = 'blog' #zhq: 应用名称对应INSTALLED_APPS中的名称
default_auto_field = 'django.db.models.BigAutoField'
def ready(self):
"""
应用启动时执行的初始化操作
导入所有Admin模块以确保它们被注册
"""
# 导入Admin模块以触发注册
try:
from blog import admin_version # noqa
from blog import admin_draft # noqa
from blog import admin_social # noqa
from blog import admin_media # noqa
except ImportError:
pass

@ -7,7 +7,7 @@ from .models import Category, Article
logger = logging.getLogger(__name__)
#zhq: SEO上下文处理器 - 为所有模板提供SEO相关变量和导航数据
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
@ -15,29 +15,32 @@ def seo_processor(requests):
return value
else:
logger.info('set processor cache.')
# zhq: 获取博客全局设置
setting = get_blog_setting()
# zhq: 构建上下文数据字典
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'SITE_NAME': setting.site_name,#zhq: 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,#zhq: 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,#zhq: Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description,#zhq: SEO描述
'SITE_DESCRIPTION': setting.site_description,#zhq: 网站描述
'SITE_KEYWORDS': setting.site_keywords,#zhq: 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',#zhq: 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length,#zhq: 文章摘要长度
'nav_category_list': Category.objects.all(),#zhq: 导航分类列表
'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,
type='p',#zhq: 页面类型
status='p'),#zhq: 发布状态
'OPEN_SITE_COMMENT': setting.open_site_comment,#zhq: 是否开启全站评论
'BEIAN_CODE': setting.beian_code,#zhq: ICP备案号
'ANALYTICS_CODE': setting.analytics_code,#zhq: 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,#zhq: 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code,#zhq: 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year,#zhq: 当前年份
"GLOBAL_HEADER": setting.global_header,#zhq: 全局头部HTML
"GLOBAL_FOOTER": setting.global_footer,#zhq: 全局尾部HTML
"COMMENT_NEED_REVIEW": setting.comment_need_review,#zhq: 评论是否需要审核
}
# zhq: 缓存上下文数据10小时提高性能
cache.set(key, value, 60 * 60 * 10)
return value

@ -5,15 +5,18 @@ from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
#zhq: 博客搜索表单类 - 继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self):
#zhq: 执行搜索操作调用父类的search方法
datas = super(BlogSearchForm, self).search()
# zhq: 如果表单验证失败,返回空结果
if not self.is_valid():
return self.no_query_found()
# zhq: 记录搜索关键词到日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -0,0 +1,90 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand):
help = 'Initialize canteen ordering themed test data'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='canteen@test.com', username='食堂管理员', password=make_password('canteen!Q@W#2025'))[0]
# 顶级分类:校园食堂
canteen_category = Category.objects.get_or_create(
name='校园食堂', parent_category=None, defaults={"index": 100}
)[0]
# 二级分类:各食堂
east_canteen = Category.objects.get_or_create(
name='东区食堂', parent_category=canteen_category, defaults={"index": 90}
)[0]
west_canteen = Category.objects.get_or_create(
name='西区食堂', parent_category=canteen_category, defaults={"index": 80}
)[0]
# 三级分类:档口
stalls = [
Category.objects.get_or_create(name='川味档口', parent_category=east_canteen, defaults={"index": 70})[0],
Category.objects.get_or_create(name='面点档口', parent_category=east_canteen, defaults={"index": 60})[0],
Category.objects.get_or_create(name='简餐档口', parent_category=west_canteen, defaults={"index": 50})[0],
Category.objects.get_or_create(name='风味小吃', parent_category=west_canteen, defaults={"index": 40})[0],
]
# 标签:口味与饮食偏好
base_tags = [
Tag.objects.get_or_create(name='辣味')[0],
Tag.objects.get_or_create(name='清淡')[0],
Tag.objects.get_or_create(name='无辣')[0],
Tag.objects.get_or_create(name='主食')[0],
Tag.objects.get_or_create(name='素食可选')[0],
]
# 示例菜品以文章承载:标题为菜名,正文为描述/价格
dish_specs = [
("宫保鸡丁套餐", "包含米饭+素菜价格¥18可加辣"),
("红烧牛肉面", "大份牛肉足量价格¥22清淡口可选"),
("番茄鸡蛋盖浇饭", "酸甜开胃价格¥15老少皆宜"),
("麻辣香锅", "自选配菜称重计价约¥20-30/份"),
("素什锦炒面", "少油少盐价格¥14素食友好"),
("鸡排饭", "现炸鸡排+配菜价格¥19无辣版本可选"),
("牛肉卷饼", "现烙饼皮价格¥12可加蛋+2"),
("酸辣粉", "地道红薯粉价格¥13可微辣/中特/特辣"),
]
tag_cycle = base_tags * 4
idx = 0
for stall in stalls:
for dish_title, dish_body in dish_specs:
article = Article.objects.get_or_create(
category=stall,
title=f'{stall.name}-{dish_title}',
defaults={
'body': f'{dish_body}\n\n口味标签:{tag_cycle[idx % len(tag_cycle)].name}',
'author': user,
'status': 'p',
'comment_status': 'o',
'type': 'a',
'article_order': 0,
'show_toc': False,
}
)[0]
# 绑定两个标签
article.tags.add(tag_cycle[idx % len(tag_cycle)])
article.tags.add(tag_cycle[(idx + 1) % len(tag_cycle)])
article.save()
idx += 1
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('Initialized canteen ordering themed test data.'))

@ -9,6 +9,7 @@ from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
#zhq: 在线中间件类 - 用于监控页面加载时间和用户访问信息
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
@ -16,16 +17,20 @@ class OnlineMiddleware(object):
def __call__(self, request):
''' page render time '''
# zhq: 记录请求开始时间
start_time = time.time()
response = self.get_response(request)
#zhq: 获取客户端IP地址和User-Agent信息
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:
#zhq: 计算页面渲染时间
cast_time = time.time() - start_time
# zhq: 如果启用了Elasticsearch记录性能数据
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
time_taken = round((cast_time) * 1000, 2) #zhq: 转换为毫秒并保留2位小数
url = request.path
from django.utils import timezone
ElaspedTimeDocumentManager.create(
@ -34,8 +39,9 @@ class OnlineMiddleware(object):
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# zhq: 在响应内容中替换加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) #zhq: 保留前5位字符
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)

@ -0,0 +1,45 @@
# 性能优化:为常用查询字段添加数据库索引
# Generated manually for performance optimization
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
# 为 Article 模型添加索引
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['status', 'type'], name='article_status_type_idx'),
),
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['status', 'pub_time'], name='article_status_pubtime_idx'),
),
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['-pub_time'], name='article_pubtime_desc_idx'),
),
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['article_order', '-pub_time'], name='article_order_pubtime_idx'),
),
# 为 Category 添加 slug 索引用于URL查找
migrations.AlterField(
model_name='category',
name='slug',
field=models.SlugField(default='no-slug', max_length=60, blank=True, db_index=True),
),
# 为 Tag 添加 slug 索引用于URL查找
migrations.AlterField(
model_name='tag',
name='slug',
field=models.SlugField(default='no-slug', max_length=60, blank=True, db_index=True),
),
]

@ -0,0 +1,55 @@
# 文章版本管理功能 - 数据库迁移
# Generated manually for article version management feature
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0002_add_performance_indexes'),
]
operations = [
migrations.CreateModel(
name='ArticleVersion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version_number', models.PositiveIntegerField(default=1, verbose_name='version number')),
('title', models.CharField(max_length=200, verbose_name='title')),
('body', models.TextField(verbose_name='body')),
('pub_time', models.DateTimeField(verbose_name='publish time')),
('status', models.CharField(max_length=1, verbose_name='status')),
('comment_status', models.CharField(max_length=1, verbose_name='comment status')),
('type', models.CharField(max_length=1, verbose_name='type')),
('category_id', models.IntegerField(verbose_name='category id')),
('category_name', models.CharField(max_length=30, verbose_name='category name')),
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
('change_summary', models.CharField(blank=True, default='', max_length=200, verbose_name='change summary')),
('is_auto_save', models.BooleanField(default=True, verbose_name='is auto save')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='blog.Article', verbose_name='article')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_versions_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
],
options={
'verbose_name': 'article version',
'verbose_name_plural': 'article versions',
'ordering': ['-version_number'],
},
),
migrations.AddIndex(
model_name='articleversion',
index=models.Index(fields=['article', '-version_number'], name='version_article_num_idx'),
),
migrations.AddIndex(
model_name='articleversion',
index=models.Index(fields=['creation_time'], name='version_creation_time_idx'),
),
migrations.AlterUniqueTogether(
name='articleversion',
unique_together={('article', 'version_number')},
),
]

@ -0,0 +1,54 @@
# 文章草稿功能 - 数据库迁移
# Generated manually for article draft auto-save feature
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0003_add_article_version'),
]
operations = [
migrations.CreateModel(
name='ArticleDraft',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, default='', max_length=200, verbose_name='title')),
('body', models.TextField(blank=True, default='', verbose_name='body')),
('category_id', models.IntegerField(blank=True, null=True, verbose_name='category id')),
('tags_data', models.JSONField(blank=True, default=list, verbose_name='tags data')),
('status', models.CharField(default='d', max_length=1, verbose_name='status')),
('comment_status', models.CharField(default='o', max_length=1, verbose_name='comment status')),
('type', models.CharField(default='a', max_length=1, verbose_name='type')),
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
('last_update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='last update time')),
('session_id', models.CharField(blank=True, default='', max_length=64, verbose_name='session id')),
('is_published', models.BooleanField(default=False, verbose_name='is published')),
('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='drafts', to='blog.Article', verbose_name='article')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_drafts', to=settings.AUTH_USER_MODEL, verbose_name='author')),
],
options={
'verbose_name': 'article draft',
'verbose_name_plural': 'article drafts',
'ordering': ['-last_update_time'],
},
),
migrations.AddIndex(
model_name='articledraft',
index=models.Index(fields=['author', '-last_update_time'], name='draft_author_time_idx'),
),
migrations.AddIndex(
model_name='articledraft',
index=models.Index(fields=['article', '-last_update_time'], name='draft_article_time_idx'),
),
migrations.AddIndex(
model_name='articledraft',
index=models.Index(fields=['is_published', '-last_update_time'], name='draft_published_time_idx'),
),
]

@ -0,0 +1,78 @@
# 用户关注和收藏功能 - 数据库迁移
# Generated manually for social features
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_add_article_draft'),
]
operations = [
# 创建用户关注模型
migrations.CreateModel(
name='UserFollow',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='follower')),
('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL, verbose_name='following')),
],
options={
'verbose_name': 'user follow',
'verbose_name_plural': 'user follows',
'ordering': ['-creation_time'],
},
),
# 创建文章收藏模型
migrations.CreateModel(
name='ArticleFavorite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
('note', models.CharField(blank=True, default='', max_length=200, verbose_name='note')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='blog.Article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'article favorite',
'verbose_name_plural': 'article favorites',
'ordering': ['-creation_time'],
},
),
# 添加索引
migrations.AddIndex(
model_name='userfollow',
index=models.Index(fields=['follower', '-creation_time'], name='follow_follower_time_idx'),
),
migrations.AddIndex(
model_name='userfollow',
index=models.Index(fields=['following', '-creation_time'], name='follow_following_time_idx'),
),
migrations.AddIndex(
model_name='articlefavorite',
index=models.Index(fields=['user', '-creation_time'], name='favorite_user_time_idx'),
),
migrations.AddIndex(
model_name='articlefavorite',
index=models.Index(fields=['article', '-creation_time'], name='favorite_article_time_idx'),
),
# 添加唯一约束
migrations.AlterUniqueTogether(
name='userfollow',
unique_together={('follower', 'following')},
),
migrations.AlterUniqueTogether(
name='articlefavorite',
unique_together={('user', 'article')},
),
]

@ -0,0 +1,43 @@
# Generated migration for ArticleLike model
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_add_social_features'),
]
operations = [
migrations.CreateModel(
name='ArticleLike',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_by', to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'article like',
'verbose_name_plural': 'article likes',
'ordering': ['-creation_time'],
},
),
migrations.AddIndex(
model_name='articlelike',
index=models.Index(fields=['user', '-creation_time'], name='like_user_time_idx'),
),
migrations.AddIndex(
model_name='articlelike',
index=models.Index(fields=['article', '-creation_time'], name='like_article_time_idx'),
),
migrations.AlterUniqueTogether(
name='articlelike',
unique_together={('user', 'article')},
),
]

@ -0,0 +1,101 @@
# Generated migration for Media Management System
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0006_add_article_like'),
]
operations = [
# 创建 MediaFile 模型
migrations.CreateModel(
name='MediaFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('original_filename', models.CharField(max_length=255, verbose_name='original filename')),
('stored_filename', models.CharField(max_length=255, unique=True, verbose_name='stored filename')),
('file_type', models.CharField(choices=[('image', 'Image'), ('file', 'File')], max_length=10, verbose_name='file type')),
('file_size', models.BigIntegerField(verbose_name='file size')),
('file_hash', models.CharField(db_index=True, max_length=32, verbose_name='file hash')),
('mime_type', models.CharField(max_length=100, verbose_name='MIME type')),
('file_path', models.CharField(max_length=500, verbose_name='file path')),
('upload_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='upload time')),
('width', models.IntegerField(blank=True, null=True, verbose_name='width')),
('height', models.IntegerField(blank=True, null=True, verbose_name='height')),
('thumbnail_path', models.CharField(blank=True, max_length=500, verbose_name='thumbnail path')),
('description', models.TextField(blank=True, verbose_name='description')),
('is_public', models.BooleanField(default=True, verbose_name='is public')),
('reference_count', models.IntegerField(default=0, verbose_name='reference count')),
('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_files', to=settings.AUTH_USER_MODEL, verbose_name='uploader')),
],
options={
'verbose_name': 'media file',
'verbose_name_plural': 'media files',
'ordering': ['-upload_time'],
},
),
# 创建 MediaFolder 模型
migrations.CreateModel(
name='MediaFolder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='folder name')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created time')),
('description', models.TextField(blank=True, verbose_name='description')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_folders', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subfolders', to='blog.mediafolder', verbose_name='parent folder')),
],
options={
'verbose_name': 'media folder',
'verbose_name_plural': 'media folders',
'ordering': ['name'],
},
),
# 创建 MediaFileFolder 模型
migrations.CreateModel(
name='MediaFileFolder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('added_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='added time')),
('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_relations', to='blog.mediafile')),
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='file_relations', to='blog.mediafolder')),
],
options={
'verbose_name': 'media file folder relation',
'verbose_name_plural': 'media file folder relations',
},
),
# 添加索引
migrations.AddIndex(
model_name='mediafile',
index=models.Index(fields=['file_type', '-upload_time'], name='media_type_time_idx'),
),
migrations.AddIndex(
model_name='mediafile',
index=models.Index(fields=['uploader', '-upload_time'], name='media_uploader_time_idx'),
),
migrations.AddIndex(
model_name='mediafile',
index=models.Index(fields=['file_hash'], name='media_hash_idx'),
),
# 添加唯一约束
migrations.AlterUniqueTogether(
name='mediafolder',
unique_together={('name', 'parent', 'owner')},
),
migrations.AlterUniqueTogether(
name='mediafilefolder',
unique_together={('file', 'folder')},
),
]

@ -0,0 +1,14 @@
# Generated by Django 5.2.7 on 2025-11-24 02:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
('blog', '0007_add_media_management'),
]
operations = [
]

@ -0,0 +1,359 @@
# Generated by Django 5.2.7 on 2025-11-25 13:02
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0008_merge_20251124_0221'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='articledraft',
options={'ordering': ['-last_update_time'], 'verbose_name': '文章草稿', 'verbose_name_plural': '文章草稿'},
),
migrations.AlterModelOptions(
name='articlefavorite',
options={'ordering': ['-creation_time'], 'verbose_name': '文章收藏', 'verbose_name_plural': '文章收藏'},
),
migrations.AlterModelOptions(
name='articlelike',
options={'ordering': ['-creation_time'], 'verbose_name': '文章点赞', 'verbose_name_plural': '文章点赞'},
),
migrations.AlterModelOptions(
name='articleversion',
options={'ordering': ['-version_number'], 'verbose_name': '文章版本', 'verbose_name_plural': '文章版本'},
),
migrations.AlterModelOptions(
name='mediafile',
options={'ordering': ['-upload_time'], 'verbose_name': '媒体文件', 'verbose_name_plural': '媒体文件'},
),
migrations.AlterModelOptions(
name='mediafilefolder',
options={'verbose_name': '文件-文件夹关联', 'verbose_name_plural': '文件-文件夹关联'},
),
migrations.AlterModelOptions(
name='mediafolder',
options={'ordering': ['name'], 'verbose_name': '媒体文件夹', 'verbose_name_plural': '媒体文件夹'},
),
migrations.AlterModelOptions(
name='userfollow',
options={'ordering': ['-creation_time'], 'verbose_name': '用户关注', 'verbose_name_plural': '用户关注'},
),
migrations.AlterField(
model_name='articledraft',
name='article',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='drafts', to='blog.article', verbose_name='文章'),
),
migrations.AlterField(
model_name='articledraft',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_drafts', to=settings.AUTH_USER_MODEL, verbose_name='作者'),
),
migrations.AlterField(
model_name='articledraft',
name='body',
field=models.TextField(blank=True, default='', verbose_name='正文'),
),
migrations.AlterField(
model_name='articledraft',
name='category_id',
field=models.IntegerField(blank=True, null=True, verbose_name='分类ID'),
),
migrations.AlterField(
model_name='articledraft',
name='comment_status',
field=models.CharField(default='o', max_length=1, verbose_name='评论状态'),
),
migrations.AlterField(
model_name='articledraft',
name='creation_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='articledraft',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='articledraft',
name='is_published',
field=models.BooleanField(default=False, verbose_name='已发布'),
),
migrations.AlterField(
model_name='articledraft',
name='last_update_time',
field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='最后更新时间'),
),
migrations.AlterField(
model_name='articledraft',
name='session_id',
field=models.CharField(blank=True, default='', max_length=64, verbose_name='会话ID'),
),
migrations.AlterField(
model_name='articledraft',
name='status',
field=models.CharField(default='d', max_length=1, verbose_name='状态'),
),
migrations.AlterField(
model_name='articledraft',
name='tags_data',
field=models.JSONField(blank=True, default=list, verbose_name='标签数据'),
),
migrations.AlterField(
model_name='articledraft',
name='title',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='标题'),
),
migrations.AlterField(
model_name='articledraft',
name='type',
field=models.CharField(default='a', max_length=1, verbose_name='类型'),
),
migrations.AlterField(
model_name='articlefavorite',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='blog.article', verbose_name='文章'),
),
migrations.AlterField(
model_name='articlefavorite',
name='creation_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='articlefavorite',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='articlefavorite',
name='note',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='备注'),
),
migrations.AlterField(
model_name='articlefavorite',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
migrations.AlterField(
model_name='articlelike',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_by', to='blog.article', verbose_name='文章'),
),
migrations.AlterField(
model_name='articlelike',
name='creation_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='articlelike',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
migrations.AlterField(
model_name='articleversion',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='blog.article', verbose_name='文章'),
),
migrations.AlterField(
model_name='articleversion',
name='body',
field=models.TextField(verbose_name='正文'),
),
migrations.AlterField(
model_name='articleversion',
name='category_id',
field=models.IntegerField(verbose_name='分类ID'),
),
migrations.AlterField(
model_name='articleversion',
name='category_name',
field=models.CharField(max_length=30, verbose_name='分类名称'),
),
migrations.AlterField(
model_name='articleversion',
name='change_summary',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='变更说明'),
),
migrations.AlterField(
model_name='articleversion',
name='comment_status',
field=models.CharField(max_length=1, verbose_name='评论状态'),
),
migrations.AlterField(
model_name='articleversion',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_versions_created', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
),
migrations.AlterField(
model_name='articleversion',
name='creation_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='articleversion',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='articleversion',
name='is_auto_save',
field=models.BooleanField(default=True, verbose_name='自动保存'),
),
migrations.AlterField(
model_name='articleversion',
name='pub_time',
field=models.DateTimeField(verbose_name='发布时间'),
),
migrations.AlterField(
model_name='articleversion',
name='status',
field=models.CharField(max_length=1, verbose_name='状态'),
),
migrations.AlterField(
model_name='articleversion',
name='title',
field=models.CharField(max_length=200, verbose_name='标题'),
),
migrations.AlterField(
model_name='articleversion',
name='type',
field=models.CharField(max_length=1, verbose_name='类型'),
),
migrations.AlterField(
model_name='articleversion',
name='version_number',
field=models.PositiveIntegerField(default=1, verbose_name='版本号'),
),
migrations.AlterField(
model_name='mediafile',
name='description',
field=models.TextField(blank=True, verbose_name='描述'),
),
migrations.AlterField(
model_name='mediafile',
name='file_hash',
field=models.CharField(db_index=True, max_length=32, verbose_name='文件哈希'),
),
migrations.AlterField(
model_name='mediafile',
name='file_path',
field=models.CharField(max_length=500, verbose_name='文件路径'),
),
migrations.AlterField(
model_name='mediafile',
name='file_size',
field=models.BigIntegerField(verbose_name='文件大小'),
),
migrations.AlterField(
model_name='mediafile',
name='file_type',
field=models.CharField(choices=[('image', '图片'), ('file', '文件')], max_length=10, verbose_name='文件类型'),
),
migrations.AlterField(
model_name='mediafile',
name='height',
field=models.IntegerField(blank=True, null=True, verbose_name='高度'),
),
migrations.AlterField(
model_name='mediafile',
name='is_public',
field=models.BooleanField(default=True, verbose_name='是否公开'),
),
migrations.AlterField(
model_name='mediafile',
name='mime_type',
field=models.CharField(max_length=100, verbose_name='MIME类型'),
),
migrations.AlterField(
model_name='mediafile',
name='original_filename',
field=models.CharField(max_length=255, verbose_name='原始文件名'),
),
migrations.AlterField(
model_name='mediafile',
name='reference_count',
field=models.IntegerField(default=0, verbose_name='引用次数'),
),
migrations.AlterField(
model_name='mediafile',
name='stored_filename',
field=models.CharField(max_length=255, unique=True, verbose_name='存储文件名'),
),
migrations.AlterField(
model_name='mediafile',
name='thumbnail_path',
field=models.CharField(blank=True, max_length=500, verbose_name='缩略图路径'),
),
migrations.AlterField(
model_name='mediafile',
name='upload_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='上传时间'),
),
migrations.AlterField(
model_name='mediafile',
name='uploader',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_files', to=settings.AUTH_USER_MODEL, verbose_name='上传者'),
),
migrations.AlterField(
model_name='mediafile',
name='width',
field=models.IntegerField(blank=True, null=True, verbose_name='宽度'),
),
migrations.AlterField(
model_name='mediafilefolder',
name='added_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='添加时间'),
),
migrations.AlterField(
model_name='mediafolder',
name='created_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='mediafolder',
name='description',
field=models.TextField(blank=True, verbose_name='描述'),
),
migrations.AlterField(
model_name='mediafolder',
name='name',
field=models.CharField(max_length=100, verbose_name='文件夹名称'),
),
migrations.AlterField(
model_name='mediafolder',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_folders', to=settings.AUTH_USER_MODEL, verbose_name='所有者'),
),
migrations.AlterField(
model_name='mediafolder',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subfolders', to='blog.mediafolder', verbose_name='父文件夹'),
),
migrations.AlterField(
model_name='userfollow',
name='creation_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='userfollow',
name='follower',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='关注者'),
),
migrations.AlterField(
model_name='userfollow',
name='following',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL, verbose_name='被关注者'),
),
migrations.AlterField(
model_name='userfollow',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

@ -16,27 +16,28 @@ from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
#zhq: 链接显示类型选择类 - 定义链接在不同页面的显示方式
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
I = ('i', _('index'))#zhq: 首页显示
L = ('l', _('list'))#zhq: 列表页显示
P = ('p', _('post'))#zhq: 文章页显示
A = ('a', _('all'))#zhq: 所有页面显示
S = ('s', _('slide'))#zhq: 幻灯片显示
#zhq: 基础模型抽象类 - 所有模型的基类,包含公共字段和方法
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):
def save(self, *args, **kwargs):#zhq: 特殊处理文章浏览量的更新,避免触发其他字段的保存逻辑
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:
# zhq: 自动生成slug字段用于SEO友好的URL
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
@ -45,6 +46,7 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
# zhq: 获取对象的完整URL地址
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
@ -55,9 +57,10 @@ class BaseModel(models.Model):
@abstractmethod
def get_absolute_url(self):
# zhq: 抽象方法子类必须实现获取绝对URL的方法
pass
#zhq: 文章模型 - 博客的核心内容模型
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = (
@ -112,10 +115,17 @@ class Article(BaseModel):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
ordering = ['-article_order', '-pub_time']#zhq: 按排序和发布时间降序排列
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
# 性能优化:添加组合索引,提升常用查询性能
indexes = [
models.Index(fields=['status', 'type'], name='article_status_type_idx'),
models.Index(fields=['status', 'pub_time'], name='article_status_pubtime_idx'),
models.Index(fields=['-pub_time'], name='article_pubtime_desc_idx'),
models.Index(fields=['article_order', '-pub_time'], name='article_order_pubtime_idx'),
]
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
@ -127,44 +137,77 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
# zhq: 获取文章所属分类的完整树形结构
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):
# 判断是否为新创建的文章
is_new = self.pk is None
# 检查是否需要创建版本(不是只更新浏览量)
should_create_version = not ('update_fields' in kwargs and
kwargs.get('update_fields') == ['views'])
super().save(*args, **kwargs)
# 如果需要创建版本且不是新文章,则创建版本记录
if should_create_version and not is_new:
try:
from blog.models_version import ArticleVersion
# 创建版本记录
ArticleVersion.create_version(
article=self,
user=self.author,
change_summary=kwargs.get('change_summary', ''),
is_auto_save=True
)
except Exception as e:
logger.error(f"Failed to create article version: {e}")
def viewed(self):
# zhq: 增加文章浏览量
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
# zhq: 获取文章的评论列表,带缓存功能
# 性能优化:预加载评论的作者和父评论,避免 N+1 查询
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')
comments = self.comment_set.filter(is_enable=True) \
.select_related('author', 'parent_comment') \
.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):
# zhq: 获取文章在Admin后台的编辑链接
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()
id__gt=self.id, status='p') \
.select_related('author', 'category') \
.order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
# 性能优化:预加载关联对象
return Article.objects.filter(id__lt=self.id, status='p') \
.select_related('author', 'category') \
.first()
def get_first_image_url(self):
"""
@ -176,7 +219,7 @@ class Article(BaseModel):
return match.group(1)
return ""
#zhq: 分类模型 - 支持多级分类结构
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
@ -185,8 +228,8 @@ class Category(BaseModel):
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
on_delete=models.CASCADE) #zhq: 自关联,支持多级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True, db_index=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
@ -239,11 +282,11 @@ class Category(BaseModel):
parse(self)
return categorys
#zhq: 标签模型 - 简单的标签管理
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True, db_index=True)
def __str__(self):
return self.name
@ -253,6 +296,7 @@ class Tag(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
# zhq: 获取该标签下的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
@ -260,32 +304,32 @@ class Tag(BaseModel):
verbose_name = _('tag')
verbose_name_plural = verbose_name
#zhq: 友情链接模型
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
sequence = models.IntegerField(_('order'), unique=True) #zhq: 链接显示顺序
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
default=LinkShowType.I) #zhq: 链接显示类型
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence']
ordering = ['sequence'] #zhq: 按顺序排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
#zhq: 侧边栏模型 - 支持自定义HTML内容
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
@ -303,7 +347,7 @@ class SideBar(models.Model):
def __str__(self):
return self.name
#zhq: 博客设置模型 - 单例模式,存储全局配置
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
@ -326,11 +370,11 @@ class BlogSettings(models.Model):
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
article_sub_length = models.IntegerField(_('article sub length'), default=300) #zhq: 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) #zhq: 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) #zhq: 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) #zhq: 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) #zhq:是否显示Google广告
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
@ -341,13 +385,13 @@ class BlogSettings(models.Model):
max_length=2000,
null=True,
blank=True,
default='')
default='') #zhq: ICP备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
default='') #zhq: 网站统计代码
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
@ -357,7 +401,7 @@ class BlogSettings(models.Model):
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
'评论是否需要审核', default=False, null=False) #zhq: 评论审核开关
class Meta:
verbose_name = _('Website configuration')
@ -367,6 +411,7 @@ class BlogSettings(models.Model):
return self.site_name
def clean(self):
# zhq: 确保配置表只有一条记录(单例模式)
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
@ -374,3 +419,6 @@ class BlogSettings(models.Model):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
# 导入多媒体管理模型
from blog.models_media import MediaFile, MediaFolder, MediaFileFolder

@ -0,0 +1,242 @@
# 文章草稿自动保存模型
# 用于在编辑文章时自动保存草稿,防止内容丢失
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article, Category
class ArticleDraft(models.Model):
"""
文章草稿模型
用于自动保存编辑中的文章防止内容丢失
"""
# 关联的文章(可为空,表示新建文章的草稿)
article = models.ForeignKey(
Article,
verbose_name='文章',
on_delete=models.CASCADE,
related_name='drafts',
null=True,
blank=True
)
# 草稿创建者
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='作者',
on_delete=models.CASCADE,
related_name='article_drafts'
)
# 草稿标题
title = models.CharField('标题', max_length=200, blank=True, default='')
# 草稿正文
body = models.TextField('正文', blank=True, default='')
# 分类ID保存时的分类
category_id = models.IntegerField('分类ID', null=True, blank=True)
# 标签JSON 格式保存标签ID列表
tags_data = models.JSONField('标签数据', default=list, blank=True)
# 状态
status = models.CharField('状态', max_length=1, default='d')
# 评论状态
comment_status = models.CharField('评论状态', max_length=1, default='o')
# 类型
type = models.CharField('类型', max_length=1, default='a')
# 创建时间
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
# 最后更新时间
last_update_time = models.DateTimeField('最后更新时间', auto_now=True, db_index=True)
# 草稿会话ID用于区分不同的编辑会话
session_id = models.CharField('会话ID', max_length=64, blank=True, default='')
# 是否已发布(草稿应用到文章后标记为已发布)
is_published = models.BooleanField('已发布', default=False)
class Meta:
ordering = ['-last_update_time']
verbose_name = '文章草稿'
verbose_name_plural = '文章草稿'
indexes = [
models.Index(fields=['author', '-last_update_time'], name='draft_author_time_idx'),
models.Index(fields=['article', '-last_update_time'], name='draft_article_time_idx'),
models.Index(fields=['is_published', '-last_update_time'], name='draft_published_time_idx'),
]
def __str__(self):
title = self.title or '(无标题)'
return f"{title} - {self.author.username} - {self.last_update_time.strftime('%Y-%m-%d %H:%M')}"
@classmethod
def save_draft(cls, user, title='', body='', article_id=None, category_id=None,
tags_data=None, status='d', comment_status='o', type='a', session_id=''):
"""
保存或更新草稿
Args:
user: 用户对象
title: 文章标题
body: 文章正文
article_id: 文章ID如果是编辑现有文章
category_id: 分类ID
tags_data: 标签数据列表
status: 文章状态
comment_status: 评论状态
type: 文章类型
session_id: 会话ID
Returns:
ArticleDraft 实例
"""
if tags_data is None:
tags_data = []
# 查找是否存在相同会话的草稿
if session_id:
draft = cls.objects.filter(
author=user,
session_id=session_id,
is_published=False
).first()
elif article_id:
# 如果是编辑现有文章,查找该文章的最新未发布草稿
draft = cls.objects.filter(
author=user,
article_id=article_id,
is_published=False
).first()
else:
# 新建文章,查找最新的未关联文章的草稿
draft = cls.objects.filter(
author=user,
article__isnull=True,
is_published=False
).first()
# 更新或创建草稿
if draft:
draft.title = title
draft.body = body
draft.category_id = category_id
draft.tags_data = tags_data
draft.status = status
draft.comment_status = comment_status
draft.type = type
if session_id:
draft.session_id = session_id
draft.save()
else:
article = None
if article_id:
try:
article = Article.objects.get(id=article_id)
except Article.DoesNotExist:
pass
draft = cls.objects.create(
author=user,
article=article,
title=title,
body=body,
category_id=category_id,
tags_data=tags_data,
status=status,
comment_status=comment_status,
type=type,
session_id=session_id
)
return draft
def apply_to_article(self, article=None):
"""
将草稿应用到文章
Args:
article: Article 实例如果为空则使用关联的文章
Returns:
Article 实例
"""
if article is None:
article = self.article
if article is None:
# 创建新文章
from blog.models import Category
category = None
if self.category_id:
try:
category = Category.objects.get(id=self.category_id)
except Category.DoesNotExist:
# 如果分类不存在,使用第一个分类
category = Category.objects.first()
if category is None:
raise ValueError("必须指定文章分类")
article = Article.objects.create(
title=self.title,
body=self.body,
author=self.author,
category=category,
status=self.status,
comment_status=self.comment_status,
type=self.type
)
else:
# 更新现有文章
article.title = self.title
article.body = self.body
if self.category_id:
try:
from blog.models import Category
category = Category.objects.get(id=self.category_id)
article.category = category
except Category.DoesNotExist:
pass
article.status = self.status
article.comment_status = self.comment_status
article.type = self.type
article.save()
# 应用标签
if self.tags_data:
from blog.models import Tag
tags = Tag.objects.filter(id__in=self.tags_data)
article.tags.set(tags)
# 标记草稿为已发布
self.is_published = True
self.article = article
self.save()
return article
def get_preview_text(self, length=100):
"""
获取草稿预览文本
Args:
length: 预览文本长度
Returns:
str: 预览文本
"""
if self.body:
if len(self.body) > length:
return self.body[:length] + '...'
return self.body
return '(空草稿)'

@ -0,0 +1,268 @@
# 多媒体管理模型
# 提供图片、文件的上传、管理和优化功能
import os
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from PIL import Image
import hashlib
class MediaFile(models.Model):
"""
媒体文件模型
存储所有上传的图片和文件信息
"""
FILE_TYPE_CHOICES = [
('image', '图片'),
('file', '文件'),
]
# 文件名(原始文件名)
original_filename = models.CharField('原始文件名', max_length=255)
# 存储文件名(实际存储的唯一文件名)
stored_filename = models.CharField('存储文件名', max_length=255, unique=True)
# 文件类型(图片或普通文件)
file_type = models.CharField('文件类型', max_length=10, choices=FILE_TYPE_CHOICES)
# 文件大小(字节)
file_size = models.BigIntegerField('文件大小')
# 文件MD5哈希用于去重
file_hash = models.CharField('文件哈希', max_length=32, db_index=True)
# MIME类型
mime_type = models.CharField('MIME类型', max_length=100)
# 文件路径(相对路径)
file_path = models.CharField('文件路径', max_length=500)
# 上传用户
uploader = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='上传者',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='uploaded_files'
)
# 上传时间
upload_time = models.DateTimeField('上传时间', default=now, db_index=True)
# 图片专属字段
# 图片宽度
width = models.IntegerField('宽度', null=True, blank=True)
# 图片高度
height = models.IntegerField('高度', null=True, blank=True)
# 缩略图路径
thumbnail_path = models.CharField('缩略图路径', max_length=500, blank=True)
# 描述/备注
description = models.TextField('描述', blank=True)
# 是否公开(私有文件只有上传者可访问)
is_public = models.BooleanField('是否公开', default=True)
# 引用计数(被多少篇文章引用)
reference_count = models.IntegerField('引用次数', default=0)
class Meta:
ordering = ['-upload_time']
verbose_name = '媒体文件'
verbose_name_plural = '媒体文件'
indexes = [
models.Index(fields=['file_type', '-upload_time'], name='media_type_time_idx'),
models.Index(fields=['uploader', '-upload_time'], name='media_uploader_time_idx'),
models.Index(fields=['file_hash'], name='media_hash_idx'),
]
def __str__(self):
return self.original_filename
def get_absolute_url(self):
"""获取文件的访问URL"""
from django.templatetags.static import static
return static(self.file_path)
def get_thumbnail_url(self):
"""获取缩略图URL"""
if self.thumbnail_path:
from django.templatetags.static import static
return static(self.thumbnail_path)
return self.get_absolute_url()
def get_file_extension(self):
"""获取文件扩展名"""
return os.path.splitext(self.original_filename)[1].lower()
def is_image(self):
"""判断是否为图片"""
return self.file_type == 'image'
def delete(self, *args, **kwargs):
"""删除文件时同时删除物理文件"""
# 删除主文件
full_path = os.path.join(settings.STATICFILES, self.file_path)
if os.path.exists(full_path):
try:
os.remove(full_path)
except Exception as e:
import logging
logging.error(f"删除文件失败 {full_path}: {e}")
# 删除缩略图
if self.thumbnail_path:
thumb_path = os.path.join(settings.STATICFILES, self.thumbnail_path)
if os.path.exists(thumb_path):
try:
os.remove(thumb_path)
except Exception as e:
import logging
logging.error(f"删除缩略图失败 {thumb_path}: {e}")
super().delete(*args, **kwargs)
@classmethod
def get_file_hash(cls, file_content):
"""计算文件MD5哈希"""
md5 = hashlib.md5()
for chunk in file_content.chunks():
md5.update(chunk)
return md5.hexdigest()
@classmethod
def check_duplicate(cls, file_hash):
"""检查是否已存在相同文件"""
return cls.objects.filter(file_hash=file_hash).first()
def generate_thumbnail(self, max_size=(300, 300)):
"""生成缩略图"""
if not self.is_image():
return False
try:
full_path = os.path.join(settings.STATICFILES, self.file_path)
if not os.path.exists(full_path):
return False
# 打开图片
img = Image.open(full_path)
# 转换RGBA为RGB处理PNG透明背景
if img.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
# 生成缩略图
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# 保存缩略图
thumb_filename = f"thumb_{self.stored_filename}"
thumb_dir = os.path.dirname(full_path)
thumb_path = os.path.join(thumb_dir, thumb_filename)
img.save(thumb_path, quality=85, optimize=True)
# 更新缩略图路径
self.thumbnail_path = os.path.join(
os.path.dirname(self.file_path),
thumb_filename
)
self.save(update_fields=['thumbnail_path'])
return True
except Exception as e:
import logging
logging.error(f"生成缩略图失败: {e}", exc_info=True)
return False
class MediaFolder(models.Model):
"""
媒体文件夹模型
用于组织和分类媒体文件
"""
# 文件夹名称
name = models.CharField('文件夹名称', max_length=100)
# 父文件夹(支持嵌套)
parent = models.ForeignKey(
'self',
verbose_name='父文件夹',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='subfolders'
)
# 所有者
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='所有者',
on_delete=models.CASCADE,
related_name='media_folders'
)
# 创建时间
created_time = models.DateTimeField('创建时间', default=now)
# 描述
description = models.TextField('描述', blank=True)
class Meta:
ordering = ['name']
verbose_name = '媒体文件夹'
verbose_name_plural = '媒体文件夹'
unique_together = [['name', 'parent', 'owner']]
def __str__(self):
return self.get_full_path()
def get_full_path(self):
"""获取完整路径"""
if self.parent:
return f"{self.parent.get_full_path()}/{self.name}"
return self.name
def get_files_count(self):
"""获取文件夹中的文件数量"""
return self.files.count()
class MediaFileFolder(models.Model):
"""
媒体文件和文件夹的关联表
支持一个文件属于多个文件夹
"""
file = models.ForeignKey(
MediaFile,
on_delete=models.CASCADE,
related_name='folder_relations'
)
folder = models.ForeignKey(
MediaFolder,
on_delete=models.CASCADE,
related_name='file_relations'
)
added_time = models.DateTimeField('添加时间', default=now)
class Meta:
unique_together = [['file', 'folder']]
verbose_name = '文件-文件夹关联'
verbose_name_plural = '文件-文件夹关联'
def __str__(self):
return f"{self.file.original_filename} -> {self.folder.name}"

@ -0,0 +1,401 @@
# 用户关注和收藏功能模型
# 实现用户间的关注关系和文章收藏功能
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
class UserFollow(models.Model):
"""
用户关注模型
记录用户之间的关注关系
"""
# 关注者(谁关注了别人)
follower = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='关注者',
on_delete=models.CASCADE,
related_name='following'
)
# 被关注者(被谁关注)
following = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='被关注者',
on_delete=models.CASCADE,
related_name='followers'
)
# 关注时间
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
class Meta:
ordering = ['-creation_time']
verbose_name = '用户关注'
verbose_name_plural = '用户关注'
unique_together = [['follower', 'following']]
indexes = [
models.Index(fields=['follower', '-creation_time'], name='follow_follower_time_idx'),
models.Index(fields=['following', '-creation_time'], name='follow_following_time_idx'),
]
def __str__(self):
return f"{self.follower.username} -> {self.following.username}"
def clean(self):
"""验证:不能关注自己"""
from django.core.exceptions import ValidationError
if self.follower == self.following:
raise ValidationError(_('Cannot follow yourself'))
@classmethod
def is_following(cls, follower, following):
"""
检查是否已关注
Args:
follower: 关注者用户对象
following: 被关注者用户对象
Returns:
bool: 是否已关注
"""
return cls.objects.filter(follower=follower, following=following).exists()
@classmethod
def follow(cls, follower, following):
"""
关注用户
Args:
follower: 关注者用户对象
following: 被关注者用户对象
Returns:
UserFollow 实例或 None
"""
if follower == following:
return None
follow, created = cls.objects.get_or_create(
follower=follower,
following=following
)
return follow if created else None
@classmethod
def unfollow(cls, follower, following):
"""
取消关注
Args:
follower: 关注者用户对象
following: 被关注者用户对象
Returns:
bool: 是否成功取消关注
"""
deleted_count, _ = cls.objects.filter(
follower=follower,
following=following
).delete()
return deleted_count > 0
@classmethod
def get_following_list(cls, user, limit=None):
"""
获取用户关注的人列表
Args:
user: 用户对象
limit: 返回数量限制
Returns:
QuerySet: 关注的用户列表
"""
queryset = cls.objects.filter(follower=user).select_related('following')
if limit:
queryset = queryset[:limit]
return [f.following for f in queryset]
@classmethod
def get_followers_list(cls, user, limit=None):
"""
获取用户的粉丝列表
Args:
user: 用户对象
limit: 返回数量限制
Returns:
QuerySet: 粉丝用户列表
"""
queryset = cls.objects.filter(following=user).select_related('follower')
if limit:
queryset = queryset[:limit]
return [f.follower for f in queryset]
@classmethod
def get_following_count(cls, user):
"""获取关注数量"""
return cls.objects.filter(follower=user).count()
@classmethod
def get_followers_count(cls, user):
"""获取粉丝数量"""
return cls.objects.filter(following=user).count()
class ArticleFavorite(models.Model):
"""
文章收藏模型
记录用户收藏的文章
"""
# 用户
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='用户',
on_delete=models.CASCADE,
related_name='favorites'
)
# 文章
article = models.ForeignKey(
Article,
verbose_name='文章',
on_delete=models.CASCADE,
related_name='favorited_by'
)
# 收藏时间
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
# 收藏备注(可选)
note = models.CharField('备注', max_length=200, blank=True, default='')
class Meta:
ordering = ['-creation_time']
verbose_name = '文章收藏'
verbose_name_plural = '文章收藏'
unique_together = [['user', 'article']]
indexes = [
models.Index(fields=['user', '-creation_time'], name='favorite_user_time_idx'),
models.Index(fields=['article', '-creation_time'], name='favorite_article_time_idx'),
]
def __str__(self):
return f"{self.user.username} -> {self.article.title}"
@classmethod
def is_favorited(cls, user, article):
"""
检查是否已收藏
Args:
user: 用户对象
article: 文章对象
Returns:
bool: 是否已收藏
"""
return cls.objects.filter(user=user, article=article).exists()
@classmethod
def add_favorite(cls, user, article, note=''):
"""
收藏文章
Args:
user: 用户对象
article: 文章对象
note: 收藏备注
Returns:
ArticleFavorite 实例或 None
"""
favorite, created = cls.objects.get_or_create(
user=user,
article=article,
defaults={'note': note}
)
return favorite if created else None
@classmethod
def remove_favorite(cls, user, article):
"""
取消收藏
Args:
user: 用户对象
article: 文章对象
Returns:
bool: 是否成功取消收藏
"""
deleted_count, _ = cls.objects.filter(
user=user,
article=article
).delete()
return deleted_count > 0
@classmethod
def get_user_favorites(cls, user, limit=None):
"""
获取用户的收藏列表
Args:
user: 用户对象
limit: 返回数量限制
Returns:
QuerySet: 收藏的文章列表
"""
queryset = cls.objects.filter(user=user).select_related('article', 'article__author', 'article__category')
if limit:
queryset = queryset[:limit]
return queryset
@classmethod
def get_favorite_count(cls, user):
"""获取用户收藏数量"""
return cls.objects.filter(user=user).count()
@classmethod
def get_article_favorite_count(cls, article):
"""获取文章被收藏次数"""
return cls.objects.filter(article=article).count()
class ArticleLike(models.Model):
"""
文章点赞模型
记录用户对文章的点赞
"""
# 用户
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='用户',
on_delete=models.CASCADE,
related_name='likes'
)
# 文章
article = models.ForeignKey(
Article,
verbose_name='文章',
on_delete=models.CASCADE,
related_name='liked_by'
)
# 点赞时间
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
class Meta:
ordering = ['-creation_time']
verbose_name = '文章点赞'
verbose_name_plural = '文章点赞'
unique_together = [['user', 'article']]
indexes = [
models.Index(fields=['user', '-creation_time'], name='like_user_time_idx'),
models.Index(fields=['article', '-creation_time'], name='like_article_time_idx'),
]
def __str__(self):
return f"{self.user.username} 👍 {self.article.title}"
@classmethod
def is_liked(cls, user, article):
"""
检查是否已点赞
Args:
user: 用户对象
article: 文章对象
Returns:
bool: 是否已点赞
"""
return cls.objects.filter(user=user, article=article).exists()
@classmethod
def add_like(cls, user, article):
"""
点赞文章
Args:
user: 用户对象
article: 文章对象
Returns:
ArticleLike 实例或 None
"""
like, created = cls.objects.get_or_create(
user=user,
article=article
)
return like if created else None
@classmethod
def remove_like(cls, user, article):
"""
取消点赞
Args:
user: 用户对象
article: 文章对象
Returns:
bool: 是否成功取消点赞
"""
deleted_count, _ = cls.objects.filter(
user=user,
article=article
).delete()
return deleted_count > 0
@classmethod
def get_article_like_count(cls, article):
"""
获取文章点赞数
Args:
article: 文章对象
Returns:
int: 点赞数
"""
return cls.objects.filter(article=article).count()
@classmethod
def get_user_like_count(cls, user):
"""
获取用户点赞数
Args:
user: 用户对象
Returns:
int: 用户点赞的文章数量
"""
return cls.objects.filter(user=user).count()
@classmethod
def get_user_likes(cls, user, limit=None):
"""
获取用户点赞的文章列表
Args:
user: 用户对象
limit: 返回数量限制
Returns:
QuerySet: 点赞的文章列表
"""
queryset = cls.objects.filter(user=user).select_related('article', 'article__author', 'article__category')
if limit:
queryset = queryset[:limit]
return queryset

@ -0,0 +1,170 @@
# 文章版本管理模型
# 用于追踪文章的修改历史,支持版本对比和回滚
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
class ArticleVersion(models.Model):
"""
文章版本模型
每次文章保存时自动创建版本记录
"""
# 关联的文章
article = models.ForeignKey(
Article,
verbose_name='文章',
on_delete=models.CASCADE,
related_name='versions'
)
# 版本号(自动递增)
version_number = models.PositiveIntegerField('版本号', default=1)
# 文章标题(保存时的标题)
title = models.CharField('标题', max_length=200)
# 文章正文(保存时的内容)
body = models.TextField('正文')
# 发布时间
pub_time = models.DateTimeField('发布时间')
# 状态
status = models.CharField('状态', max_length=1)
# 评论状态
comment_status = models.CharField('评论状态', max_length=1)
# 类型
type = models.CharField('类型', max_length=1)
# 分类保存时的分类ID
category_id = models.IntegerField('分类ID')
category_name = models.CharField('分类名称', max_length=30)
# 创建版本的用户
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name='创建者',
on_delete=models.SET_NULL,
null=True,
related_name='article_versions_created'
)
# 版本创建时间
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
# 变更说明(可选)
change_summary = models.CharField(
'变更说明',
max_length=200,
blank=True,
default=''
)
# 是否为自动保存
is_auto_save = models.BooleanField('自动保存', default=True)
class Meta:
ordering = ['-version_number']
verbose_name = '文章版本'
verbose_name_plural = '文章版本'
unique_together = [['article', 'version_number']]
indexes = [
models.Index(fields=['article', '-version_number'], name='version_article_num_idx'),
models.Index(fields=['creation_time'], name='version_creation_time_idx'),
]
def __str__(self):
return f"{self.article.title} - v{self.version_number}"
@classmethod
def create_version(cls, article, user=None, change_summary='', is_auto_save=True):
"""
创建文章版本
Args:
article: Article 实例
user: 创建版本的用户
change_summary: 变更说明
is_auto_save: 是否为自动保存
Returns:
ArticleVersion 实例
"""
# 获取最新版本号
latest_version = cls.objects.filter(article=article).first()
version_number = (latest_version.version_number + 1) if latest_version else 1
# 创建版本记录
version = cls.objects.create(
article=article,
version_number=version_number,
title=article.title,
body=article.body,
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
category_id=article.category_id,
category_name=article.category.name,
created_by=user or article.author,
change_summary=change_summary,
is_auto_save=is_auto_save
)
return version
def restore_to_article(self):
"""
将此版本恢复到文章
Returns:
bool: 是否成功恢复
"""
try:
article = self.article
article.title = self.title
article.body = self.body
article.pub_time = self.pub_time
article.status = self.status
article.comment_status = self.comment_status
article.type = self.type
# 尝试恢复分类如果分类ID仍存在
from blog.models import Category
try:
category = Category.objects.get(id=self.category_id)
article.category = category
except Category.DoesNotExist:
# 如果原分类已删除,保持当前分类不变
pass
article.save()
return True
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Failed to restore version {self.id}: {e}")
return False
def get_diff_with_current(self):
"""
获取与当前文章的差异
Returns:
dict: 包含差异信息的字典
"""
article = self.article
diff = {
'title_changed': self.title != article.title,
'body_changed': self.body != article.body,
'status_changed': self.status != article.status,
'category_changed': self.category_id != article.category_id,
}
return diff

@ -0,0 +1,316 @@
# API 速率限制
# 提供灵活的API请求频率限制功能
# 支持按IP、按用户、按API端点限制
import logging
import time
from functools import wraps
from django.core.cache import cache
from django.http import JsonResponse
from django.conf import settings
logger = logging.getLogger(__name__)
class RateLimitExceeded(Exception):
"""速率限制超出异常"""
pass
class RateLimiter:
"""速率限制器"""
# 默认限制配置
DEFAULT_LIMITS = {
'default': {'requests': 100, 'window': 60}, # 100次/分钟
'strict': {'requests': 10, 'window': 60}, # 10次/分钟
'loose': {'requests': 1000, 'window': 60}, # 1000次/分钟
}
@classmethod
def get_client_ip(cls, request):
"""获取客户端IP地址"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR', '')
return ip
@classmethod
def get_cache_key(cls, identifier, scope='default'):
"""
生成缓存键
Args:
identifier: 标识符IP用户ID等
scope: 作用域API端点名称等
Returns:
str: 缓存键
"""
return f"rate_limit:{scope}:{identifier}"
@classmethod
def is_rate_limited(cls, identifier, limit_config, scope='default'):
"""
检查是否超过速率限制
Args:
identifier: 标识符
limit_config: 限制配置 {'requests': 100, 'window': 60}
scope: 作用域
Returns:
tuple: (是否限制, 剩余请求数, 重置时间)
"""
requests_limit = limit_config['requests']
time_window = limit_config['window']
cache_key = cls.get_cache_key(identifier, scope)
# 获取当前窗口的请求记录
current_time = int(time.time())
window_start = current_time - time_window
# 使用列表存储请求时间戳
request_times = cache.get(cache_key, [])
# 过滤掉窗口外的请求
request_times = [t for t in request_times if t > window_start]
# 检查是否超限
if len(request_times) >= requests_limit:
# 计算重置时间
oldest_request = min(request_times)
reset_time = oldest_request + time_window
remaining = 0
is_limited = True
else:
# 添加当前请求
request_times.append(current_time)
remaining = requests_limit - len(request_times)
reset_time = current_time + time_window
is_limited = False
# 更新缓存
cache.set(cache_key, request_times, time_window + 10)
return is_limited, remaining, reset_time
@classmethod
def get_limit_config(cls, limit_name='default'):
"""
获取限制配置
Args:
limit_name: 限制名称或自定义配置
Returns:
dict: 限制配置
"""
if isinstance(limit_name, dict):
return limit_name
# 从设置中获取
custom_limits = getattr(settings, 'RATE_LIMITS', {})
if limit_name in custom_limits:
return custom_limits[limit_name]
if limit_name in cls.DEFAULT_LIMITS:
return cls.DEFAULT_LIMITS[limit_name]
# 默认限制
return cls.DEFAULT_LIMITS['default']
def rate_limit(limit='default', key_func=None, scope=None):
"""
API速率限制装饰器
Args:
limit: 限制配置名称或字典 {'requests': 100, 'window': 60}
key_func: 自定义键函数 func(request) -> str
scope: 作用域名称默认使用视图函数名
使用示例:
@rate_limit(limit='strict')
def my_api_view(request):
...
@rate_limit(limit={'requests': 50, 'window': 60})
def another_view(request):
...
@rate_limit(key_func=lambda req: req.user.id, scope='user_actions')
def user_api(request):
...
"""
def decorator(view_func):
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
# 获取限制配置
limit_config = RateLimiter.get_limit_config(limit)
# 确定作用域
view_scope = scope or view_func.__name__
# 确定标识符
if key_func:
identifier = key_func(request)
elif request.user.is_authenticated:
# 已登录用户使用用户ID
identifier = f"user_{request.user.id}"
else:
# 未登录用户使用IP
identifier = f"ip_{RateLimiter.get_client_ip(request)}"
# 检查速率限制
is_limited, remaining, reset_time = RateLimiter.is_rate_limited(
identifier,
limit_config,
view_scope
)
# 添加速率限制头
response_headers = {
'X-RateLimit-Limit': str(limit_config['requests']),
'X-RateLimit-Remaining': str(remaining),
'X-RateLimit-Reset': str(reset_time),
}
if is_limited:
# 超过限制
retry_after = reset_time - int(time.time())
logger.warning(
f"Rate limit exceeded for {identifier} "
f"on {view_scope}: {limit_config}"
)
response = JsonResponse({
'success': False,
'error': 'rate_limit_exceeded',
'message': f'请求过于频繁,请在 {retry_after} 秒后重试',
'retry_after': retry_after
}, status=429)
response_headers['Retry-After'] = str(retry_after)
# 设置响应头
for header, value in response_headers.items():
response[header] = value
return response
# 执行视图
response = view_func(request, *args, **kwargs)
# 添加速率限制头到响应
for header, value in response_headers.items():
response[header] = value
return response
return wrapped_view
return decorator
def ip_rate_limit(limit='default', scope=None):
"""
基于IP的速率限制装饰器
Args:
limit: 限制配置
scope: 作用域
示例:
@ip_rate_limit(limit='strict')
def api_view(request):
...
"""
return rate_limit(
limit=limit,
key_func=lambda req: f"ip_{RateLimiter.get_client_ip(req)}",
scope=scope
)
def user_rate_limit(limit='default', scope=None):
"""
基于用户的速率限制装饰器
Args:
limit: 限制配置
scope: 作用域
示例:
@user_rate_limit(limit={'requests': 50, 'window': 3600})
def api_view(request):
...
"""
def key_func(request):
if request.user.is_authenticated:
return f"user_{request.user.id}"
# 未登录用户降级到IP限制
return f"ip_{RateLimiter.get_client_ip(request)}"
return rate_limit(
limit=limit,
key_func=key_func,
scope=scope
)
# ==================== 中间件 ====================
class RateLimitMiddleware:
"""
全局速率限制中间件
settings.py 中配置:
MIDDLEWARE = [
...
'blog.rate_limit.RateLimitMiddleware',
]
RATE_LIMIT_ENABLED = True
GLOBAL_RATE_LIMIT = {'requests': 1000, 'window': 60}
"""
def __init__(self, get_response):
self.get_response = get_response
self.enabled = getattr(settings, 'RATE_LIMIT_ENABLED', False)
self.limit = getattr(settings, 'GLOBAL_RATE_LIMIT', {'requests': 1000, 'window': 60})
def __call__(self, request):
if self.enabled and request.path.startswith('/blog/api/'):
# 只对API请求进行全局限制
identifier = f"global_ip_{RateLimiter.get_client_ip(request)}"
is_limited, remaining, reset_time = RateLimiter.is_rate_limited(
identifier,
self.limit,
'global'
)
if is_limited:
retry_after = reset_time - int(time.time())
response = JsonResponse({
'success': False,
'error': 'rate_limit_exceeded',
'message': '全局请求频率超限,请稍后再试',
'retry_after': retry_after
}, status=429)
response['X-RateLimit-Limit'] = str(self.limit['requests'])
response['X-RateLimit-Remaining'] = '0'
response['X-RateLimit-Reset'] = str(reset_time)
response['Retry-After'] = str(retry_after)
return response
response = self.get_response(request)
return response

@ -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("发送失败,请重试")
}
}
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,13 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*
* See the Getting Started docs for more information:
* http://getbootstrap.com/getting-started/#support-ie10-width
*/
@-ms-viewport { width: device-width; }
@-o-viewport { width: device-width; }
@viewport { width: device-width; }

@ -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;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

@ -0,0 +1,51 @@
// NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
// IT'S JUST JUNK FOR OUR DOCS!
// ++++++++++++++++++++++++++++++++++++++++++
/*!
* Copyright 2014-2015 Twitter, Inc.
*
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see https://creativecommons.org/licenses/by/3.0/.
*/
// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
(function () {
'use strict';
function emulatedIEMajorVersion() {
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
if (groups === null) {
return null
}
var ieVersionNum = parseInt(groups[1], 10)
var ieMajorVersion = Math.floor(ieVersionNum)
return ieMajorVersion
}
function actualNonEmulatedIEMajorVersion() {
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
// IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode
}
if (jscriptVersion < 9) {
return 8 // IE8 (or lower; haven't tested on IE<8)
}
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
}
var ua = window.navigator.userAgent
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE
}
var emulated = emulatedIEMajorVersion()
if (emulated === null) {
return // Not IE
}
var nonEmulated = actualNonEmulatedIEMajorVersion()
if (emulated !== nonEmulated) {
window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
}
})();

@ -0,0 +1,23 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
// See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width
(function () {
'use strict';
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement('style')
msViewportStyle.appendChild(
document.createTextNode(
'@-ms-viewport{width:auto!important}'
)
)
document.querySelector('head').appendChild(msViewportStyle)
}
})();

@ -0,0 +1,457 @@
/* 深色模式修复 - 覆盖 style.css 中的硬编码白色背景 */
/* 覆盖所有白色背景为使用CSS变量 */
[data-theme="dark"] .site-header,
[data-theme="dark"] #masthead {
background-color: var(--nav-bg) !important;
color: var(--nav-text) !important;
}
[data-theme="dark"] .site-content,
[data-theme="dark"] #content {
background-color: var(--bg-primary) !important;
}
[data-theme="dark"] .widget-area,
[data-theme="dark"] #secondary {
background-color: var(--sidebar-bg) !important;
}
[data-theme="dark"] .entry-content,
[data-theme="dark"] .entry-summary,
[data-theme="dark"] .page-content,
[data-theme="dark"] article {
background-color: var(--article-bg) !important;
color: var(--text-primary) !important;
}
[data-theme="dark"] .site {
background-color: var(--bg-primary) !important;
}
[data-theme="dark"] body {
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
/* 修复所有白色背景的元素 */
[data-theme="dark"] *[style*="background: #fff"],
[data-theme="dark"] *[style*="background-color: #fff"],
[data-theme="dark"] *[style*="background: white"],
[data-theme="dark"] *[style*="background-color: white"] {
background-color: var(--bg-primary) !important;
}
/* 修复所有白色文字的元素(排除按钮和链接) */
[data-theme="dark"] *[style*="color: #fff"]:not(.btn):not(a),
[data-theme="dark"] *[style*="color: white"]:not(.btn):not(a) {
color: var(--text-primary) !important;
}
/* 评论区修复 */
[data-theme="dark"] #comments,
[data-theme="dark"] .comment-list,
[data-theme="dark"] .comment,
[data-theme="dark"] .comment-body,
[data-theme="dark"] .comment-content {
background-color: var(--comment-bg) !important;
color: var(--text-primary) !important;
border-color: var(--comment-border) !important;
}
/* 导航菜单修复 */
[data-theme="dark"] .nav-menu,
[data-theme="dark"] .main-navigation,
[data-theme="dark"] #site-navigation {
background-color: var(--nav-bg) !important;
}
[data-theme="dark"] .nav-menu li,
[data-theme="dark"] .main-navigation li {
background-color: transparent !important;
}
[data-theme="dark"] .nav-menu a,
[data-theme="dark"] .main-navigation a {
color: var(--nav-text) !important;
}
[data-theme="dark"] .nav-menu a:hover,
[data-theme="dark"] .main-navigation a:hover {
background-color: var(--nav-hover-bg) !important;
color: var(--link-hover) !important;
}
/* Widget 修复 */
[data-theme="dark"] .widget {
background-color: var(--sidebar-bg) !important;
color: var(--text-primary) !important;
border-color: var(--sidebar-border) !important;
}
[data-theme="dark"] .widget-title {
color: var(--text-primary) !important;
border-color: var(--border-primary) !important;
}
[data-theme="dark"] .widget ul,
[data-theme="dark"] .widget ol {
background-color: transparent !important;
}
[data-theme="dark"] .widget a {
color: var(--link-color) !important;
}
/* 文章列表修复 */
[data-theme="dark"] .hentry,
[data-theme="dark"] .post,
[data-theme="dark"] .page {
background-color: var(--card-bg) !important;
color: var(--text-primary) !important;
border-color: var(--card-border) !important;
}
[data-theme="dark"] .entry-header {
background-color: transparent !important;
}
[data-theme="dark"] .entry-title a {
color: var(--text-primary) !important;
}
[data-theme="dark"] .entry-title a:hover {
color: var(--link-hover) !important;
}
[data-theme="dark"] .entry-meta,
[data-theme="dark"] .entry-footer {
color: var(--text-secondary) !important;
background-color: transparent !important;
}
/* 搜索框修复 */
[data-theme="dark"] #searchform,
[data-theme="dark"] .search-form {
background-color: var(--input-bg) !important;
}
[data-theme="dark"] #s,
[data-theme="dark"] .search-field {
background-color: var(--input-bg) !important;
color: var(--input-text) !important;
border-color: var(--input-border) !important;
}
/* 分页修复 */
[data-theme="dark"] .pagination,
[data-theme="dark"] .page-links,
[data-theme="dark"] .nav-links {
background-color: transparent !important;
}
[data-theme="dark"] .pagination a,
[data-theme="dark"] .page-links a,
[data-theme="dark"] .nav-links a {
background-color: var(--card-bg) !important;
color: var(--link-color) !important;
border-color: var(--border-primary) !important;
}
[data-theme="dark"] .pagination a:hover,
[data-theme="dark"] .page-links a:hover,
[data-theme="dark"] .nav-links a:hover {
background-color: var(--bg-hover) !important;
}
[data-theme="dark"] .pagination .current,
[data-theme="dark"] .page-links > .current {
background-color: var(--accent-primary) !important;
color: var(--text-inverse) !important;
}
/* 面包屑导航修复 */
[data-theme="dark"] .breadcrumbs,
[data-theme="dark"] .breadcrumb {
background-color: var(--bg-secondary) !important;
color: var(--text-secondary) !important;
}
/* 侧边栏小工具特定修复 */
[data-theme="dark"] #calendar_wrap {
background-color: var(--card-bg) !important;
}
[data-theme="dark"] #calendar_wrap table,
[data-theme="dark"] #calendar_wrap th,
[data-theme="dark"] #calendar_wrap td {
background-color: transparent !important;
color: var(--text-primary) !important;
border-color: var(--border-primary) !important;
}
/* 标签云修复 */
[data-theme="dark"] .tagcloud a,
[data-theme="dark"] .wp_widget_tag_cloud a {
background-color: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
border-color: var(--border-primary) !important;
}
[data-theme="dark"] .tagcloud a:hover,
[data-theme="dark"] .wp_widget_tag_cloud a:hover {
background-color: var(--bg-hover) !important;
color: var(--link-hover) !important;
}
/* 最近评论修复 */
[data-theme="dark"] .recentcomments {
background-color: transparent !important;
color: var(--text-primary) !important;
}
/* RSS 链接修复 */
[data-theme="dark"] .rss-date,
[data-theme="dark"] .rssSummary {
color: var(--text-secondary) !important;
}
/* 存档页面修复 */
[data-theme="dark"] .archive-meta,
[data-theme="dark"] .page-header {
background-color: var(--bg-secondary) !important;
color: var(--text-primary) !important;
border-color: var(--border-primary) !important;
}
/* 404 页面修复 */
[data-theme="dark"] .error404 .widget {
background-color: var(--card-bg) !important;
}
/* 图片说明修复 */
[data-theme="dark"] .wp-caption,
[data-theme="dark"] .gallery-caption {
background-color: var(--bg-secondary) !important;
color: var(--text-secondary) !important;
}
[data-theme="dark"] .wp-caption-text {
color: var(--text-secondary) !important;
}
/* 嵌入内容修复 */
[data-theme="dark"] embed,
[data-theme="dark"] iframe,
[data-theme="dark"] object {
border-color: var(--border-primary) !important;
}
/* 按钮修复 - 确保按钮上的白色文字不被改变 */
[data-theme="dark"] .btn,
[data-theme="dark"] button,
[data-theme="dark"] input[type="submit"],
[data-theme="dark"] input[type="button"],
[data-theme="dark"] .comment-reply-link {
color: inherit;
}
[data-theme="dark"] .btn-primary,
[data-theme="dark"] .btn-success,
[data-theme="dark"] .btn-info,
[data-theme="dark"] .btn-warning,
[data-theme="dark"] .btn-danger {
color: var(--text-inverse) !important;
}
/* Sticky post 修复 */
[data-theme="dark"] .sticky {
background-color: var(--bg-secondary) !important;
border-color: var(--accent-primary) !important;
}
/* 引用文字修复 */
[data-theme="dark"] cite {
color: var(--text-secondary) !important;
}
/* 列表修复 */
[data-theme="dark"] ul,
[data-theme="dark"] ol,
[data-theme="dark"] dl {
color: var(--text-primary) !important;
}
/* 定义列表修复 */
[data-theme="dark"] dt {
color: var(--text-primary) !important;
}
[data-theme="dark"] dd {
color: var(--text-secondary) !important;
}
/* 强调文本修复 */
[data-theme="dark"] strong,
[data-theme="dark"] b {
color: var(--text-primary) !important;
}
[data-theme="dark"] em,
[data-theme="dark"] i {
color: var(--text-primary) !important;
}
/* 删除线修复 */
[data-theme="dark"] del,
[data-theme="dark"] s {
color: var(--text-tertiary) !important;
}
/* 下划线修复 */
[data-theme="dark"] ins,
[data-theme="dark"] u {
color: var(--text-primary) !important;
background-color: var(--bg-tertiary) !important;
}
/* 小号文字修复 */
[data-theme="dark"] small {
color: var(--text-secondary) !important;
}
/* 标记文字修复 */
[data-theme="dark"] mark {
background-color: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
}
/* Pygments 代码高亮修复 */
[data-theme="dark"] .highlight,
[data-theme="dark"] .codehilite {
background-color: var(--code-block-bg) !important;
}
[data-theme="dark"] .highlight pre,
[data-theme="dark"] .codehilite pre {
background-color: transparent !important;
}
/* 站点标题和描述修复 */
[data-theme="dark"] .site-title,
[data-theme="dark"] .site-description {
color: var(--nav-text) !important;
}
[data-theme="dark"] .site-title a {
color: var(--nav-text) !important;
}
[data-theme="dark"] .site-title a:hover {
color: var(--link-hover) !important;
}
/* 页面容器修复 */
[data-theme="dark"] #page,
[data-theme="dark"] .site,
[data-theme="dark"] #main,
[data-theme="dark"] .wrapper {
background-color: var(--bg-primary) !important;
}
/* 修复特定的警告框背景 */
[data-theme="dark"] *[style*="background: #fff9c0"],
[data-theme="dark"] *[style*="background-color: #fff9c0"] {
background-color: rgba(255, 249, 192, 0.2) !important;
color: var(--text-primary) !important;
}
/* 修复特定的警告框背景 */
[data-theme="dark"] *[style*="background: #fff3cd"],
[data-theme="dark"] *[style*="background-color: #fff3cd"] {
background-color: rgba(255, 243, 205, 0.2) !important;
color: var(--text-primary) !important;
}
/* 补充:文章卡片内部元素修复 */
[data-theme="dark"] .post-thumbnail,
[data-theme="dark"] .entry-thumbnail {
background-color: transparent !important;
}
/* 补充:作者信息框修复 */
[data-theme="dark"] .author-info,
[data-theme="dark"] .author-bio {
background-color: var(--bg-secondary) !important;
color: var(--text-primary) !important;
border-color: var(--border-primary) !important;
}
/* 补充:相关文章修复 */
[data-theme="dark"] .related-posts,
[data-theme="dark"] .related-articles {
background-color: var(--bg-secondary) !important;
}
/* 补充:分类和标签显示修复 */
[data-theme="dark"] .cat-links,
[data-theme="dark"] .tags-links {
color: var(--text-secondary) !important;
}
[data-theme="dark"] .cat-links a,
[data-theme="dark"] .tags-links a {
color: var(--link-color) !important;
background-color: var(--bg-tertiary) !important;
}
/* 补充:阅读更多链接修复 */
[data-theme="dark"] .more-link {
color: var(--link-color) !important;
}
[data-theme="dark"] .more-link:hover {
color: var(--link-hover) !important;
}
/* 补充:表单元素标签修复 */
[data-theme="dark"] label {
color: var(--text-primary) !important;
}
/* 补充:占位符修复 */
[data-theme="dark"] ::placeholder {
color: var(--input-placeholder) !important;
opacity: 1;
}
[data-theme="dark"] :-ms-input-placeholder {
color: var(--input-placeholder) !important;
}
[data-theme="dark"] ::-ms-input-placeholder {
color: var(--input-placeholder) !important;
}
/* 补充:选中文本修复 */
[data-theme="dark"] ::selection {
background-color: var(--accent-primary) !important;
color: var(--text-inverse) !important;
}
[data-theme="dark"] ::-moz-selection {
background-color: var(--accent-primary) !important;
color: var(--text-inverse) !important;
}
/* 修复 hfeed 类容器 */
[data-theme="dark"] .hfeed {
background-color: var(--bg-primary) !important;
}
/* 修复所有可能的白色背景覆盖 */
[data-theme="dark"] .site-header,
[data-theme="dark"] .site-content,
[data-theme="dark"] .site-footer {
background-color: transparent !important;
}

@ -0,0 +1,273 @@
/*
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;
margin-bottom: 0;
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%;
}
img.size-full,
img.size-large,
img.header-image,
img.wp-post-image,
img[class*="align"],
img[class*="wp-image-"],
img[class*="attachment-"] {
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;
max-width: 960px;
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;
border-top: 1px solid #ededed;
display: inline-block !important;
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 .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;
position: absolute;
top: 100%;
z-index: 1;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
.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 {
border-left: 0;
clip: inherit;
overflow: inherit;
height: inherit;
width: inherit;
}
.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;
display: block;
font-size: 11px;
line-height: 2.181818182;
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,
.main-navigation .current_page_ancestor > a {
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 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 {
float: left;
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 {
float: right;
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;
}
/* add input font for <IE9 Password Box to make the bullets show up */
input[type="password"] {
font-family: Helvetica, Arial, sans-serif;
}
/* RTL overrides for IE7 and IE8
-------------------------------------------------------------- */
.rtl .site-header h1,
.rtl .site-header h2 {
text-align: right;
}
.rtl .widget-area,
.rtl .author-description {
float: left;
}
.rtl .author-avatar,
.rtl .site-content {
float: right;
}
.rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul {
text-align: right;
}
.rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li {
margin-left: 40px;
margin-right: auto;
}
.rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
.ie7 .rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
.ie7 .rtl .main-navigation ul li {
z-index: 99;
}
.ie7 .rtl .main-navigation li ul {
position: absolute;
bottom: 100%;
right: 0;
z-index: 1;
}
.ie7 .rtl .main-navigation li {
margin-right: auto;
margin-left: 40px;
}
.ie7 .rtl .main-navigation li ul ul ul {
position: relative;
z-index: 1;
}

@ -0,0 +1,74 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: red;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: red;
border-left-color: red;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
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-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

@ -0,0 +1,305 @@
.icon-sn-google {
background-position: 0 -28px;
}
.icon-sn-bg-google {
background-color: #4285f4;
background-position: 0 0;
}
.fa-sn-google {
color: #4285f4;
}
.icon-sn-github {
background-position: -28px -28px;
}
.icon-sn-bg-github {
background-color: #333;
background-position: -28px 0;
}
.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;
}
.icon-sn-qq {
background-position: -84px -28px;
}
.icon-sn-bg-qq {
background-color: #0098e6;
background-position: -84px 0;
}
.fa-sn-qq {
color: #0098e6;
}
.icon-sn-twitter {
background-position: -112px -28px;
}
.icon-sn-bg-twitter {
background-color: #50abf1;
background-position: -112px 0;
}
.fa-sn-twitter {
color: #50abf1;
}
.icon-sn-facebook {
background-position: -140px -28px;
}
.icon-sn-bg-facebook {
background-color: #4862a3;
background-position: -140px 0;
}
.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;
}
.icon-sn-linkedin {
background-position: -336px -28px;
}
.icon-sn-bg-linkedin {
background-color: #0077b9;
background-position: -336px 0;
}
.fa-sn-linkedin {
color: #0077b9;
}
[class*=icon-sn-] {
display: inline-block;
background-image: url('../img/icon-sn.svg');
background-repeat: no-repeat;
width: 28px;
height: 28px;
vertical-align: middle;
background-size: auto 56px;
}
[class*=icon-sn-]:hover {
opacity: .8;
filter: alpha(opacity=80);
}
.btn-sn-google {
background: #4285f4;
}
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3;
}
.btn-sn-github {
background: #333;
}
.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;
}
.btn-sn-qq {
background: #0098e6;
}
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd;
}
.btn-sn-twitter {
background: #50abf1;
}
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef;
}
.btn-sn-facebook {
background: #4862a3;
}
.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;
}
.btn-sn-linkedin {
background: #0077b9;
}
.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;
}

File diff suppressed because one or more lines are too long

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

@ -0,0 +1,91 @@
/**
* Created by liangliang on 2016/11/20.
*/
function do_reply(parentid) {
console.log(parentid);
$("#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_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));
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
function slideTopSet() {
var top = $(document).scrollTop();
if (top > 200) {
rocket.addClass('show');
} else {
rocket.removeClass('show');
}
}
$(document).on('click', '#rocket', function (event) {
rocket.addClass('move');
$('body, html').animate({
scrollTop: 0
}, 800);
});
$(document).on('animationEnd', function () {
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
$(document).on('webkitAnimationEnd', function () {
setTimeout(function () {
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);
};
}
};
// $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');
// selector.on('change', function () {
// form.submit();
// });
// });

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

Loading…
Cancel
Save