diff --git a/doc/DjangoBlog/.dockerignore b/doc/DjangoBlog/.dockerignore
new file mode 100644
index 0000000..bd68a58
--- /dev/null
+++ b/doc/DjangoBlog/.dockerignore
@@ -0,0 +1,12 @@
+bin/data/
+# virtualenv
+venv/
+collectedstatic/
+djangoblog/whoosh_index/
+uploads/
+settings_production.py
+*.md
+docs/
+logs/
+static/
+.github/
diff --git a/doc/DjangoBlog/.gitattributes b/doc/DjangoBlog/.gitattributes
new file mode 100644
index 0000000..fd52ece
--- /dev/null
+++ b/doc/DjangoBlog/.gitattributes
@@ -0,0 +1,6 @@
+blog/static/* linguist-vendored
+*.js linguist-vendored
+*.css linguist-vendored
+* text=auto
+*.sh text eol=lf
+*.conf text eol=lf
\ No newline at end of file
diff --git a/doc/DjangoBlog/.github/ISSUE_TEMPLATE.md b/doc/DjangoBlog/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..2b5b7aa
--- /dev/null
+++ b/doc/DjangoBlog/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,18 @@
+
+
+**我确定我已经查看了** (标注`[ ]`为`[x]`)
+
+- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
+- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
+- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
+
+----
+
+**我要申请** (标注`[ ]`为`[x]`)
+
+- [ ] BUG 反馈
+- [ ] 添加新的特性或者功能
+- [ ] 请求技术支持
diff --git a/doc/DjangoBlog/.github/workflows/codeql-analysis.yml b/doc/DjangoBlog/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..52775e0
--- /dev/null
+++ b/doc/DjangoBlog/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,49 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ - '**/*.yml'
+ - '**/*.txt'
+ pull_request:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ - '**/*.yml'
+ - '**/*.txt'
+ schedule:
+ - cron: '30 1 * * 0'
+
+
+jobs:
+ CodeQL-Build:
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: python
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
\ No newline at end of file
diff --git a/doc/DjangoBlog/.github/workflows/deploy-master.yml b/doc/DjangoBlog/.github/workflows/deploy-master.yml
new file mode 100644
index 0000000..c07a326
--- /dev/null
+++ b/doc/DjangoBlog/.github/workflows/deploy-master.yml
@@ -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
\ No newline at end of file
diff --git a/doc/DjangoBlog/.github/workflows/django.yml b/doc/DjangoBlog/.github/workflows/django.yml
new file mode 100644
index 0000000..ebe7953
--- /dev/null
+++ b/doc/DjangoBlog/.github/workflows/django.yml
@@ -0,0 +1,371 @@
+name: Django CI
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ pull_request:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # 标准测试 - Python 3.10
+ - python-version: "3.10"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 标准测试 - Python 3.11
+ - python-version: "3.11"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 完整测试 - 包含ES和覆盖率
+ - python-version: "3.11"
+ test-type: "full"
+ database: "mysql"
+ elasticsearch: true
+ coverage: true
+
+ # Docker构建测试
+ - python-version: "3.11"
+ test-type: "docker"
+ database: "none"
+ elasticsearch: false
+ coverage: false
+
+ name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
+
+ steps:
+ - name: Checkout代码
+ uses: actions/checkout@v4
+
+ - name: 设置测试信息
+ id: test-info
+ run: |
+ echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
+ if [ "${{ matrix.test-type }}" = "docker" ]; then
+ echo "skip_python_setup=true" >> $GITHUB_OUTPUT
+ else
+ echo "skip_python_setup=false" >> $GITHUB_OUTPUT
+ fi
+
+ # MySQL数据库设置 (只有需要数据库的测试才执行)
+ - name: 启动MySQL数据库
+ if: matrix.database == 'mysql'
+ uses: samin/mysql-action@v1.3
+ with:
+ host port: 3306
+ container port: 3306
+ character set server: utf8mb4
+ collation server: utf8mb4_general_ci
+ mysql version: latest
+ mysql root password: root
+ mysql database: djangoblog
+ mysql user: root
+ mysql password: root
+
+ # Elasticsearch设置 (只有完整测试才执行)
+ - name: 配置系统参数 (ES)
+ if: matrix.elasticsearch == true
+ run: |
+ sudo swapoff -a
+ sudo sysctl -w vm.swappiness=1
+ sudo sysctl -w fs.file-max=262144
+ sudo sysctl -w vm.max_map_count=262144
+
+ - name: 启动Elasticsearch
+ if: matrix.elasticsearch == true
+ uses: miyataka/elasticsearch-github-actions@1
+ with:
+ stack-version: '7.12.1'
+ plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
+
+ # Python环境设置 (Docker测试跳过)
+ - name: 设置Python ${{ matrix.python-version }}
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ cache-dependency-path: 'requirements.txt'
+
+ # 多层缓存策略优化
+ - name: 缓存Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/pip
+ .pytest_cache
+ key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
+ ${{ runner.os }}-python-${{ matrix.python-version }}-
+ ${{ runner.os }}-python-
+
+ # Django缓存优化 (测试数据库等)
+ - name: 缓存Django资源
+ if: matrix.test-type != 'docker'
+ uses: actions/cache@v4
+ with:
+ path: |
+ .coverage*
+ htmlcov/
+ .django_cache/
+ key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-django-${{ matrix.test-type }}-
+ ${{ runner.os }}-django-
+
+ - name: 安装Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ run: |
+ echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
+ python -m pip install --upgrade pip setuptools wheel
+
+ # 安装基础依赖
+ pip install -r requirements.txt
+
+ # 根据测试类型安装额外依赖
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 安装覆盖率工具"
+ pip install coverage[toml]
+ fi
+
+ # 验证关键依赖
+ echo "🔍 验证关键依赖安装"
+ python -c "import django; print(f'Django version: {django.get_version()}')"
+ python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
+
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ python -c "import elasticsearch; print('Elasticsearch client: OK')"
+ fi
+
+ # Django环境准备
+ - name: 准备Django环境
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🔧 准备Django测试环境"
+
+ # 等待数据库就绪
+ echo "⏳ 等待MySQL数据库启动..."
+ for i in {1..30}; do
+ if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
+ echo "✅ MySQL数据库连接成功"
+ break
+ fi
+ echo "🔄 等待数据库启动... ($i/30)"
+ sleep 2
+ done
+
+ # 等待Elasticsearch就绪 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "⏳ 等待Elasticsearch启动..."
+ for i in {1..30}; do
+ if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
+ echo "✅ Elasticsearch连接成功"
+ break
+ fi
+ echo "🔄 等待Elasticsearch启动... ($i/30)"
+ sleep 2
+ done
+ fi
+
+ # Django测试执行
+ - name: 执行数据库迁移
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🗄️ 执行数据库迁移"
+
+ # 检查迁移文件
+ echo "📋 检查待应用的迁移..."
+ python manage.py showmigrations
+
+ # 检查是否有未创建的迁移
+ python manage.py makemigrations --check --verbosity 2
+
+ # 执行迁移
+ python manage.py migrate --verbosity 2
+
+ echo "✅ 数据库迁移完成"
+
+ - name: 运行Django测试
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
+
+ # 显示Django配置信息
+ python manage.py diffsettings | head -20
+
+ # 运行测试
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 运行测试并生成覆盖率报告"
+ coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
+
+ echo "📈 生成覆盖率报告"
+ coverage xml
+ coverage report --show-missing
+ coverage html
+
+ echo "📋 覆盖率统计:"
+ coverage report | tail -1
+ else
+ echo "🧪 运行标准测试"
+ python manage.py test --verbosity=2 --failfast
+ fi
+
+ echo "✅ 测试执行完成"
+
+ # 覆盖率报告上传 (只有完整测试才执行)
+ - name: 上传覆盖率到Codecov
+ if: matrix.coverage == true && success()
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}
+ fail_ci_if_error: false
+ verbose: true
+
+ - name: 上传覆盖率到Codecov (备用)
+ if: matrix.coverage == true && failure()
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
+ fail_ci_if_error: false
+ verbose: true
+
+ # Docker构建测试
+ - name: 设置QEMU
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-qemu-action@v3
+
+ - name: 设置Docker Buildx
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-buildx-action@v3
+
+ - name: Docker构建测试
+ if: matrix.test-type == 'docker'
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: false
+ tags: djangoblog/djangoblog:test-${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # 收集测试工件 (失败时收集调试信息)
+ - name: 收集测试工件
+ if: failure() && matrix.test-type != 'docker'
+ run: |
+ echo "🔍 收集测试失败的调试信息"
+
+ # 收集Django日志
+ if [ -d "logs" ]; then
+ echo "📄 Django日志文件:"
+ ls -la logs/
+ if [ -f "logs/djangoblog.log" ]; then
+ echo "🔍 最新日志内容:"
+ tail -100 logs/djangoblog.log
+ fi
+ fi
+
+ # 显示数据库状态
+ echo "🗄️ 数据库连接状态:"
+ python -c "
+ try:
+ from django.db import connection
+ cursor = connection.cursor()
+ cursor.execute('SELECT VERSION()')
+ print(f'MySQL版本: {cursor.fetchone()[0]}')
+ cursor.execute('SHOW TABLES')
+ tables = cursor.fetchall()
+ print(f'数据库表数量: {len(tables)}')
+ except Exception as e:
+ print(f'数据库连接错误: {e}')
+ " || true
+
+ # Elasticsearch状态 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "🔍 Elasticsearch状态:"
+ curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
+ fi
+
+ # 上传测试工件
+ - name: 上传覆盖率HTML报告
+ if: matrix.coverage == true && always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report-${{ steps.test-info.outputs.test_name }}
+ path: htmlcov/
+ retention-days: 30
+
+ # 性能统计
+ - name: 测试性能统计
+ if: always() && matrix.test-type != 'docker'
+ run: |
+ echo "⚡ 测试性能统计:"
+ echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
+ echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
+
+ # 系统资源使用情况
+ echo "💻 系统资源:"
+ echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
+ echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
+ echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
+
+ # 测试结果汇总
+ - name: 测试完成总结
+ if: always()
+ run: |
+ echo "📋 ============ 测试执行总结 ============"
+ echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
+ echo " 🐍 Python版本: ${{ matrix.python-version }}"
+ echo " 🗄️ 数据库: ${{ matrix.database }}"
+ echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
+ echo " 📊 覆盖率: ${{ matrix.coverage }}"
+ echo " ⚡ 状态: ${{ job.status }}"
+ echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
+ echo "============================================"
+
+ # 根据测试结果显示不同消息
+ if [ "${{ job.status }}" = "success" ]; then
+ echo "🎉 测试执行成功!"
+ else
+ echo "❌ 测试执行失败,请检查上面的日志"
+ fi
diff --git a/doc/DjangoBlog/.github/workflows/docker.yml b/doc/DjangoBlog/.github/workflows/docker.yml
new file mode 100644
index 0000000..0712f46
--- /dev/null
+++ b/doc/DjangoBlog/.github/workflows/docker.yml
@@ -0,0 +1,43 @@
+name: docker
+
+on:
+ push:
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.yml'
+ branches:
+ - 'master'
+ - 'dev'
+ workflow_dispatch:
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set env to docker dev tag
+ if: endsWith(github.ref, '/dev')
+ run: |
+ echo "DOCKER_TAG=test" >> $GITHUB_ENV
+ - name: Set env to docker latest tag
+ if: endsWith(github.ref, '/master')
+ run: |
+ echo "DOCKER_TAG=latest" >> $GITHUB_ENV
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}
+
+
diff --git a/doc/DjangoBlog/.github/workflows/publish-release.yml b/doc/DjangoBlog/.github/workflows/publish-release.yml
new file mode 100644
index 0000000..5eb0853
--- /dev/null
+++ b/doc/DjangoBlog/.github/workflows/publish-release.yml
@@ -0,0 +1,39 @@
+name: publish release
+
+on:
+ release:
+ types: [ published ]
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v3
+ with:
+ images: name/app
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ push: true
+ platforms: |
+ linux/amd64
+ linux/arm64
+ linux/arm/v7
+ linux/arm/v6
+ linux/386
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}
diff --git a/doc/DjangoBlog/.gitignore b/doc/DjangoBlog/.gitignore
new file mode 100644
index 0000000..76302b1
--- /dev/null
+++ b/doc/DjangoBlog/.gitignore
@@ -0,0 +1,79 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.pot
+
+# Django stuff:
+*.log
+logs/
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+
+# PyCharm
+# http://www.jetbrains.com/pycharm/webhelp/project.html
+.idea
+.iml
+# virtualenv
+venv/
+
+collectedstatic/
+djangoblog/whoosh_index/
+google93fd32dbd906620a.html
+baidu_verify_FlHL7cUyC9.html
+BingSiteAuth.xml
+cb9339dbe2ff86a5aa169d28dba5f615.txt
+werobot_session.*
+django.jpg
+uploads/
+settings_production.py
+werobot_session.db
+bin/datas/
diff --git a/doc/DjangoBlog/Dockerfile b/doc/DjangoBlog/Dockerfile
new file mode 100644
index 0000000..80b46ac
--- /dev/null
+++ b/doc/DjangoBlog/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.11
+ENV PYTHONUNBUFFERED 1
+WORKDIR /code/djangoblog/
+RUN apt-get update && \
+ apt-get install default-libmysqlclient-dev gettext -y && \
+ rm -rf /var/lib/apt/lists/*
+ADD requirements.txt requirements.txt
+RUN pip install --upgrade pip && \
+ pip install --no-cache-dir -r requirements.txt && \
+ pip install --no-cache-dir gunicorn[gevent] && \
+ pip cache purge
+
+ADD . .
+RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
+ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
diff --git a/doc/DjangoBlog/LICENSE b/doc/DjangoBlog/LICENSE
new file mode 100644
index 0000000..3b08474
--- /dev/null
+++ b/doc/DjangoBlog/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2025 车亮亮
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/doc/DjangoBlog/README.md b/doc/DjangoBlog/README.md
new file mode 100644
index 0000000..56aa4cc
--- /dev/null
+++ b/doc/DjangoBlog/README.md
@@ -0,0 +1,158 @@
+# DjangoBlog
+
+
+
+
+
+
+
+
+
+ 一款功能强大、设计优雅的现代化博客系统
+
+ English • 简体中文
+
+
+---
+
+DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能,还通过一个灵活的插件系统,让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者,DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
+
+## ✨ 特性亮点
+
+- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
+- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
+- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
+- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
+- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
+- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
+- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
+- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能,代码解耦,易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
+- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
+- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
+- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
+
+## 🛠️ 技术栈
+
+- **后端**: Python 3.10, Django 4.0
+- **数据库**: MySQL, SQLite (可配置)
+- **缓存**: Redis
+- **前端**: HTML5, CSS3, JavaScript
+- **搜索**: Whoosh, Elasticsearch (可配置)
+- **编辑器**: Markdown (mdeditor)
+
+## 🚀 快速开始
+
+### 1. 环境准备
+
+确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
+
+### 2. 克隆与安装
+
+```bash
+# 克隆项目到本地
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
+
+# 安装依赖
+pip install -r requirements.txt
+```
+
+### 3. 项目配置
+
+- **数据库**:
+ 打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ 在 MySQL 中创建数据库:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **更多配置**:
+ 关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
+
+### 4. 初始化数据库
+
+```bash
+python manage.py makemigrations
+python manage.py migrate
+
+# 创建一个超级管理员账户
+python manage.py createsuperuser
+```
+
+### 5. 运行项目
+
+```bash
+# (可选) 生成一些测试数据
+python manage.py create_testdata
+
+# (可选) 收集和压缩静态文件
+python manage.py collectstatic --noinput
+python manage.py compress --force
+
+# 启动开发服务器
+python manage.py runserver
+```
+
+现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
+
+## 部署
+
+- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
+- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术,请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
+- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
+
+## 🧩 插件系统
+
+插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
+
+- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
+- **现有插件**: `view_count`(浏览计数), `seo_optimizer`(SEO优化)等都是通过插件系统实现的。
+- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
+
+## 🤝 贡献指南
+
+我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Issue 或 Pull Request。
+
+## 📄 许可证
+
+本项目基于 [MIT License](LICENSE) 开源。
+
+---
+
+## ❤️ 支持与赞助
+
+如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
+
+
+
+
+
+
+ (左) 支付宝 / (右) 微信
+
+
+## 🙏 鸣谢
+
+特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
+
+
+
+
+
+
+
+---
+> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。
diff --git a/doc/DjangoBlog/accounts/__init__.py b/doc/DjangoBlog/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/accounts/admin.py b/doc/DjangoBlog/accounts/admin.py
new file mode 100644
index 0000000..67c1b87
--- /dev/null
+++ b/doc/DjangoBlog/accounts/admin.py
@@ -0,0 +1,63 @@
+from django import forms
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.contrib.auth.forms import UsernameField
+from django.utils.translation import gettext_lazy as _
+
+# Register your models here.
+from .models import BlogUser
+
+
+class BlogUserCreationForm(forms.ModelForm):
+ #sjt 后台创建用户表单:处理密码设置和验证
+ password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # sjt 密码字段
+ password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # sjt 确认密码字段
+
+ class Meta:
+ model = BlogUser # sjt 关联BlogUser模型
+ fields = ('email',) # sjt 表单包含的字段
+
+ def clean_password2(self):
+ #sjt 验证两次输入密码一致性
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError(_("passwords do not match"))
+ return password2
+
+ def save(self, commit=True):
+ #sjt 保存用户:加密密码并标记来源为后台
+ user = super().save(commit=False)
+ user.set_password(self.cleaned_data["password1"]) # sjt 加密密码
+ if commit:
+ user.source = 'adminsite' # sjt 标记来源为后台添加
+ user.save()
+ return user
+
+
+class BlogUserChangeForm(UserChangeForm):
+ #sjt 后台修改用户表单:配置修改用户的字段
+ class Meta:
+ model = BlogUser # sjt 关联BlogUser模型
+ fields = '__all__' # sjt 包含所有字段
+ field_classes = {'username': UsernameField} # sjt 用户名字段类
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class BlogUserAdmin(UserAdmin):
+ #sjt Admin后台用户管理配置:定义显示和操作方式
+ form = BlogUserChangeForm # sjt 修改用户表单
+ add_form = BlogUserCreationForm # sjt 创建用户表单
+ list_display = ( # sjt 列表页显示的字段
+ 'id',
+ 'nickname',
+ 'username',
+ 'email',
+ 'last_login',
+ 'date_joined',
+ 'source')
+ list_display_links = ('id', 'username') # sjt 列表页可点击的链接字段
+ ordering = ('-id',) # sjt 按ID倒序排列
+ search_fields = ('username', 'nickname', 'email') # sjt 搜索字段
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/apps.py b/doc/DjangoBlog/accounts/apps.py
new file mode 100644
index 0000000..2ff3c94
--- /dev/null
+++ b/doc/DjangoBlog/accounts/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ #sjt accounts应用的配置类,定义应用名称
+ name = 'accounts'
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/forms.py b/doc/DjangoBlog/accounts/forms.py
new file mode 100644
index 0000000..783440c
--- /dev/null
+++ b/doc/DjangoBlog/accounts/forms.py
@@ -0,0 +1,128 @@
+from django import forms
+from django.contrib.auth import get_user_model, password_validation
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
+from django.core.exceptions import ValidationError
+from django.forms import widgets
+from django.utils.translation import gettext_lazy as _
+from . import utils
+from .models import BlogUser
+
+
+class LoginForm(AuthenticationForm):
+ #sjt 登录表单:定义登录表单字段样式
+ def __init__(self, *args, **kwargs):
+ super(LoginForm, self).__init__(*args, **kwargs)
+ # sjt 设置用户名输入框样式
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ # sjt 设置密码输入框样式
+ self.fields['password'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+
+
+class RegisterForm(UserCreationForm):
+ #sjt 注册表单:验证用户名、邮箱、密码合法性
+ def __init__(self, *args, **kwargs):
+ super(RegisterForm, self).__init__(*args, **kwargs)
+ # sjt 设置用户名输入框样式
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ # sjt 设置邮箱输入框样式
+ self.fields['email'].widget = widgets.EmailInput(
+ attrs={'placeholder': "email", "class": "form-control"})
+ # sjt 设置密码输入框样式
+ self.fields['password1'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+ # sjt 设置确认密码输入框样式
+ self.fields['password2'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "repeat password", "class": "form-control"})
+
+ def clean_email(self):
+ #sjt 验证邮箱唯一性:已存在则抛出异常
+ email = self.cleaned_data['email']
+ if get_user_model().objects.filter(email=email).exists():
+ raise ValidationError(_("email already exists"))
+ return email
+
+ class Meta:
+ model = get_user_model() # sjt 关联用户模型
+ fields = ("username", "email") # sjt 表单包含的字段
+
+
+class ForgetPasswordForm(forms.Form):
+ #sjt 密码找回表单:验证邮箱、验证码、新密码合法性
+ new_password1 = forms.CharField(
+ label=_("New password"),
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control",
+ 'placeholder': _("New password")
+ }
+ ),
+ ) # sjt 新密码字段
+
+ new_password2 = forms.CharField(
+ label="确认密码",
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control",
+ 'placeholder': _("Confirm password")
+ }
+ ),
+ ) # sjt 确认密码字段
+
+ email = forms.EmailField(
+ label='邮箱',
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Email")
+ }
+ ),
+ ) # sjt 邮箱字段
+
+ code = forms.CharField(
+ label=_('Code'),
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Code")
+ }
+ ),
+ ) # sjt 验证码字段
+
+ def clean_new_password2(self):
+ #sjt 验证两次输入密码一致性,并检查密码强度
+ password1 = self.data.get("new_password1")
+ password2 = self.data.get("new_password2")
+ if password1 and password2 and password1 != password2:
+ raise ValidationError(_("passwords do not match"))
+ password_validation.validate_password(password2) # sjt 调用Django密码验证器
+ return password2
+
+ def clean_email(self):
+ #sjt 验证邮箱是否已注册
+ user_email = self.cleaned_data.get("email")
+ if not BlogUser.objects.filter(
+ email=user_email
+ ).exists():
+ raise ValidationError(_("email does not exist"))
+ return user_email
+
+ def clean_code(self):
+ #sjt 验证验证码有效性
+ code = self.cleaned_data.get("code")
+ error = utils.verify(
+ email=self.cleaned_data.get("email"),
+ code=code,
+ )
+ if error:
+ raise ValidationError(error)
+ return code
+
+
+class ForgetPasswordCodeForm(forms.Form):
+ #sjt 发送验证码表单:验证邮箱格式
+ email = forms.EmailField(
+ label=_('Email'),
+ ) # sjt 邮箱字段(用于发送验证码)
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/migrations/0001_initial.py b/doc/DjangoBlog/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..d2fbcab
--- /dev/null
+++ b/doc/DjangoBlog/accounts/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BlogUser',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+ ],
+ options={
+ 'verbose_name': '用户',
+ 'verbose_name_plural': '用户',
+ 'ordering': ['-id'],
+ 'get_latest_by': 'id',
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ ]
diff --git a/doc/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/doc/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
new file mode 100644
index 0000000..1a9f509
--- /dev/null
+++ b/doc/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -0,0 +1,46 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='bloguser',
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='nickname',
+ field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='source',
+ field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
+ ),
+ ]
diff --git a/doc/DjangoBlog/accounts/migrations/__init__.py b/doc/DjangoBlog/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/accounts/models.py b/doc/DjangoBlog/accounts/models.py
new file mode 100644
index 0000000..b5d167c
--- /dev/null
+++ b/doc/DjangoBlog/accounts/models.py
@@ -0,0 +1,39 @@
+from django.contrib.auth.models import AbstractUser
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from djangoblog.utils import get_current_site
+
+
+# Create your models here.
+
+class BlogUser(AbstractUser):
+ #sjt 扩展Django内置用户模型,增加博客系统所需的用户属性
+ nickname = models.CharField(_('nick name'), max_length=100, blank=True) # sjt 用户昵称
+ creation_time = models.DateTimeField(_('creation time'), default=now) # sjt 账号创建时间
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now) # sjt 信息最后修改时间
+ source = models.CharField(_('create source'), max_length=100, blank=True) # sjt 账号创建来源(如注册、后台添加)
+
+ def get_absolute_url(self):
+ #sjt 获取用户详情页的相对URL
+ return reverse(
+ 'blog:author_detail', kwargs={
+ 'author_name': self.username})
+
+ def __str__(self):
+ #sjt 对象字符串表示,返回用户邮箱
+ return self.email
+
+ def get_full_url(self):
+ #sjt 获取用户详情页的完整URL(包含域名)
+ site = get_current_site().domain
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ ordering = ['-id'] # sjt 按ID倒序排列
+ verbose_name = _('user') # sjt 模型显示名称
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id' # sjt 最新记录按ID判断
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/templatetags/__init__.py b/doc/DjangoBlog/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/accounts/tests.py b/doc/DjangoBlog/accounts/tests.py
new file mode 100644
index 0000000..abdc173
--- /dev/null
+++ b/doc/DjangoBlog/accounts/tests.py
@@ -0,0 +1,194 @@
+from django.test import Client, RequestFactory, TestCase
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+
+from accounts.models import BlogUser
+from blog.models import Article, Category
+from djangoblog.utils import *
+from . import utils
+
+
+# Create your tests here.
+
+class AccountTest(TestCase):
+ def setUp(self):
+ #sjt 测试初始化:创建测试客户端和测试用户
+ self.client = Client()
+ self.factory = RequestFactory()
+ self.blog_user = BlogUser.objects.create_user(
+ username="test",
+ email="admin@admin.com",
+ password="12345678"
+ )
+ self.new_test = "xxx123--=" # sjt 测试用新密码
+
+ def test_validate_account(self):
+ #sjt 测试用户账号验证:管理员登录、文章创建等功能
+ site = get_current_site().domain
+ # sjt 创建超级用户
+ user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="qwer!@#$ggg")
+ testuser = BlogUser.objects.get(username='liangliangyy1')
+
+ # sjt 测试登录
+ loginresult = self.client.login(
+ username='liangliangyy1',
+ password='qwer!@#$ggg')
+ self.assertEqual(loginresult, True)
+ response = self.client.get('/admin/')
+ self.assertEqual(response.status_code, 200)
+
+ # sjt 测试创建分类
+ category = Category()
+ category.name = "categoryaaa"
+ category.creation_time = timezone.now()
+ category.last_modify_time = timezone.now()
+ category.save()
+
+ # sjt 测试创建文章
+ article = Article()
+ article.title = "nicetitleaaa"
+ article.body = "nicecontentaaa"
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ # sjt 测试访问文章管理页
+ response = self.client.get(article.get_admin_url())
+ self.assertEqual(response.status_code, 200)
+
+ def test_validate_register(self):
+ #sjt 测试注册流程:注册->邮箱验证->登录->功能验证
+ # sjt 验证初始状态无此用户
+ self.assertEquals(
+ 0, len(
+ BlogUser.objects.filter(
+ email='user123@user.com')))
+ # sjt 提交注册表单
+ response = self.client.post(reverse('account:register'), {
+ 'username': 'user1233',
+ 'email': 'user123@user.com',
+ 'password1': 'password123!q@wE#R$T',
+ 'password2': 'password123!q@wE#R$T',
+ })
+ # sjt 验证用户创建成功
+ self.assertEquals(
+ 1, len(
+ BlogUser.objects.filter(
+ email='user123@user.com')))
+ # sjt 验证邮箱并激活用户
+ user = BlogUser.objects.filter(email='user123@user.com')[0]
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ path = reverse('accounts:result')
+ url = '{path}?type=validation&id={id}&sign={sign}'.format(
+ path=path, id=user.id, sign=sign)
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ # sjt 测试登录及权限
+ self.client.login(username='user1233', password='password123!q@wE#R$T')
+ user = BlogUser.objects.filter(email='user123@user.com')[0]
+ user.is_superuser = True
+ user.is_staff = True
+ user.save()
+ delete_sidebar_cache()
+ # ... 后续测试创建分类、文章等功能
+
+ def test_verify_email_code(self):
+ #sjt 测试验证码验证功能
+ to_email = "admin@admin.com"
+ code = generate_code() # sjt 生成验证码
+ utils.set_code(to_email, code) # sjt 缓存验证码
+ utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
+
+ # sjt 验证正确验证码
+ err = utils.verify("admin@admin.com", code)
+ self.assertEqual(err, None)
+
+ # sjt 验证错误邮箱的验证码
+ err = utils.verify("admin@123.com", code)
+ self.assertEqual(type(err), str)
+
+ def test_forget_password_email_code_success(self):
+ #sjt 测试成功发送密码找回验证码
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict(email="admin@admin.com")
+ )
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.content.decode("utf-8"), "ok")
+
+ def test_forget_password_email_code_fail(self):
+ #sjt 测试发送密码找回验证码失败情况(无效邮箱)
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict()
+ )
+ self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict(email="admin@com")
+ )
+ self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+
+ def test_forget_password_email_success(self):
+ #sjt 测试成功找回密码流程
+ code = generate_code()
+ utils.set_code(self.blog_user.email, code)
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email=self.blog_user.email,
+ code=code,
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+ self.assertEqual(resp.status_code, 302)
+
+ # sjt 验证密码是否修改成功
+ blog_user = BlogUser.objects.filter(
+ email=self.blog_user.email,
+ ).first()
+ self.assertNotEqual(blog_user, None)
+ self.assertEqual(blog_user.check_password(data["new_password1"]), True)
+
+ def test_forget_password_email_not_user(self):
+ #sjt 测试使用未注册邮箱找回密码
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email="123@123.com",
+ code="123456",
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+
+ self.assertEqual(resp.status_code, 200)
+
+ def test_forget_password_email_code_error(self):
+ #sjt 测试使用错误验证码找回密码
+ code = generate_code()
+ utils.set_code(self.blog_user.email, code)
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email=self.blog_user.email,
+ code="111111", # sjt 错误验证码
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+
+ self.assertEqual(resp.status_code, 200)
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/urls.py b/doc/DjangoBlog/accounts/urls.py
new file mode 100644
index 0000000..7cadb83
--- /dev/null
+++ b/doc/DjangoBlog/accounts/urls.py
@@ -0,0 +1,28 @@
+from django.urls import path
+from django.urls import re_path
+
+from . import views
+from .forms import LoginForm
+
+app_name = "accounts" # sjt 应用命名空间
+
+urlpatterns = [re_path(r'^login/$', # sjt 登录页面路由
+ views.LoginView.as_view(success_url='/'),
+ name='login',
+ kwargs={'authentication_form': LoginForm}),
+ re_path(r'^register/$', # sjt 注册页面路由
+ views.RegisterView.as_view(success_url="/"),
+ name='register'),
+ re_path(r'^logout/$', # sjt 注销功能路由
+ views.LogoutView.as_view(),
+ name='logout'),
+ path(r'account/result.html', # sjt 注册/验证结果页路由
+ views.account_result,
+ name='result'),
+ re_path(r'^forget_password/$', # sjt 密码找回页面路由
+ views.ForgetPasswordView.as_view(),
+ name='forget_password'),
+ re_path(r'^forget_password_code/$', # sjt 发送密码找回验证码路由
+ views.ForgetPasswordEmailCode.as_view(),
+ name='forget_password_code'),
+ ]
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/user_login_backend.py b/doc/DjangoBlog/accounts/user_login_backend.py
new file mode 100644
index 0000000..2cf3c1f
--- /dev/null
+++ b/doc/DjangoBlog/accounts/user_login_backend.py
@@ -0,0 +1,26 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+
+
+class EmailOrUsernameModelBackend(ModelBackend):
+ #sjt 自定义认证后端:支持使用用户名或邮箱登录
+
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ #sjt 认证逻辑:根据输入判断是邮箱还是用户名,验证密码
+ if '@' in username: # sjt 包含@符号则视为邮箱登录
+ kwargs = {'email': username}
+ else: # sjt 否则视为用户名登录
+ kwargs = {'username': username}
+ try:
+ user = get_user_model().objects.get(** kwargs) # sjt 查询用户
+ if user.check_password(password): # sjt 验证密码
+ return user
+ except get_user_model().DoesNotExist: # sjt 用户不存在
+ return None
+
+ def get_user(self, username):
+ #sjt 根据用户ID获取用户对象
+ try:
+ return get_user_model().objects.get(pk=username)
+ except get_user_model().DoesNotExist:
+ return None
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/utils.py b/doc/DjangoBlog/accounts/utils.py
new file mode 100644
index 0000000..e1f9194
--- /dev/null
+++ b/doc/DjangoBlog/accounts/utils.py
@@ -0,0 +1,46 @@
+import typing
+from datetime import timedelta
+
+from django.core.cache import cache
+from django.utils.translation import gettext
+from django.utils.translation import gettext_lazy as _
+
+from djangoblog.utils import send_email
+
+_code_ttl = timedelta(minutes=5) # sjt 验证码有效期(5分钟)
+
+
+def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
+ '''sjt 发送密码重置验证码邮件
+ Args:
+ to_mail: 接收邮箱
+ subject: 邮件主题
+ code: 验证码
+ '''
+ html_content = _(
+ "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it "
+ "properly") % {'code': code}
+ send_email([to_mail], subject, html_content)
+
+
+def verify(email: str, code: str) -> typing.Optional[str]:
+ '''sjt 验证验证码有效性
+ Args:
+ email: 请求邮箱
+ code: 验证码
+ Return:
+ 如果有错误返回错误信息字符串,否则返回None
+ '''
+ cache_code = get_code(email) # sjt 从缓存获取验证码
+ if cache_code != code: # sjt 验证码不匹配
+ return gettext("Verification code error")
+
+
+def set_code(email: str, code: str):
+ #sjt 将验证码存入缓存(有效期5分钟)
+ cache.set(email, code, _code_ttl.seconds)
+
+
+def get_code(email: str) -> typing.Optional[str]:
+ #sjt 从缓存获取验证码
+ return cache.get(email)
\ No newline at end of file
diff --git a/doc/DjangoBlog/accounts/views.py b/doc/DjangoBlog/accounts/views.py
new file mode 100644
index 0000000..56d2540
--- /dev/null
+++ b/doc/DjangoBlog/accounts/views.py
@@ -0,0 +1,225 @@
+import logging
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth import get_user_model
+from django.contrib.auth import logout
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.hashers import make_password
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.views import View
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
+from django.views.generic import FormView, RedirectView
+
+from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
+from . import utils
+from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
+from .models import BlogUser
+
+logger = logging.getLogger(__name__)
+
+
+# Create your views here.
+
+class RegisterView(FormView):
+ #sjt 用户注册视图:处理注册表单提交,发送邮箱验证链接
+ form_class = RegisterForm # sjt 注册表单类
+ template_name = 'account/registration_form.html' # sjt 注册页面模板
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ #sjt 处理请求分发,添加CSRF保护
+ return super(RegisterView, self).dispatch(*args, **kwargs)
+
+ def form_valid(self, form):
+ #sjt 表单验证通过后:创建未激活用户,生成验证链接并发送邮件
+ if form.is_valid():
+ user = form.save(False) # sjt 不立即保存用户
+ user.is_active = False # sjt 初始为未激活状态(需邮箱验证)
+ user.source = 'Register' # sjt 标记来源为注册
+ user.save(True) # sjt 保存用户
+ site = get_current_site().domain # sjt 获取当前站点域名
+ # sjt 生成双重加密签名(防止链接篡改)
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+
+ if settings.DEBUG: # sjt 开发环境下使用本地域名
+ site = '127.0.0.1:8000'
+ path = reverse('account:result') # sjt 获取结果页路由
+ # sjt 构建验证链接
+ url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
+ site=site, path=path, id=user.id, sign=sign)
+
+ # sjt 构建验证邮件内容
+ content = """
+ 请点击下面链接验证您的邮箱
+
+ {url}
+
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {url}
+ """.format(url=url)
+ # sjt 发送验证邮件
+ send_email(
+ emailto=[
+ user.email,
+ ],
+ title='验证您的电子邮箱',
+ content=content)
+
+ # sjt 重定向到注册结果页
+ url = reverse('accounts:result') + \
+ '?type=register&id=' + str(user.id)
+ return HttpResponseRedirect(url)
+ else:
+ return self.render_to_response({
+ 'form': form
+ })
+
+
+class LogoutView(RedirectView):
+ #sjt 用户注销视图:处理用户注销并跳转登录页
+ url = '/login/' # sjt 注销后跳转的URL
+
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ #sjt 处理请求分发,禁止缓存
+ return super(LogoutView, self).dispatch(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ #sjt 处理GET请求:执行注销操作并清除缓存
+ logout(request) # sjt 注销用户
+ delete_sidebar_cache() # sjt 删除侧边栏缓存
+ return super(LogoutView, self).get(request, *args, **kwargs)
+
+
+class LoginView(FormView):
+ #sjt 用户登录视图:支持用户名/邮箱登录,处理登录状态
+ form_class = LoginForm # sjt 登录表单类
+ template_name = 'account/login.html' # sjt 登录页面模板
+ success_url = '/' # sjt 登录成功默认跳转页
+ redirect_field_name = REDIRECT_FIELD_NAME # sjt 重定向字段名
+ login_ttl = 2626560 # sjt 记住登录状态的有效期(一个月,单位秒)
+
+ @method_decorator(sensitive_post_parameters('password'))
+ @method_decorator(csrf_protect)
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ #sjt 处理请求分发:保护敏感参数、CSRF保护、禁止缓存
+ return super(LoginView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ #sjt 构建上下文数据:获取重定向URL
+ redirect_to = self.request.GET.get(self.redirect_field_name)
+ if redirect_to is None:
+ redirect_to = '/'
+ kwargs['redirect_to'] = redirect_to
+
+ return super(LoginView, self).get_context_data(** kwargs)
+
+ def form_valid(self, form):
+ #sjt 表单验证通过后:验证用户凭据,处理登录状态(含"记住我"功能)
+ form = AuthenticationForm(data=self.request.POST, request=self.request)
+
+ if form.is_valid(): # sjt 验证用户凭据
+ delete_sidebar_cache() # sjt 删除侧边栏缓存
+ logger.info(self.redirect_field_name)
+
+ auth.login(self.request, form.get_user()) # sjt 登录用户
+ if self.request.POST.get("remember"): # sjt 如果勾选"记住我"
+ self.request.session.set_expiry(self.login_ttl) # sjt 设置会话有效期
+ return super(LoginView, self).form_valid(form)
+ else:
+ return self.render_to_response({
+ 'form': form
+ })
+
+ def get_success_url(self):
+ #sjt 获取登录成功后的跳转URL(验证安全性)
+ redirect_to = self.request.POST.get(self.redirect_field_name)
+ # sjt 验证跳转URL是否安全
+ if not url_has_allowed_host_and_scheme(
+ url=redirect_to, allowed_hosts=[
+ self.request.get_host()]):
+ redirect_to = self.success_url
+ return redirect_to
+
+
+def account_result(request):
+ #sjt 注册/验证结果页:处理邮箱验证逻辑,激活用户账号
+ type = request.GET.get('type') # sjt 获取操作类型(注册/验证)
+ id = request.GET.get('id') # sjt 获取用户ID
+
+ user = get_object_or_404(get_user_model(), id=id) # sjt 获取用户对象
+ logger.info(type)
+ if user.is_active: # sjt 如果用户已激活,直接跳转首页
+ return HttpResponseRedirect('/')
+ if type and type in ['register', 'validation']:
+ if type == 'register': # sjt 注册成功结果页
+ content = '''
+ 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
+ '''
+ title = '注册成功'
+ else: # sjt 邮箱验证结果页
+ # sjt 验证签名是否正确
+ c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ sign = request.GET.get('sign')
+ if sign != c_sign: # sjt 签名错误返回403
+ return HttpResponseForbidden()
+ user.is_active = True # sjt 激活用户
+ user.save()
+ content = '''
+ 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
+ '''
+ title = '验证成功'
+ return render(request, 'account/result.html', { # sjt 渲染结果页
+ 'title': title,
+ 'content': content
+ })
+ else:
+ return HttpResponseRedirect('/')
+
+
+class ForgetPasswordView(FormView):
+ #sjt 密码找回视图:验证验证码后重置密码
+ form_class = ForgetPasswordForm # sjt 密码找回表单
+ template_name = 'account/forget_password.html' # sjt 密码找回页面模板
+
+ def form_valid(self, form):
+ #sjt 表单验证通过后:更新用户密码并跳转登录页
+ if form.is_valid():
+ blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
+ # sjt 加密并更新密码
+ blog_user.password = make_password(form.cleaned_data["new_password2"])
+ blog_user.save()
+ return HttpResponseRedirect('/login/') # sjt 跳转登录页
+ else:
+ return self.render_to_response({'form': form})
+
+
+class ForgetPasswordEmailCode(View):
+ #sjt 发送密码找回验证码视图:处理发送验证码请求
+
+ def post(self, request: HttpRequest):
+ #sjt 处理POST请求:验证邮箱并发送验证码
+ form = ForgetPasswordCodeForm(request.POST) # sjt 验证邮箱表单
+ if not form.is_valid(): # sjt 邮箱验证失败
+ return HttpResponse("错误的邮箱")
+ to_email = form.cleaned_data["email"] # sjt 获取目标邮箱
+
+ code = generate_code() # sjt 生成验证码
+ utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
+ utils.set_code(to_email, code) # sjt 缓存验证码(有效期5分钟)
+
+ return HttpResponse("ok") # sjt 发送成功返回"ok"
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/__init__.py b/doc/DjangoBlog/blog/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/blog/admin.py b/doc/DjangoBlog/blog/admin.py
new file mode 100644
index 0000000..d07430b
--- /dev/null
+++ b/doc/DjangoBlog/blog/admin.py
@@ -0,0 +1,144 @@
+from django import forms
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+# Register your models here.
+from .models import Article, Category, Tag, Links, SideBar, BlogSettings
+
+
+class ArticleForm(forms.ModelForm):
+ # body = forms.CharField(widget=AdminPagedownWidget())
+
+ class Meta:
+ # mk: 指定表单关联的模型和字段
+ model = Article
+ fields = '__all__'
+
+
+def makr_article_publish(modeladmin, request, queryset):
+ # mk: 批量发布文章操作,将选中的文章状态更新为已发布('p')
+ queryset.update(status='p')
+
+
+def draft_article(modeladmin, request, queryset):
+ # mk: 批量将文章设为草稿状态,将选中的文章状态更新为草稿('d')
+ queryset.update(status='d')
+
+
+def close_article_commentstatus(modeladmin, request, queryset):
+ # mk: 批量关闭文章评论功能,将选中的文章评论状态更新为关闭('c')
+ queryset.update(comment_status='c')
+
+
+def open_article_commentstatus(modeladmin, request, queryset):
+ # mk: 批量开启文章评论功能,将选中的文章评论状态更新为开启('o')
+ queryset.update(comment_status='o')
+
+
+# mk: 为自定义管理操作设置显示名称
+makr_article_publish.short_description = _('Publish selected articles')
+draft_article.short_description = _('Draft selected articles')
+close_article_commentstatus.short_description = _('Close article comments')
+open_article_commentstatus.short_description = _('Open article comments')
+
+
+class ArticlelAdmin(admin.ModelAdmin):
+ # mk: 设置文章管理界面每页显示20条记录
+ list_per_page = 20
+ # mk: 设置搜索字段,支持在文章正文和标题中搜索
+ search_fields = ('body', 'title')
+ # mk: 指定使用的表单类
+ form = ArticleForm
+ # mk: 设置在管理界面列表中显示的字段
+ list_display = (
+ 'id',
+ 'title',
+ 'author',
+ 'link_to_category',
+ 'creation_time',
+ 'views',
+ 'status',
+ 'type',
+ 'article_order')
+ # mk: 设置可以作为链接点击的字段
+ list_display_links = ('id', 'title')
+ # mk: 设置过滤器,可以在右侧边栏按状态、类型、分类筛选
+ list_filter = ('status', 'type', 'category')
+ # mk: 设置日期层级结构,用于按日期筛选文章
+ date_hierarchy = 'creation_time'
+ # mk: 设置多对多字段的横向筛选器
+ filter_horizontal = ('tags',)
+ # mk: 排除某些字段在表单中显示
+ exclude = ('creation_time', 'last_modify_time')
+ # mk: 启用"在站点上查看"功能
+ view_on_site = True
+ # mk: 注册自定义的批量操作
+ actions = [
+ makr_article_publish,
+ draft_article,
+ close_article_commentstatus,
+ open_article_commentstatus]
+ # mk: 设置使用原始ID字段的外键字段,提升性能
+ raw_id_fields = ('author', 'category',)
+
+ def link_to_category(self, obj):
+ # mk: 创建指向分类编辑页面的链接
+ 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'%s ' % (link, obj.category.name))
+
+ # mk: 设置分类链接字段的显示名称
+ link_to_category.short_description = _('category')
+
+ def get_form(self, request, obj=None, **kwargs):
+ # mk: 自定义表单,限制作者字段只能选择超级用户
+ form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
+ form.base_fields['author'].queryset = get_user_model(
+ ).objects.filter(is_superuser=True)
+ return form
+
+ def save_model(self, request, obj, form, change):
+ # mk: 保存模型实例,调用父类的保存方法
+ super(ArticlelAdmin, self).save_model(request, obj, form, change)
+
+ def get_view_on_site_url(self, obj=None):
+ # mk: 返回在站点上查看文章的URL
+ if obj:
+ url = obj.get_full_url()
+ return url
+ else:
+ from djangoblog.utils import get_current_site
+ site = get_current_site().domain
+ return site
+
+
+class TagAdmin(admin.ModelAdmin):
+ # mk: 标签管理界面排除某些字段
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+class CategoryAdmin(admin.ModelAdmin):
+ # mk: 设置分类管理界面显示的字段
+ list_display = ('name', 'parent_category', 'index')
+ # mk: 分类管理界面排除某些字段
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+class LinksAdmin(admin.ModelAdmin):
+ # mk: 链接管理界面排除某些字段
+ exclude = ('last_mod_time', 'creation_time')
+
+
+class SideBarAdmin(admin.ModelAdmin):
+ # mk: 设置侧边栏管理界面显示的字段
+ list_display = ('name', 'content', 'is_enable', 'sequence')
+ # mk: 侧边栏管理界面排除某些字段
+ exclude = ('last_mod_time', 'creation_time')
+
+
+class BlogSettingsAdmin(admin.ModelAdmin):
+ # mk: 博客设置管理界面使用默认配置
+ pass
diff --git a/doc/DjangoBlog/blog/apps.py b/doc/DjangoBlog/blog/apps.py
new file mode 100644
index 0000000..469872c
--- /dev/null
+++ b/doc/DjangoBlog/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig # mk:导入Django的应用配置类#
+
+
+class BlogConfig(AppConfig): # mk:定义博客应用的配置类,继承自AppConfig#
+ name = 'blog' # mk:指定应用的名称为'blog'#
diff --git a/doc/DjangoBlog/blog/context_processors.py b/doc/DjangoBlog/blog/context_processors.py
new file mode 100644
index 0000000..8964719
--- /dev/null
+++ b/doc/DjangoBlog/blog/context_processors.py
@@ -0,0 +1,77 @@
+import logging # mk:导入日志记录模块#
+
+from django.utils import timezone # mk:导入Django的时间工具模块#
+
+from djangoblog.utils import cache, get_blog_setting # mk:从项目工具模块导入缓存和获取博客设置的函数#
+from .models import Category, Article # mk:从当前应用的模型中导入分类和文章模型#
+
+logger = logging.getLogger(__name__) # mk:创建当前模块的日志记录器#
+
+
+def seo_processor(requests): # mk:SEO处理器函数,用于获取网站SEO相关配置信息并缓存#
+ """
+ SEO处理器函数,用于获取网站SEO相关配置信息并缓存
+
+ 该函数从缓存中获取SEO配置信息,如果缓存不存在则从数据库获取并设置缓存。
+ 主要包含网站基本信息、SEO配置、导航分类列表、页面列表等数据。
+
+ Args:
+ requests: HTTP请求对象,用于获取请求协议和主机信息
+
+ Returns:
+ dict: 包含SEO配置信息的字典,具体包含:
+ - SITE_NAME: 网站名称
+ - SHOW_GOOGLE_ADSENSE: 是否显示Google Adsense
+ - GOOGLE_ADSENSE_CODES: Google Adsense代码
+ - SITE_SEO_DESCRIPTION: 网站SEO描述
+ - SITE_DESCRIPTION: 网站描述
+ - SITE_KEYWORDS: 网站关键词
+ - SITE_BASE_URL: 网站基础URL
+ - ARTICLE_SUB_LENGTH: 文章摘要长度
+ - nav_category_list: 导航分类列表
+ - nav_pages: 导航页面列表
+ - OPEN_SITE_COMMENT: 是否开启网站评论
+ - BEIAN_CODE: 备案号
+ - ANALYTICS_CODE: 统计代码
+ - BEIAN_CODE_GONGAN: 公安备案号
+ - SHOW_GONGAN_CODE: 是否显示公安备案号
+ - CURRENT_YEAR: 当前年份
+ - GLOBAL_HEADER: 全局头部代码
+ - GLOBAL_FOOTER: 全局底部代码
+ - COMMENT_NEED_REVIEW: 评论是否需要审核
+ """
+ key = 'seo_processor' # mk:设置缓存键名#
+ value = cache.get(key) # mk:尝试从缓存中获取SEO数据#
+ # mk:检查缓存是否存在,如果存在则直接返回缓存数据#
+ if value:
+ return value
+ else:
+ logger.info('set processor cache.') # mk:记录设置缓存的日志信息#
+ # mk:缓存不存在,从数据库获取配置信息#
+ setting = get_blog_setting()
+ value = { # mk:构建包含网站SEO配置和导航数据的字典对象#
+ 'SITE_NAME': setting.site_name, # mk:网站名称,显示在网页标题等位置#
+ 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # mk:是否显示Google Adsense广告的开关#
+ 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # mk:Google Adsense广告代码内容#
+ 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # mk:网站SEO描述信息,用于meta标签#
+ 'SITE_DESCRIPTION': setting.site_description, # mk:网站描述信息#
+ 'SITE_KEYWORDS': setting.site_keywords, # mk:网站关键词,用于SEO优化#
+ 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # mk:网站基础URL地址,由请求协议和主机名组成#
+ 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # mk:文章摘要显示长度设置#
+ 'nav_category_list': Category.objects.all(), # mk:获取所有文章分类,用于导航菜单显示#
+ 'nav_pages': Article.objects.filter( # mk:获取所有已发布的页面文章,用于导航菜单显示#
+ type='p',
+ status='p'),
+ 'OPEN_SITE_COMMENT': setting.open_site_comment, # mk:网站评论功能开关设置#
+ 'BEIAN_CODE': setting.beian_code, # mk:网站备案号信息#
+ 'ANALYTICS_CODE': setting.analytics_code, # mk:网站统计分析代码#
+ "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # mk:公安备案号信息#
+ "SHOW_GONGAN_CODE": setting.show_gongan_code, # mk:是否显示公安备案信息的开关#
+ "CURRENT_YEAR": timezone.now().year, # mk:当前年份,用于页面底部显示版权年份#
+ "GLOBAL_HEADER": setting.global_header, # mk:全局页头HTML代码#
+ "GLOBAL_FOOTER": setting.global_footer, # mk:全局页脚HTML代码#
+ "COMMENT_NEED_REVIEW": setting.comment_need_review, # mk:评论是否需要审核的设置#
+ }
+ # mk:将获取到的数据缓存10小时,减少数据库查询压力#
+ cache.set(key, value, 60 * 60 * 10)
+ return value # mk:返回构建的SEO和导航数据#
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/documents.py b/doc/DjangoBlog/blog/documents.py
new file mode 100644
index 0000000..9347269
--- /dev/null
+++ b/doc/DjangoBlog/blog/documents.py
@@ -0,0 +1,322 @@
+import time
+
+import elasticsearch.client
+from django.conf import settings
+from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
+from elasticsearch_dsl.connections import connections
+
+from blog.models import Article
+
+# mk:检查是否启用了Elasticsearch配置
+ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
+
+if ELASTICSEARCH_ENABLED:
+ # mk:创建Elasticsearch连接
+ connections.create_connection(
+ hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
+ from elasticsearch import Elasticsearch
+
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ from elasticsearch.client import IngestClient
+
+ c = IngestClient(es)
+ try:
+ # mk:尝试获取geoip管道,如果不存在则创建
+ c.get_pipeline('geoip')
+ except elasticsearch.exceptions.NotFoundError:
+ c.put_pipeline('geoip', body='''{
+ "description" : "Add geoip info",
+ "processors" : [
+ {
+ "geoip" : {
+ "field" : "ip"
+ }
+ }
+ ]
+ }''')
+
+
+class GeoIp(InnerDoc):
+ """
+ mk:
+ 地理位置信息文档类
+ 用于存储IP地址对应的地理位置信息
+ """
+ continent_name = Keyword()
+ country_iso_code = Keyword()
+ country_name = Keyword()
+ location = GeoPoint()
+
+
+class UserAgentBrowser(InnerDoc):
+ """
+ mk:
+ 用户代理浏览器信息类
+ 存储浏览器的家族和版本信息
+ """
+ Family = Keyword()
+ Version = Keyword()
+
+
+class UserAgentOS(UserAgentBrowser):
+ """
+ mk:
+ 用户代理操作系统信息类
+ 继承自UserAgentBrowser,存储操作系统的家族和版本信息
+ """
+ pass
+
+
+class UserAgentDevice(InnerDoc):
+ """
+ mk:
+ 用户代理设备信息类
+ 存储设备的家族、品牌和型号信息
+ """
+ Family = Keyword()
+ Brand = Keyword()
+ Model = Keyword()
+
+
+class UserAgent(InnerDoc):
+ """
+ mk:
+ 用户代理完整信息类
+ 包含浏览器、操作系统、设备等完整用户代理信息
+ """
+ browser = Object(UserAgentBrowser, required=False)
+ os = Object(UserAgentOS, required=False)
+ device = Object(UserAgentDevice, required=False)
+ string = Text()
+ is_bot = Boolean()
+
+
+class ElapsedTimeDocument(Document):
+ """
+ mk:
+ 性能监控文档类
+ 用于记录页面访问性能数据,包括URL、响应时间、访问时间等信息
+ """
+ url = Keyword()
+ time_taken = Long()
+ log_datetime = Date()
+ ip = Keyword()
+ geoip = Object(GeoIp, required=False)
+ useragent = Object(UserAgent, required=False)
+
+ class Index:
+ name = 'performance'
+ settings = {
+ "number_of_shards": 1,
+ "number_of_replicas": 0
+ }
+
+ class Meta:
+ doc_type = 'ElapsedTime'
+
+
+class ElaspedTimeDocumentManager:
+ """
+ mk:
+ 性能监控文档管理类
+ 提供性能监控数据的索引创建、删除和保存功能
+ """
+
+ @staticmethod
+ def build_index():
+ """
+ mk:
+ 构建性能监控索引
+ 检查索引是否存在,如果不存在则初始化索引
+ """
+ from elasticsearch import Elasticsearch
+ client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ res = client.indices.exists(index="performance")
+ if not res:
+ ElapsedTimeDocument.init()
+
+ @staticmethod
+ def delete_index():
+ """
+ mk:
+ 删除性能监控索引
+ 删除名为'performance'的索引,忽略400和404错误
+ """
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ es.indices.delete(index='performance', ignore=[400, 404])
+
+ @staticmethod
+ def create(url, time_taken, log_datetime, useragent, ip):
+ """
+ mk:
+ 创建并保存性能监控记录
+
+ Args:
+ url (str): 访问的URL地址
+ time_taken (int): 请求耗时(毫秒)
+ log_datetime (datetime): 日志记录时间
+ useragent (object): 用户代理对象,包含浏览器、系统、设备信息
+ ip (str): 访问者IP地址
+ """
+ ElaspedTimeDocumentManager.build_index()
+ ua = UserAgent()
+ ua.browser = UserAgentBrowser()
+ ua.browser.Family = useragent.browser.family
+ ua.browser.Version = useragent.browser.version_string
+
+ ua.os = UserAgentOS()
+ ua.os.Family = useragent.os.family
+ ua.os.Version = useragent.os.version_string
+
+ ua.device = UserAgentDevice()
+ ua.device.Family = useragent.device.family
+ ua.device.Brand = useragent.device.brand
+ ua.device.Model = useragent.device.model
+ ua.string = useragent.ua_string
+ ua.is_bot = useragent.is_bot
+
+ doc = ElapsedTimeDocument(
+ meta={
+ 'id': int(
+ round(
+ time.time() *
+ 1000))
+ },
+ url=url,
+ time_taken=time_taken,
+ log_datetime=log_datetime,
+ useragent=ua, ip=ip)
+ doc.save(pipeline="geoip")
+
+
+class ArticleDocument(Document):
+ """
+ mk:
+ 文章文档类
+ 用于Elasticsearch中的文章搜索索引,包含文章的完整信息
+ """
+ body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ author = Object(properties={
+ 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ category = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ tags = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+
+ pub_time = Date()
+ status = Text()
+ comment_status = Text()
+ type = Text()
+ views = Integer()
+ article_order = Integer()
+
+ class Index:
+ name = 'blog'
+ settings = {
+ "number_of_shards": 1,
+ "number_of_replicas": 0
+ }
+
+ class Meta:
+ doc_type = 'Article'
+
+
+class ArticleDocumentManager():
+ """
+ mk:
+ 文章文档管理类
+ 提供文章索引的创建、删除、重建和更新功能
+ """
+
+ def __init__(self):
+ """
+ mk:
+ 初始化文章文档管理器
+ 自动创建索引
+ """
+ self.create_index()
+
+ def create_index(self):
+ """
+ mk:
+ 创建文章索引
+ 初始化ArticleDocument索引结构
+ """
+ ArticleDocument.init()
+
+ def delete_index(self):
+ """
+ mk:
+ 删除文章索引
+ 删除名为'blog'的索引,忽略400和404错误
+ """
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ es.indices.delete(index='blog', ignore=[400, 404])
+
+ def convert_to_doc(self, articles):
+ """
+ mk:
+ 将文章模型对象转换为文档对象
+
+ Args:
+ articles (list): Article模型对象列表
+
+ Returns:
+ list: 转换后的ArticleDocument文档对象列表
+ """
+ return [
+ ArticleDocument(
+ meta={
+ 'id': article.id},
+ body=article.body,
+ title=article.title,
+ author={
+ 'nickname': article.author.username,
+ 'id': article.author.id},
+ category={
+ 'name': article.category.name,
+ 'id': article.category.id},
+ tags=[
+ {
+ 'name': t.name,
+ 'id': t.id} for t in article.tags.all()],
+ pub_time=article.pub_time,
+ status=article.status,
+ comment_status=article.comment_status,
+ type=article.type,
+ views=article.views,
+ article_order=article.article_order) for article in articles]
+
+ def rebuild(self, articles=None):
+ """
+ mk:
+ 重建文章索引
+
+ Args:
+ articles (list, optional): 指定要重建索引的文章列表,如果为None则重建所有文章
+ """
+ ArticleDocument.init()
+ articles = articles if articles else Article.objects.all()
+ docs = self.convert_to_doc(articles)
+ for doc in docs:
+ doc.save()
+
+ def update_docs(self, docs):
+ """
+ mk:
+ 批量更新文档
+
+ Args:
+ docs (list): ArticleDocument文档对象列表
+ """
+ for doc in docs:
+ doc.save()
diff --git a/doc/DjangoBlog/blog/forms.py b/doc/DjangoBlog/blog/forms.py
new file mode 100644
index 0000000..c7b8fe7
--- /dev/null
+++ b/doc/DjangoBlog/blog/forms.py
@@ -0,0 +1,39 @@
+import logging
+
+from django import forms
+from haystack.forms import SearchForm
+
+logger = logging.getLogger(__name__)
+
+
+class BlogSearchForm(SearchForm):
+ """
+ mk:
+ 博客搜索表单类
+
+ 继承自haystack的SearchForm,添加了自定义的查询字段和搜索逻辑
+ """
+ querydata = forms.CharField(required=True)
+
+ def search(self):
+ """
+ mk:
+ 执行搜索操作
+
+ 首先调用父类的搜索方法获取基础搜索结果,然后验证表单数据的有效性,
+ 如果表单数据有效且查询数据存在,则记录查询日志
+
+ Returns:
+ 搜索结果数据集
+ """
+ # mk:调用父类的搜索方法获取基础搜索结果
+ datas = super(BlogSearchForm, self).search()
+
+ # mk:验证表单数据,如果无效则返回无查询结果
+ if not self.is_valid():
+ return self.no_query_found()
+
+ # mk:如果查询数据存在,记录查询日志
+ if self.cleaned_data['querydata']:
+ logger.info(self.cleaned_data['querydata'])
+ return datas
diff --git a/doc/DjangoBlog/blog/management/__init__.py b/doc/DjangoBlog/blog/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/blog/management/commands/__init__.py b/doc/DjangoBlog/blog/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/blog/management/commands/build_index.py b/doc/DjangoBlog/blog/management/commands/build_index.py
new file mode 100644
index 0000000..69c8da8
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/build_index.py
@@ -0,0 +1,40 @@
+from django.core.management.base import BaseCommand
+
+from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
+ ELASTICSEARCH_ENABLED
+
+
+# mk:TODO 参数化
+class Command(BaseCommand):
+ """
+ mk:
+ Django管理命令类,用于构建搜索索引
+
+ 该命令负责初始化和重建Elasticsearch索引,包括_elapsed_time和article两种文档类型
+ """
+ help = 'build search index'
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 处理命令执行逻辑
+
+ Args:
+ *args: 位置参数
+ **options: 命令行选项参数
+
+ Returns:
+ None
+ """
+ # mk:检查Elasticsearch是否启用
+ if ELASTICSEARCH_ENABLED:
+ # mk:构建_elapsed_time文档的索引
+ ElaspedTimeDocumentManager.build_index()
+ manager = ElapsedTimeDocument()
+ manager.init()
+
+ # mk:重新构建article文档的索引
+ manager = ArticleDocumentManager()
+ manager.delete_index()
+ manager.rebuild()
+
diff --git a/doc/DjangoBlog/blog/management/commands/build_search_words.py b/doc/DjangoBlog/blog/management/commands/build_search_words.py
new file mode 100644
index 0000000..f0f1f30
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/build_search_words.py
@@ -0,0 +1,36 @@
+from django.core.management.base import BaseCommand
+
+from blog.models import Tag, Category
+
+
+# mk:TODO 参数化
+class Command(BaseCommand):
+ """
+ mk:
+ Django管理命令类,用于构建搜索词列表
+
+ 该命令从Tag和Category模型中提取名称数据,合并去重后输出
+ """
+ help = 'build search words'
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 处理命令的主要逻辑
+
+ 从Tag和Category模型中获取所有名称,合并为一个去重集合,
+ 然后将每个名称作为独立行打印输出
+
+ Args:
+ *args: 位置参数
+ **options: 命令行选项参数
+
+ Returns:
+ None
+ """
+ #mk: 从Tag和Category模型中提取所有名称并合并去重
+ datas = set([t.name for t in Tag.objects.all()] +
+ [t.name for t in Category.objects.all()])
+ #mk: 将去重后的名称集合按行打印输出
+ print('\n'.join(datas))
+
diff --git a/doc/DjangoBlog/blog/management/commands/clear_cache.py b/doc/DjangoBlog/blog/management/commands/clear_cache.py
new file mode 100644
index 0000000..01bb34a
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/clear_cache.py
@@ -0,0 +1,32 @@
+from django.core.management.base import BaseCommand
+
+from djangoblog.utils import cache
+
+
+class Command(BaseCommand):
+ """
+ mk:
+ Django管理命令类,用于清除整个缓存
+
+ 该类继承自Django的BaseCommand,提供了一个自定义的管理命令
+ 可以通过命令行调用来清除应用的所有缓存数据
+ """
+ help = 'clear the whole cache'
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 处理管理命令的主要逻辑
+
+ 参数:
+ *args: 位置参数元组
+ **options: 关键字参数字典
+
+ 返回值:
+ None
+ """
+ # mk:清除所有缓存数据
+ cache.clear()
+ # mk:输出成功信息到标准输出
+ self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
+
diff --git a/doc/DjangoBlog/blog/management/commands/create_testdata.py b/doc/DjangoBlog/blog/management/commands/create_testdata.py
new file mode 100644
index 0000000..b5cfb14
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/create_testdata.py
@@ -0,0 +1,67 @@
+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):
+ """
+ mk:
+ Django管理命令类,用于创建测试数据
+
+ 该命令会创建测试用户、分类、标签和文章数据,用于开发和测试环境
+ 继承自Django的BaseCommand基类
+ """
+ help = 'create test datas'
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 处理命令执行的主要逻辑
+
+ 参数:
+ *args: 位置参数元组
+ **options: 命令行选项字典
+
+ 返回值:
+ None
+ """
+ # mk:创建或获取测试用户
+ user = get_user_model().objects.get_or_create(
+ email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
+
+ # mk:创建父级分类
+ pcategory = Category.objects.get_or_create(
+ name='我是父类目', parent_category=None)[0]
+
+ # mk:创建子级分类
+ category = Category.objects.get_or_create(
+ name='子类目', parent_category=pcategory)[0]
+
+ category.save()
+
+ # mk:创建基础标签
+ basetag = Tag()
+ basetag.name = "标签"
+ basetag.save()
+
+ # mk:批量创建20篇文章及其对应标签
+ for i in range(1, 20):
+ article = Article.objects.get_or_create(
+ category=category,
+ title='nice title ' + str(i),
+ body='nice content ' + str(i),
+ author=user)[0]
+ tag = Tag()
+ tag.name = "标签" + str(i)
+ tag.save()
+ article.tags.add(tag)
+ article.tags.add(basetag)
+ article.save()
+
+ #mk: 清除缓存并输出成功信息
+ from djangoblog.utils import cache
+ cache.clear()
+ self.stdout.write(self.style.SUCCESS('created test datas \n'))
+
diff --git a/doc/DjangoBlog/blog/management/commands/ping_baidu.py b/doc/DjangoBlog/blog/management/commands/ping_baidu.py
new file mode 100644
index 0000000..06c62bb
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/ping_baidu.py
@@ -0,0 +1,86 @@
+from django.core.management.base import BaseCommand
+
+from djangoblog.spider_notify import SpiderNotify
+from djangoblog.utils import get_current_site
+from blog.models import Article, Tag, Category
+
+site = get_current_site().domain
+
+
+class Command(BaseCommand):
+ """
+ mk:
+ Django管理命令类,用于向百度搜索引擎推送网站URL
+
+ 该命令支持推送文章、标签、分类等不同类型的页面URL到百度搜索引擎,
+ 以提高网站内容的收录效率。
+
+ Attributes:
+ help (str): 命令帮助信息
+ """
+ help = 'notify baidu url'
+
+ def add_arguments(self, parser):
+ """
+ mk:
+ 添加命令行参数
+
+ :param parser: 参数解析器对象,用于定义命令行参数
+ :return: None
+ """
+ parser.add_argument(
+ 'data_type',
+ type=str,
+ choices=[
+ 'all',
+ 'article',
+ 'tag',
+ 'category'],
+ help='article : all article,tag : all tag,category: all category,all: All of these')
+
+ def get_full_url(self, path):
+ """
+ mk:
+ 根据相对路径生成完整的HTTPS URL
+
+ :param path: 相对路径
+ :return: 完整的HTTPS URL字符串
+ """
+ url = "https://{site}{path}".format(site=site, path=path)
+ return url
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 命令处理函数,根据指定的数据类型收集URL并推送到百度
+
+ :param args: 位置参数
+ :param options: 命令行选项字典,包含data_type键
+ :return: None
+ """
+ type = options['data_type']
+ self.stdout.write('start get %s' % type)
+
+ urls = []
+ # mk:收集文章URL
+ if type == 'article' or type == 'all':
+ for article in Article.objects.filter(status='p'):
+ urls.append(article.get_full_url())
+ # mk:收集标签URL
+ if type == 'tag' or type == 'all':
+ for tag in Tag.objects.all():
+ url = tag.get_absolute_url()
+ urls.append(self.get_full_url(url))
+ # mk:收集分类URL
+ if type == 'category' or type == 'all':
+ for category in Category.objects.all():
+ url = category.get_absolute_url()
+ urls.append(self.get_full_url(url))
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ 'start notify %d urls' %
+ len(urls)))
+ SpiderNotify.baidu_notify(urls)
+ self.stdout.write(self.style.SUCCESS('finish notify'))
+
diff --git a/doc/DjangoBlog/blog/management/commands/sync_user_avatar.py b/doc/DjangoBlog/blog/management/commands/sync_user_avatar.py
new file mode 100644
index 0000000..0e36126
--- /dev/null
+++ b/doc/DjangoBlog/blog/management/commands/sync_user_avatar.py
@@ -0,0 +1,82 @@
+import requests
+from django.core.management.base import BaseCommand
+from django.templatetags.static import static
+
+from djangoblog.utils import save_user_avatar
+from oauth.models import OAuthUser
+from oauth.oauthmanager import get_manager_by_type
+
+
+class Command(BaseCommand):
+ """
+ mk:
+ Django管理命令类,用于同步用户头像
+
+ 该命令会遍历所有OAuth用户,检查并更新他们的头像URL,
+ 确保头像图片可以正常访问,如果无法访问则尝试重新获取或使用默认头像
+ """
+ help = 'sync user avatar'
+
+ def test_picture(self, url):
+ """
+ mk:
+ 测试图片URL是否可以正常访问
+
+ Args:
+ url (str): 要测试的图片URL地址
+
+ Returns:
+ bool: 如果URL可以正常访问返回True,否则返回False
+ """
+ try:
+ if requests.get(url, timeout=2).status_code == 200:
+ return True
+ except:
+ pass
+
+ def handle(self, *args, **options):
+ """
+ mk:
+ 命令处理函数,执行用户头像同步逻辑
+
+ Args:
+ *args: 位置参数
+ **options: 命令行选项参数
+
+ Returns:
+ None
+ """
+ static_url = static("../")
+ users = OAuthUser.objects.all()
+ self.stdout.write(f'开始同步{len(users)}个用户头像')
+ for u in users:
+ self.stdout.write(f'开始同步:{u.nickname}')
+ url = u.picture
+ if url:
+ # mk:如果头像URL以静态URL开头,说明可能是本地资源
+ if url.startswith(static_url):
+ #mk: 测试图片是否可以正常访问
+ if self.test_picture(url):
+ continue
+ else:
+ # mk:如果无法访问且用户有元数据,则通过OAuth管理器重新获取头像
+ if u.metadata:
+ manage = get_manager_by_type(u.type)
+ url = manage.get_picture(u.metadata)
+ url = save_user_avatar(url)
+ else:
+ # mk:否则使用默认头像
+ url = static('blog/img/avatar.png')
+ else:
+ # mk:对于非本地资源,直接保存用户头像
+ url = save_user_avatar(url)
+ else:
+ # mk:如果没有头像URL,使用默认头像
+ url = static('blog/img/avatar.png')
+ if url:
+ self.stdout.write(
+ f'结束同步:{u.nickname}.url:{url}')
+ u.picture = url
+ u.save()
+ self.stdout.write('结束同步')
+
diff --git a/doc/DjangoBlog/blog/middleware.py b/doc/DjangoBlog/blog/middleware.py
new file mode 100644
index 0000000..53912cc
--- /dev/null
+++ b/doc/DjangoBlog/blog/middleware.py
@@ -0,0 +1,69 @@
+import logging
+import time
+
+from ipware import get_client_ip
+from user_agents import parse
+
+from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
+
+logger = logging.getLogger(__name__)
+
+
+class OnlineMiddleware(object):
+ """
+ mk:
+ 在线用户中间件类,用于记录页面加载时间和用户访问信息
+
+ 该中间件会在每个请求处理前后记录时间,计算页面渲染耗时,
+ 并将相关信息存储到Elasticsearch中(如果启用的话)。
+
+ Args:
+ get_response: Django中间件的get_response回调函数
+ """
+ def __init__(self, get_response=None):
+ self.get_response = get_response
+ super().__init__()
+
+ def __call__(self, request):
+ """
+ mk:
+ 中间件调用方法,处理请求并记录页面加载时间
+
+ 记录请求开始时间,处理请求,计算耗时,并将相关信息存储到Elasticsearch。
+ 同时替换响应内容中的加载时间占位符。
+
+ Args:
+ request: Django HttpRequest对象,包含当前请求信息
+
+ Returns:
+ HttpResponse: 处理后的HTTP响应对象
+ """
+ ''' page render time '''
+ start_time = time.time()
+ response = self.get_response(request)
+ http_user_agent = request.META.get('HTTP_USER_AGENT', '')
+ ip, _ = get_client_ip(request)
+ user_agent = parse(http_user_agent)
+ if not response.streaming:
+ try:
+ # mk:计算页面渲染耗时
+ cast_time = time.time() - start_time
+ if ELASTICSEARCH_ENABLED:
+ # mk:将耗时转换为毫秒并四舍五入到小数点后两位
+ time_taken = round((cast_time) * 1000, 2)
+ url = request.path
+ from django.utils import timezone
+ # mk:创建Elasticsearch文档记录
+ ElaspedTimeDocumentManager.create(
+ url=url,
+ time_taken=time_taken,
+ log_datetime=timezone.now(),
+ useragent=user_agent,
+ ip=ip)
+ # mk:替换响应内容中的加载时间占位符
+ response.content = response.content.replace(
+ b'', str.encode(str(cast_time)[:5]))
+ except Exception as e:
+ logger.error("Error OnlineMiddleware: %s" % e)
+
+ return response
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/migrations/0001_initial.py b/doc/DjangoBlog/blog/migrations/0001_initial.py
new file mode 100644
index 0000000..58397f7
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0001_initial.py
@@ -0,0 +1,158 @@
+#mk: Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import mdeditor.fields
+
+
+class Migration(migrations.Migration):
+ """
+ mk:
+ Django 数据库迁移类,用于初始化博客应用所需的数据库表结构。
+
+ 该迁移文件定义了博客系统所需的核心数据模型,包括网站配置、友情链接、侧边栏、标签、分类和文章等。
+ """
+
+ # mk:标记此迁移为初始迁移
+ initial = True
+
+ # mk:定义依赖关系,确保在用户模型之后执行
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ # mk:定义具体的数据库操作
+ operations = [
+ # mk:创建网站配置模型 BlogSettings
+ migrations.CreateModel(
+ name='BlogSettings',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
+ ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
+ ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
+ ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
+ ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
+ ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
+ ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
+ ('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
+ ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
+ ('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
+ ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
+ ('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
+ ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
+ ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
+ ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
+ ],
+ options={
+ 'verbose_name': '网站配置',
+ 'verbose_name_plural': '网站配置',
+ },
+ ),
+
+ # mk:创建友情链接模型 Links
+ migrations.CreateModel(
+ name='Links',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
+ ('link', models.URLField(verbose_name='链接地址')),
+ ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
+ ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ],
+ options={
+ 'verbose_name': '友情链接',
+ 'verbose_name_plural': '友情链接',
+ 'ordering': ['sequence'],
+ },
+ ),
+
+ # mk:创建侧边栏模型 SideBar
+ migrations.CreateModel(
+ name='SideBar',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100, verbose_name='标题')),
+ ('content', models.TextField(verbose_name='内容')),
+ ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ],
+ options={
+ 'verbose_name': '侧边栏',
+ 'verbose_name_plural': '侧边栏',
+ 'ordering': ['sequence'],
+ },
+ ),
+
+ # mk:创建标签模型 Tag
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
+ ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
+ ],
+ options={
+ 'verbose_name': '标签',
+ 'verbose_name_plural': '标签',
+ 'ordering': ['name'],
+ },
+ ),
+
+ # mk:创建分类模型 Category
+ migrations.CreateModel(
+ name='Category',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
+ ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
+ ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
+ ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
+ ],
+ options={
+ 'verbose_name': '分类',
+ 'verbose_name_plural': '分类',
+ 'ordering': ['-index'],
+ },
+ ),
+
+ # mk:创建文章模型 Article
+ migrations.CreateModel(
+ name='Article',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
+ ('body', mdeditor.fields.MDTextField(verbose_name='正文')),
+ ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
+ ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
+ ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
+ ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
+ ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
+ ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
+ ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
+ ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
+ ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
+ ],
+ options={
+ 'verbose_name': '文章',
+ 'verbose_name_plural': '文章',
+ 'ordering': ['-article_order', '-pub_time'],
+ 'get_latest_by': 'id',
+ },
+ ),
+ ]
+
diff --git a/doc/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py b/doc/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py
new file mode 100644
index 0000000..dd81fef
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -0,0 +1,34 @@
+#mk: Generated by Django 4.1.7 on 2023-03-29 06:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ """
+ mk:
+ Django数据库迁移类,用于向BlogSettings模型添加全局头部和尾部字段
+
+ 该迁移依赖于blog应用的0001_initial初始迁移,包含了两个操作:
+ 1. 向BlogSettings模型添加global_footer字段
+ 2. 向BlogSettings模型添加global_header字段
+ """
+
+ dependencies = [
+ ('blog', '0001_initial'),
+ ]
+
+ operations = [
+ # mk:添加全局尾部字段到BlogSettings模型
+ migrations.AddField(
+ model_name='blogsettings',
+ name='global_footer',
+ field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
+ ),
+ # mk:添加全局头部字段到BlogSettings模型
+ migrations.AddField(
+ model_name='blogsettings',
+ name='global_header',
+ field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
+ ),
+ ]
+
diff --git a/doc/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py b/doc/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py
new file mode 100644
index 0000000..05c3f38
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -0,0 +1,27 @@
+# mk:Generated by Django 4.2.1 on 2023-05-09 07:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ """
+ mk:
+ 数据库迁移类,用于向BlogSettings模型添加评论审核功能
+
+ 该迁移依赖于blog应用的0002_blogsettings_global_footer_and_more迁移,
+ 主要操作是为BlogSettings模型添加一个布尔类型的字段,用于控制
+ 评论是否需要管理员审核后才能显示。
+ """
+ dependencies = [
+ ('blog', '0002_blogsettings_global_footer_and_more'),
+ ]
+
+ operations = [
+ # mk:添加评论审核控制字段到BlogSettings模型
+ migrations.AddField(
+ model_name='blogsettings',
+ name='comment_need_review',
+ field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
+ ),
+ ]
+
diff --git a/doc/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/doc/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
new file mode 100644
index 0000000..08bbf6d
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -0,0 +1,45 @@
+# mk:Generated by Django 4.2.1 on 2023-05-09 07:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ """"
+ mk:
+ 数据库迁移类,用于重命名博客设置模型中的字段名
+
+ 该迁移将以下字段进行重命名:
+ - analyticscode -> analytics_code
+ - beiancode -> beian_code
+ - sitename -> site_name
+
+ 继承自 migrations.Migration 类,遵循Django的数据库迁移机制
+ """
+
+ # mk:定义迁移依赖关系,确保在执行当前迁移前,blog应用的0003_blogsettings_comment_need_review迁移已经完成
+ dependencies = [
+ ('blog', '0003_blogsettings_comment_need_review'),
+ ]
+
+ # mk:定义具体的迁移操作列表
+ operations = [
+ # mk:将BlogSettings模型中的analyticscode字段重命名为analytics_code
+ migrations.RenameField(
+ model_name='blogsettings',
+ old_name='analyticscode',
+ new_name='analytics_code',
+ ),
+ # mk:将BlogSettings模型中的beiancode字段重命名为beian_code
+ migrations.RenameField(
+ model_name='blogsettings',
+ old_name='beiancode',
+ new_name='beian_code',
+ ),
+ #mk:将BlogSettings模型中的sitename字段重命名为site_name
+ migrations.RenameField(
+ model_name='blogsettings',
+ old_name='sitename',
+ new_name='site_name',
+ ),
+ ]
+
diff --git a/doc/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/doc/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
new file mode 100644
index 0000000..36b59ce
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -0,0 +1,651 @@
+# mk:Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import mdeditor.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='article',
+ options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
+ ),
+ migrations.AlterModelOptions(
+ name='category',
+ options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
+ ),
+ migrations.AlterModelOptions(
+ name='links',
+ options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
+ ),
+ migrations.AlterModelOptions(
+ name='sidebar',
+# mk:Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import mdeditor.fields
+
+
+class Migration(migrations.Migration):
+ """
+ mk:
+ Django 数据库迁移类,用于更新博客应用中的模型字段和选项。
+
+ 此迁移主要完成以下操作:
+ - 更新多个模型的元数据(如排序方式、显示名称等)
+ - 移除旧的时间字段(created_time 和 last_mod_time)
+ - 添加新的时间字段(creation_time 和 last_modify_time)
+ - 修改多个模型字段的属性(如 verbose_name、default 值、最大长度等)
+
+ Attributes:
+ dependencies (list): 指定当前迁移所依赖的其他迁移文件。
+ operations (list): 包含所有数据库变更操作的列表。
+ """
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
+ ]
+
+ operations = [
+ # mk:更新 Article 模型的默认排序规则和其他元信息
+ migrations.AlterModelOptions(
+ name='article',
+ options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
+ ),
+ # mk:更新 Category 模型的排序规则及显示名称
+ migrations.AlterModelOptions(
+ name='category',
+ options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
+ ),
+ # mk:更新 Links 模型的排序规则及显示名称
+ migrations.AlterModelOptions(
+ name='links',
+ options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
+ ),
+ # mk:更新 Sidebar 模型的排序规则及显示名称
+ migrations.AlterModelOptions(
+ name='sidebar',
+ options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
+ ),
+ # mk:更新 Tag 模型的排序规则及显示名称
+ migrations.AlterModelOptions(
+ name='tag',
+ options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
+ ),
+
+ # mk:删除 Article 模型中已废弃的时间字段
+ migrations.RemoveField(
+ model_name='article',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='article',
+ name='last_mod_time',
+ ),
+
+ # mk:删除 Category 模型中已废弃的时间字段
+ migrations.RemoveField(
+ model_name='category',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='category',
+ name='last_mod_time',
+ ),
+
+ # mk:删除 Links 模型中已废弃的时间字段
+ migrations.RemoveField(
+ model_name='links',
+ name='created_time',
+ ),
+
+ # mk:删除 Sidebar 模型中已废弃的时间字段
+ migrations.RemoveField(
+ model_name='sidebar',
+ name='created_time',
+ ),
+
+ # mk:删除 Tag 模型中已废弃的时间字段
+ migrations.RemoveField(
+ model_name='tag',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='tag',
+ name='last_mod_time',
+ ),
+
+ # mk:为 Article 模型添加新的创建时间和最后修改时间字段
+ migrations.AddField(
+ model_name='article',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='article',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+
+ #mk: 为 Category 模型添加新的创建时间和最后修改时间字段
+ migrations.AddField(
+ model_name='category',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='category',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+
+ # mk:为 Links 模型添加新的创建时间字段
+ migrations.AddField(
+ model_name='links',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+
+ # mk:为 Sidebar 模型添加新的创建时间字段
+ migrations.AddField(
+ model_name='sidebar',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+
+ # mk:为 Tag 模型添加新的创建时间和最后修改时间字段
+ migrations.AddField(
+ model_name='tag',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='tag',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+
+ # mk:修改 Article 模型各字段定义以增强可读性和规范性
+ migrations.AlterField(
+ model_name='article',
+ name='article_order',
+ field=models.IntegerField(default=0, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='body',
+ field=mdeditor.fields.MDTextField(verbose_name='body'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='comment_status',
+ field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='pub_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='show_toc',
+ field=models.BooleanField(default=False, verbose_name='show toc'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='status',
+ field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='tags',
+ field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='title',
+ field=models.CharField(max_length=200, unique=True, verbose_name='title'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='type',
+ field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='views',
+ field=models.PositiveIntegerField(default=0, verbose_name='views'),
+ ),
+
+ # mk:修改 BlogSettings 模型相关配置项字段定义
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='article_comment_count',
+ field=models.IntegerField(default=5, verbose_name='article comment count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='article_sub_length',
+ field=models.IntegerField(default=300, verbose_name='article sub length'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='google_adsense_codes',
+ field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='open_site_comment',
+ field=models.BooleanField(default=True, verbose_name='open site comment'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='show_google_adsense',
+ field=models.BooleanField(default=False, verbose_name='show adsense'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='sidebar_article_count',
+ field=models.IntegerField(default=10, verbose_name='sidebar article count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='sidebar_comment_count',
+ field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_description',
+ field=models.TextField(default='', max_length=1000, verbose_name='site description'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_keywords',
+ field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_name',
+ field=models.CharField(default='', max_length=200, verbose_name='site name'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_seo_description',
+ field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
+ ),
+
+ # mk:修改 Category 模型字段定义
+ migrations.AlterField(
+ model_name='category',
+ name='index',
+ field=models.IntegerField(default=0, verbose_name='index'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='parent_category',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
+ ),
+
+ # mk:修改 Links 模型字段定义
+ migrations.AlterField(
+ model_name='links',
+ name='is_enable',
+ field=models.BooleanField(default=True, verbose_name='is show'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='last_mod_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='link',
+ field=models.URLField(verbose_name='link'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='sequence',
+ field=models.IntegerField(unique=True, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='show_type',
+ field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
+ ),
+
+ # mk:修改 Sidebar 模型字段定义
+ migrations.AlterField(
+ model_name='sidebar',
+ name='content',
+ field=models.TextField(verbose_name='content'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='is_enable',
+ field=models.BooleanField(default=True, verbose_name='is enable'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='last_mod_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='name',
+ field=models.CharField(max_length=100, verbose_name='title'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='sequence',
+ field=models.IntegerField(unique=True, verbose_name='order'),
+ ),
+
+ # mk:修改 Tag 模型字段定义
+ migrations.AlterField(
+ model_name='tag',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
+ ),
+ ]
+ options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
+ ),
+ migrations.AlterModelOptions(
+ name='tag',
+ options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
+ ),
+ migrations.RemoveField(
+ model_name='article',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='article',
+ name='last_mod_time',
+ ),
+ migrations.RemoveField(
+ model_name='category',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='category',
+ name='last_mod_time',
+ ),
+ migrations.RemoveField(
+ model_name='links',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='sidebar',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='tag',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='tag',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='article',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='article',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AddField(
+ model_name='category',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='category',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AddField(
+ model_name='links',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='sidebar',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='tag',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='tag',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='article_order',
+ field=models.IntegerField(default=0, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='body',
+ field=mdeditor.fields.MDTextField(verbose_name='body'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='comment_status',
+ field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='pub_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='show_toc',
+ field=models.BooleanField(default=False, verbose_name='show toc'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='status',
+ field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='tags',
+ field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='title',
+ field=models.CharField(max_length=200, unique=True, verbose_name='title'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='type',
+ field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='views',
+ field=models.PositiveIntegerField(default=0, verbose_name='views'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='article_comment_count',
+ field=models.IntegerField(default=5, verbose_name='article comment count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='article_sub_length',
+ field=models.IntegerField(default=300, verbose_name='article sub length'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='google_adsense_codes',
+ field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='open_site_comment',
+ field=models.BooleanField(default=True, verbose_name='open site comment'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='show_google_adsense',
+ field=models.BooleanField(default=False, verbose_name='show adsense'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='sidebar_article_count',
+ field=models.IntegerField(default=10, verbose_name='sidebar article count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='sidebar_comment_count',
+ field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_description',
+ field=models.TextField(default='', max_length=1000, verbose_name='site description'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_keywords',
+ field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_name',
+ field=models.CharField(default='', max_length=200, verbose_name='site name'),
+ ),
+ migrations.AlterField(
+ model_name='blogsettings',
+ name='site_seo_description',
+ field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='index',
+ field=models.IntegerField(default=0, verbose_name='index'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='parent_category',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='is_enable',
+ field=models.BooleanField(default=True, verbose_name='is show'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='last_mod_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='link',
+ field=models.URLField(verbose_name='link'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='sequence',
+ field=models.IntegerField(unique=True, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='links',
+ name='show_type',
+ field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='content',
+ field=models.TextField(verbose_name='content'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='is_enable',
+ field=models.BooleanField(default=True, verbose_name='is enable'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='last_mod_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='name',
+ field=models.CharField(max_length=100, verbose_name='title'),
+ ),
+ migrations.AlterField(
+ model_name='sidebar',
+ name='sequence',
+ field=models.IntegerField(unique=True, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='name',
+ field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
+ ),
+ ]
diff --git a/doc/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py b/doc/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py
new file mode 100644
index 0000000..194cc28
--- /dev/null
+++ b/doc/DjangoBlog/blog/migrations/0006_alter_blogsettings_options.py
@@ -0,0 +1,26 @@
+# mk:Generated by Django 4.2.7 on 2024-01-26 02:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ """
+ mk:
+ Django数据库迁移类,用于修改BlogSettings模型的显示选项配置
+
+ 该迁移类继承自Django的Migration基类,主要作用是更新blogsettings模型的元数据选项,
+ 设置更友好的模型名称显示。
+ """
+
+ dependencies = [
+ ('blog', '0005_alter_article_options_alter_category_options_and_more'),
+ ]
+
+ operations = [
+ # mk:修改BlogSettings模型的显示选项,设置单数和复数形式的可读名称
+ migrations.AlterModelOptions(
+ name='blogsettings',
+ options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
+ ),
+ ]
+
diff --git a/doc/DjangoBlog/blog/migrations/__init__.py b/doc/DjangoBlog/blog/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/blog/models.py b/doc/DjangoBlog/blog/models.py
new file mode 100644
index 0000000..f33e01d
--- /dev/null
+++ b/doc/DjangoBlog/blog/models.py
@@ -0,0 +1,653 @@
+import logging
+import re
+from abc import abstractmethod
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from mdeditor.fields import MDTextField
+from uuslug import slugify
+
+from djangoblog.utils import cache_decorator, cache
+from djangoblog.utils import get_current_site
+
+logger = logging.getLogger(__name__)
+
+
+class LinkShowType(models.TextChoices):
+ """
+ mk:
+ 友情链接显示类型枚举
+
+ 属性:
+ I: 首页显示
+ L: 列表页显示
+ P: 文章页显示
+ A: 全部页面显示
+ S: 幻灯片显示
+ """
+ I = ('i', _('index'))
+ L = ('l', _('list'))
+ P = ('p', _('post'))
+ A = ('a', _('all'))
+ S = ('s', _('slide'))
+
+
+class BaseModel(models.Model):
+ """
+ mk:
+ 基础模型类,提供通用字段和方法
+
+ 属性:
+ id: 主键ID
+ creation_time: 创建时间
+ last_modify_time: 最后修改时间
+ """
+ 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):
+ """
+ mk:
+ 保存模型实例
+
+ Args:
+ *args: 位置参数
+ **kwargs: 关键字参数
+
+ Returns:
+ None
+ """
+ is_update_views = isinstance(
+ self,
+ Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
+ if is_update_views:
+ Article.objects.filter(pk=self.pk).update(views=self.views)
+ else:
+ if 'slug' in self.__dict__:
+ slug = getattr(
+ self, 'title') if 'title' in self.__dict__ else getattr(
+ self, 'name')
+ setattr(self, 'slug', slugify(slug))
+ super().save(*args, **kwargs)
+
+ def get_full_url(self):
+ """
+ mk:
+ 获取完整的URL地址
+
+ Returns:
+ str: 完整的URL地址
+ """
+ site = get_current_site().domain
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ abstract = True
+
+ @abstractmethod
+ def get_absolute_url(self):
+ """
+ mk:
+ 抽象方法,获取绝对URL地址
+
+ Returns:
+ str: 绝对URL地址
+ """
+ pass
+
+
+class Article(BaseModel):
+ """
+ mk:
+ 文章模型类
+
+ 属性:
+ title: 标题
+ body: 正文内容
+ pub_time: 发布时间
+ status: 状态(草稿/发布)
+ comment_status: 评论状态(开启/关闭)
+ type: 类型(文章/页面)
+ views: 浏览量
+ author: 作者
+ article_order: 排序
+ show_toc: 是否显示目录
+ category: 分类
+ tags: 标签
+ """
+ STATUS_CHOICES = (
+ ('d', _('Draft')),
+ ('p', _('Published')),
+ )
+ COMMENT_STATUS = (
+ ('o', _('Open')),
+ ('c', _('Close')),
+ )
+ TYPE = (
+ ('a', _('Article')),
+ ('p', _('Page')),
+ )
+ title = models.CharField(_('title'), max_length=200, unique=True)
+ body = MDTextField(_('body'))
+ pub_time = models.DateTimeField(
+ _('publish time'), blank=False, null=False, default=now)
+ status = models.CharField(
+ _('status'),
+ max_length=1,
+ choices=STATUS_CHOICES,
+ default='p')
+ comment_status = models.CharField(
+ _('comment status'),
+ max_length=1,
+ choices=COMMENT_STATUS,
+ default='o')
+ type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
+ views = models.PositiveIntegerField(_('views'), default=0)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ blank=False,
+ null=False,
+ on_delete=models.CASCADE)
+ article_order = models.IntegerField(
+ _('order'), blank=False, null=False, default=0)
+ show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
+ category = models.ForeignKey(
+ 'Category',
+ verbose_name=_('category'),
+ on_delete=models.CASCADE,
+ blank=False,
+ null=False)
+ tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
+
+ def body_to_string(self):
+ """
+ mk:
+ 将正文内容转换为字符串
+
+ Returns:
+ str: 正文内容字符串
+ """
+ return self.body
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 文章标题
+ """
+ return self.title
+
+ class Meta:
+ ordering = ['-article_order', '-pub_time']
+ verbose_name = _('article')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def get_absolute_url(self):
+ """
+ mk:
+ 获取文章详情页的绝对URL
+
+ Returns:
+ str: 文章详情页URL
+ """
+ return reverse('blog:detailbyid', kwargs={
+ 'article_id': self.id,
+ 'year': self.creation_time.year,
+ 'month': self.creation_time.month,
+ 'day': self.creation_time.day
+ })
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ """
+ mk:
+ 获取分类树结构
+
+ Returns:
+ list: 分类名称和URL的元组列表
+ """
+ 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):
+ """
+ mk:
+ 保存文章实例
+
+ Args:
+ *args: 位置参数
+ **kwargs: 关键字参数
+
+ Returns:
+ None
+ """
+ super().save(*args, **kwargs)
+
+ def viewed(self):
+ """
+ mk:
+ 增加文章浏览量并保存
+ """
+ self.views += 1
+ self.save(update_fields=['views'])
+
+ def comment_list(self):
+ """
+ mk:
+ 获取文章评论列表,带缓存功能
+
+ Returns:
+ QuerySet: 评论查询集
+ """
+ cache_key = 'article_comments_{id}'.format(id=self.id)
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get article comments:{id}'.format(id=self.id))
+ return value
+ else:
+ comments = self.comment_set.filter(is_enable=True).order_by('-id')
+ cache.set(cache_key, comments, 60 * 100)
+ logger.info('set article comments:{id}'.format(id=self.id))
+ return comments
+
+ def get_admin_url(self):
+ """
+ mk:
+ 获取后台管理URL
+
+ Returns:
+ str: 后台管理URL
+ """
+ info = (self._meta.app_label, self._meta.model_name)
+ return reverse('admin:%s_%s_change' % info, args=(self.pk,))
+
+ @cache_decorator(expiration=60 * 100)
+ def next_article(self):
+ """
+ mk:
+ 获取下一篇已发布的文章
+
+ Returns:
+ Article|None: 下一篇文章对象或None
+ """
+ # 下一篇
+ return Article.objects.filter(
+ id__gt=self.id, status='p').order_by('id').first()
+
+ @cache_decorator(expiration=60 * 100)
+ def prev_article(self):
+ """
+ mk:
+ 获取上一篇已发布的文章
+
+ Returns:
+ Article|None: 上一篇文章对象或None
+ """
+ # 前一篇
+ return Article.objects.filter(id__lt=self.id, status='p').first()
+
+ def get_first_image_url(self):
+ """
+ mk:
+ 从文章正文中提取第一张图片的URL
+
+ Returns:
+ str: 第一张图片的URL,如果没有找到则返回空字符串
+ """
+ match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
+ if match:
+ return match.group(1)
+ return ""
+
+
+class Category(BaseModel):
+ """
+ mk:
+ 文章分类模型类
+
+ 属性:
+ name: 分类名称
+ parent_category: 父级分类
+ slug: URL别名
+ index: 排序索引
+ """
+ name = models.CharField(_('category name'), max_length=30, unique=True)
+ parent_category = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent category'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ index = models.IntegerField(default=0, verbose_name=_('index'))
+
+ class Meta:
+ ordering = ['-index']
+ verbose_name = _('category')
+ verbose_name_plural = verbose_name
+
+ def get_absolute_url(self):
+ """
+ mk:
+ 获取分类详情页的绝对URL
+
+ Returns:
+ str: 分类详情页URL
+ """
+ return reverse(
+ 'blog:category_detail', kwargs={
+ 'category_name': self.slug})
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 分类名称
+ """
+ return self.name
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ """
+ mk:
+ 递归获取分类目录的父级分类树
+
+ Returns:
+ list: 包含当前分类及其所有父级分类的列表
+ """
+ categorys = []
+
+ def parse(category):
+ categorys.append(category)
+ if category.parent_category:
+ parse(category.parent_category)
+
+ parse(self)
+ return categorys
+
+ @cache_decorator(60 * 60 * 10)
+ def get_sub_categorys(self):
+ """
+ mk:
+ 获取当前分类目录的所有子分类
+
+ Returns:
+ list: 包含当前分类及其所有子分类的列表
+ """
+ categorys = []
+ all_categorys = Category.objects.all()
+
+ def parse(category):
+ if category not in categorys:
+ categorys.append(category)
+ childs = all_categorys.filter(parent_category=category)
+ for child in childs:
+ if category not in categorys:
+ categorys.append(child)
+ parse(child)
+
+ parse(self)
+ return categorys
+
+
+class Tag(BaseModel):
+ """
+ mk:
+ 文章标签模型类
+
+ 属性:
+ name: 标签名称
+ slug: URL别名
+ """
+ name = models.CharField(_('tag name'), max_length=30, unique=True)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 标签名称
+ """
+ return self.name
+
+ def get_absolute_url(self):
+ """
+ mk:
+ 获取标签详情页的绝对URL
+
+ Returns:
+ str: 标签详情页URL
+ """
+ return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
+
+ @cache_decorator(60 * 60 * 10)
+ def get_article_count(self):
+ """
+ mk:
+ 获取使用该标签的文章数量
+
+ Returns:
+ int: 文章数量
+ """
+ return Article.objects.filter(tags__name=self.name).distinct().count()
+
+ class Meta:
+ ordering = ['name']
+ verbose_name = _('tag')
+ verbose_name_plural = verbose_name
+
+
+class Links(models.Model):
+ """
+ mk:
+ 友情链接模型类
+
+ 属性:
+ name: 链接名称
+ link: 链接地址
+ sequence: 排序
+ is_enable: 是否启用
+ show_type: 显示类型
+ creation_time: 创建时间
+ last_mod_time: 最后修改时间
+ """
+ name = models.CharField(_('link name'), max_length=30, unique=True)
+ link = models.URLField(_('link'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(
+ _('is show'), default=True, blank=False, null=False)
+ show_type = models.CharField(
+ _('show type'),
+ max_length=1,
+ choices=LinkShowType.choices,
+ default=LinkShowType.I)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('link')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 链接名称
+ """
+ return self.name
+
+
+class SideBar(models.Model):
+ """
+ mk:
+ 侧边栏模型类,用于展示HTML内容
+
+ 属性:
+ name: 标题
+ content: 内容
+ sequence: 排序
+ is_enable: 是否启用
+ creation_time: 创建时间
+ last_mod_time: 最后修改时间
+ """
+ name = models.CharField(_('title'), max_length=100)
+ content = models.TextField(_('content'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(_('is enable'), default=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('sidebar')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 侧边栏标题
+ """
+ return self.name
+
+
+class BlogSettings(models.Model):
+ """
+ mk:
+ 博客配置模型类
+
+ 属性:
+ site_name: 网站名称
+ site_description: 网站描述
+ site_seo_description: SEO描述
+ site_keywords: SEO关键词
+ article_sub_length: 文章摘要长度
+ sidebar_article_count: 侧边栏文章数量
+ sidebar_comment_count: 侧边栏评论数量
+ article_comment_count: 文章评论数量
+ show_google_adsense: 是否显示Google广告
+ google_adsense_codes: Google广告代码
+ open_site_comment: 是否开启网站评论
+ global_header: 公共头部代码
+ global_footer: 公共尾部代码
+ beian_code: 备案号
+ analytics_code: 网站统计代码
+ show_gongan_code: 是否显示公安备案号
+ gongan_beiancode: 公安备案号
+ comment_need_review: 评论是否需要审核
+ """
+ site_name = models.CharField(
+ _('site name'),
+ max_length=200,
+ null=False,
+ blank=False,
+ default='')
+ site_description = models.TextField(
+ _('site description'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ site_seo_description = models.TextField(
+ _('site seo description'), max_length=1000, null=False, blank=False, default='')
+ site_keywords = models.TextField(
+ _('site keywords'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ article_sub_length = models.IntegerField(_('article sub length'), default=300)
+ sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
+ sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
+ article_comment_count = models.IntegerField(_('article comment count'), default=5)
+ show_google_adsense = models.BooleanField(_('show adsense'), default=False)
+ google_adsense_codes = models.TextField(
+ _('adsense code'), max_length=2000, null=True, blank=True, default='')
+ open_site_comment = models.BooleanField(_('open site comment'), default=True)
+ global_header = models.TextField("公共头部", null=True, blank=True, default='')
+ global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
+ beian_code = models.CharField(
+ '备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ analytics_code = models.TextField(
+ "网站统计代码",
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ show_gongan_code = models.BooleanField(
+ '是否显示公安备案号', default=False, null=False)
+ gongan_beiancode = models.TextField(
+ '公安备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ comment_need_review = models.BooleanField(
+ '评论是否需要审核', default=False, null=False)
+
+ class Meta:
+ verbose_name = _('Website configuration')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ """
+ mk:
+ 返回对象的字符串表示
+
+ Returns:
+ str: 网站名称
+ """
+ return self.site_name
+
+ def clean(self):
+ """
+ mk:
+ 数据验证,确保只存在一个配置实例
+
+ Raises:
+ ValidationError: 当已存在配置实例时抛出异常
+ """
+ if BlogSettings.objects.exclude(id=self.id).count():
+ raise ValidationError(_('There can only be one configuration'))
+
+ def save(self, *args, **kwargs):
+ """
+ mk:
+ 保存配置并清除缓存
+
+ Args:
+ *args: 位置参数
+ **kwargs: 关键字参数
+
+ Returns:
+ None
+ """
+ super().save(*args, **kwargs)
+ from djangoblog.utils import cache
+ cache.clear()
diff --git a/doc/DjangoBlog/blog/search_indexes.py b/doc/DjangoBlog/blog/search_indexes.py
new file mode 100644
index 0000000..f053317
--- /dev/null
+++ b/doc/DjangoBlog/blog/search_indexes.py
@@ -0,0 +1,39 @@
+from haystack import indexes
+
+from blog.models import Article
+
+
+class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ """
+ mk:
+ 文章搜索索引类
+
+ 用于为Article模型创建搜索引擎索引,继承自haystack的SearchIndex和Indexable类
+ """
+
+ # 定义搜索文档的文本字段,document=True表示这是主要的搜索字段
+ # use_template=True表示使用模板来定义索引内容
+ text = indexes.CharField(document=True, use_template=True)
+
+ def get_model(self):
+ """
+ mk:
+ 获取索引关联的模型类
+
+ Returns:
+ Article: 返回Article模型类
+ """
+ return Article
+
+ def index_queryset(self, using=None):
+ """
+ mk:
+ 获取需要建立索引的查询集
+
+ Args:
+ using (str, optional): 数据库别名,默认为None
+
+ Returns:
+ QuerySet: 返回状态为'p'(已发布)的文章查询集
+ """
+ return self.get_model().objects.filter(status='p')
diff --git a/doc/DjangoBlog/blog/static/account/css/account.css b/doc/DjangoBlog/blog/static/account/css/account.css
new file mode 100644
index 0000000..7d4cec7
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/account/css/account.css
@@ -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;
+}
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/account/js/account.js b/doc/DjangoBlog/blog/static/account/js/account.js
new file mode 100644
index 0000000..f1a8771
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/account/js/account.js
@@ -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("")
+ return
+ }
+ myErr.remove()
+ time(ts)
+ },
+ error: function (e) {
+ alert("发送失败,请重试")
+ }
+ }
+ );
+}
diff --git a/doc/DjangoBlog/blog/static/assets/css/bootstrap.min.css b/doc/DjangoBlog/blog/static/assets/css/bootstrap.min.css
new file mode 100644
index 0000000..ed3905e
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/css/bootstrap.min.css
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/assets/css/docs.min.css b/doc/DjangoBlog/blog/static/assets/css/docs.min.css
new file mode 100644
index 0000000..3945197
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/css/docs.min.css
@@ -0,0 +1,11 @@
+/*!
+ * 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)
+ */@-ms-viewport{width:device-width}@-o-viewport{width:device-width}@viewport{width:device-width}.hll{background-color:#ffc}.c{color:#999}.err{color:#A00;background-color:#FAA}.k{color:#069}.o{color:#555}.cm{color:#999}.cp{color:#099}.c1{color:#999}.cs{color:#999}.gd{background-color:#FCC;border:1px solid #C00}.ge{font-style:italic}.gr{color:red}.gh{color:#030}.gi{background-color:#CFC;border:1px solid #0C0}.go{color:#AAA}.gp{color:#009}.gu{color:#030}.gt{color:#9C6}.kc{color:#069}.kd{color:#069}.kn{color:#069}.kp{color:#069}.kr{color:#069}.kt{color:#078}.m{color:#F60}.s{color:#d44950}.na{color:#4f9fcf}.nb{color:#366}.nc{color:#0A8}.no{color:#360}.nd{color:#99F}.ni{color:#999}.ne{color:#C00}.nf{color:#C0F}.nl{color:#99F}.nn{color:#0CF}.nt{color:#2f6f9f}.nv{color:#033}.ow{color:#000}.w{color:#bbb}.mf{color:#F60}.mh{color:#F60}.mi{color:#F60}.mo{color:#F60}.sb{color:#C30}.sc{color:#C30}.sd{color:#C30;font-style:italic}.s2{color:#C30}.se{color:#C30}.sh{color:#C30}.si{color:#A00}.sx{color:#C30}.sr{color:#3AA}.s1{color:#C30}.ss{color:#FC3}.bp{color:#366}.vc{color:#033}.vg{color:#033}.vi{color:#033}.il{color:#F60}.css .nt+.nt,.css .o,.css .o+.nt{color:#999}.select2-container{position:relative;display:inline-block;zoom:1;*display:inline;vertical-align:top;padding:0;border:0}.select2-container:hover{border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.select2-container,.select2-drop,.select2-search,.select2-search input{-moz-box-sizing:border-box;-ms-box-sizing:border-box;-webkit-box-sizing:border-box;-khtml-box-sizing:border-box;box-sizing:border-box}.select2-container .select2-choice{display:block;overflow:hidden;text-decoration:none;padding:4px 12px;margin:0;color:#333;text-shadow:0 1px 0 #fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif;font-weight:700;font-size:13px;cursor:default;height:18px;background-color:#f3f3f3;background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-o-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(to bottom,#f5f5f5,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);-webkit-background-clip:padding;-moz-background-clip:padding;background-clip:padding;border:1px solid #dcdcdc;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-webkit-box-sizing:content-box;-khtml-box-sizing:content-box;box-sizing:content-box}.select2-container .select2-choice:hover{color:#333;text-shadow:none;border-color:#c6c6c6;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f8f8f8),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-o-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(to bottom,#f8f8f8,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8f8f8', endColorstr='#fff1f1f1', GradientType=0);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);-moz-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);background-position:0 0;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;z-index:2}.select2-container-active .select2-choice:hover{border:1px solid #4D90FE}.select2-container.select2-drop-above .select2-choice{background-image:-webkit-gradient(linear,left bottom,left top,color-stop(0,#eee),color-stop(.9,#fff));background-image:-webkit-linear-gradient(center bottom,#eee 0,#fff 90%);background-image:-moz-linear-gradient(center bottom,#eee 0,#fff 90%);background-image:-o-linear-gradient(bottom,#eee 0,#fff 90%);background-image:-ms-linear-gradient(top,#eee 0,#fff 90%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0 );background-image:linear-gradient(top,#eee 0,#fff 90%)}.select2-container .select2-choice span{margin-right:26px;display:block;overflow:hidden;white-space:nowrap;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;text-overflow:ellipsis}.select2-container .select2-choice abbr{display:block;position:absolute;right:26px;top:8px;width:12px;height:12px;font-size:17px;line-height:16px;color:#595959;font-weight:700;cursor:pointer;text-decoration:none;border:0;outline:0}.select2-container .select2-choice abbr:hover{color:#222;cursor:pointer}.select2-drop-mask{position:absolute;left:0;top:0;z-index:9998;opacity:0}.select2-drop{background:#fff;color:#000;border:1px solid #aaa;position:absolute;top:100%;-webkit-box-shadow:0 2px 4px rgba(0,0,0,.2);-moz-box-shadow:0 2px 4px rgba(0,0,0,.2);-o-box-shadow:0 2px 4px rgba(0,0,0,.2);box-shadow:0 2px 4px rgba(0,0,0,.2);z-index:9999;width:100%;margin-top:1px}.select2-drop.select2-drop-above{margin-top:-1px;-webkit-box-shadow:0 -2px 4px rgba(0,0,0,.2);-moz-box-shadow:0 -2px 4px rgba(0,0,0,.2);-o-box-shadow:0 -2px 4px rgba(0,0,0,.2);box-shadow:0 -2px 4px rgba(0,0,0,.2)}.select2-container .select2-choice div{-webkit-border-radius:0 2px 2px 0;-moz-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;position:absolute;right:0;top:0;display:block;height:100%;width:18px}.select2-container .select2-choice div b{background:url(/assets/img/select2.png) no-repeat -30px 2px;display:block;width:100%;height:100%}.select2-search{display:inline-block;white-space:nowrap;z-index:10000;min-height:26px;width:100%;margin:0;padding:4px 4px 0 4px}.select2-search-hidden{display:block;position:absolute;left:-10000px}.select2-search input{background:#fff url(/assets/img/select2.png) no-repeat 100% -22px;background:url(/assets/img/select2.png) no-repeat 100% -22px,-webkit-gradient(linear,left bottom,left top,color-stop(.85,#fff),color-stop(.99,#eee));background:url(/assets/img/select2.png) no-repeat 100% -22px,-webkit-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-moz-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-o-linear-gradient(bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-ms-linear-gradient(top,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,linear-gradient(top,#fff 85%,#eee 99%);padding:4px 20px 4px 5px;outline:0;border:1px solid #aaa;font-family:sans-serif;font-size:1em;width:100%;margin:0;height:auto!important;min-height:26px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0}.select2-drop.select2-drop-above .select2-search input{margin-top:4px}.select2-search input.select2-active{background:#fff url(../img/spinner.gif) no-repeat 100%;background:url(../img/spinner.gif) no-repeat 100%,-webkit-gradient(linear,left bottom,left top,color-stop(.85,#fff),color-stop(.99,#eee));background:url(../img/spinner.gif) no-repeat 100%,-webkit-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-moz-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-o-linear-gradient(bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-ms-linear-gradient(top,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,linear-gradient(top,#fff 85%,#eee 99%)}.select2-container-active .select2-choice,.select2-container-active .select2-choices{border:1px solid #4D90FE;outline:0}.select2-dropdown-open .select2-choice,.select2-dropdown-open .select2-choice:hover{background-color:#f4f4f4;background-image:-moz-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f6f6f6),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:-o-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:linear-gradient(to bottom,#f6f6f6,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.select2-dropdown-open .select2-choice div{background:0 0;border-left:none}.select2-results{margin:4px 1px 4px 0;padding:0;position:relative;overflow-x:hidden;overflow-y:auto;max-height:200px}.select2-results ul.select2-result-sub{margin:0}.select2-results ul.select2-result-sub>li .select2-result-label{padding-left:20px}.select2-results ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:40px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:60px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:80px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:100px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:110px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:120px}.select2-results li{list-style:none;display:list-item}.select2-results li.select2-result-with-children>.select2-result-label{font-weight:700}.select2-results .select2-result-label{padding:3px 7px 4px;margin:0;cursor:pointer}.select2-results .select2-highlighted{background:#eee}.select2-results li em{background:#feffde;font-style:normal}.select2-results .select2-highlighted em{background:0 0}.select2-results .select2-no-results,.select2-results .select2-searching,.select2-results .select2-selection-limit{background:#f4f4f4;display:list-item;padding-left:4px}.select2-results .select2-disabled{display:none}.select2-more-results.select2-active{background:#f4f4f4 url(../img/spinner.gif) no-repeat 100%}.select2-more-results{background:#f4f4f4;display:list-item}.select2-container.select2-container-disabled .select2-choice{color:#b3b3b3;border-color:#d9d9d9;background-color:#e6e6e6;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;text-shadow:none;cursor:default}.select2-container.select2-container-disabled .select2-choice div{opacity:.5;filter:alpha(opacity=50)}.select2-container-multi .select2-choices{background-color:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;margin:0;padding:0;cursor:text;overflow:hidden;height:auto!important;height:1%;position:relative}.select2-container-multi .select2-choices:hover{border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.select2-container-multi .select2-choices{min-height:26px}.select2-container-multi.select2-container-active .select2-choices{border:1px solid #4D90FE;outline:0}.select2-container-multi .select2-choices li{float:left;list-style:none}.select2-container-multi .select2-choices .select2-search-field{white-space:nowrap;margin:0;padding:0}.select2-container-multi .select2-choices .select2-search-field input{color:#666;background:0 0!important;font-family:sans-serif;font-size:100%;height:23px;padding:5px;margin:1px 0;outline:0;border:0;-webkit-box-shadow:none;-moz-box-shadow:none;-o-box-shadow:none;box-shadow:none}.select2-container-multi .select2-choices .select2-search-field input.select2-active{background:#fff url(../img/spinner.gif) no-repeat 100%!important}.select2-default{color:#999!important}.select2-container-multi .select2-choices .select2-search-choice{-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;background-color:#DAE4F6;color:#222;font-family:Arial;border:1px solid #DAE4F6;line-height:23px;padding:0 19px 0 5px;margin:1px;position:relative;cursor:default}.select2-container-multi .select2-choices .select2-search-choice span{cursor:default}.select2-container-multi .select2-choices .select2-search-choice-focus{background:#A6D7F5}.select2-search-choice-close{display:block;position:absolute;right:3px;top:4px;width:12px;height:13px;font-size:17px;line-height:16px;color:#444;font-weight:700;outline:0}.select2-search-choice-close:hover{text-decoration:none;color:#222;cursor:pointer}.select2-container-multi.select2-container-disabled .select2-choices{background-color:#f4f4f4;background-image:none;border:1px solid #ddd;cursor:default}.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice{background-image:none;background-color:#f4f4f4;border:1px solid #ddd;padding:3px 5px 3px 5px}.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close{display:none}.select2-result-selectable .select2-match,.select2-result-unselectable .select2-result-selectable .select2-match{font-weight:700}.select2-result-unselectable .select2-match{text-decoration:none}.select2-offscreen{position:absolute;left:-10000px}.select2-results::-webkit-scrollbar{height:16px;width:10px}.select2-results::-webkit-scrollbar-button:end:increment,.select2-results::-webkit-scrollbar-button:start:decrement{background-color:transparent;display:block;height:0}.select2-results::-webkit-scrollbar-track{background-clip:padding-box;border:solid transparent;border-width:0 0 0 4px}.select2-results::-webkit-scrollbar-track-piece{background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.select2-results::-webkit-scrollbar:hover{background-color:#f3f3f3;border:1px solid #dbdbdb}.select2-results::-webkit-scrollbar-thumb:horizontal,.select2-results::-webkit-scrollbar-thumb:vertical{background-color:#c6c6c6;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.select2-results::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.2);border:solid transparent;border-width:0;-webkit-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);-moz-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);background-clip:padding-box}.select2-results::-webkit-scrollbar-thumb:hover{background-color:#949494}.select2-results::-webkit-scrollbar-thumb:active{background-color:rgba(0,0,0,.5);-webkit-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);-moz-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);box-shadow:inset 1px 1px 3px rgba(0,0,0,.35)}@media only screen and (-webkit-min-device-pixel-ratio:1.5){.select2-container .select2-choice div b,.select2-search input{background-image:url(/assets/img/select2x2.png)!important;background-repeat:no-repeat!important;background-size:60px 40px!important}.select2-search input{background-position:100% -21px!important}}/*!
+ * Bootstrap Docs (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under the Creative Commons Attribution 3.0 Unported License. For
+ * details, see https://creativecommons.org/licenses/by/3.0/.
+ */body{position:relative;padding-top:94px}.table code{font-size:13px;font-weight:400}h2 code,h3 code,h4 code{background-color:inherit}.btn-outline{color:#4d90fe;background-color:transparent;border-color:#4d90fe}.btn-outline:active,.btn-outline:focus,.btn-outline:hover{color:#fff;background-color:#4d90fe;border-color:#4d90fe}.btn-outline-inverse{color:#fff;background-color:transparent;border-color:#fff}.btn-outline-inverse:active,.btn-outline-inverse:focus,.btn-outline-inverse:hover{color:#2d87e2;text-shadow:none;background-color:#fff;border-color:#fff}#skippy{display:block;padding:1em;color:#777;background-color:#f1f1f1;outline:0}#skippy .skiplink-text{padding:.5em;outline:1px dotted}#content:focus{outline:0}.bs-docs-footer{padding-top:40px;padding-bottom:30px;margin-top:100px;color:#777;text-align:center;border-top:1px solid #e5e5e5}.bs-docs-footer-links{padding-left:0;margin-bottom:20px}.bs-docs-footer-links li{display:inline-block}.bs-docs-footer-links li+li{margin-left:15px}@media (min-width:768px){.bs-docs-footer{text-align:left}.bs-docs-footer p{margin-bottom:0}}.bs-docs-header,.bs-docs-masthead{position:relative;padding:30px 0;color:#b3d4f4;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.1);background-color:#2d87e2;background-image:-webkit-linear-gradient(top,#1b6ec1 0,#2d87e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#1b6ec1),to(#2d87e2));background-image:-o-linear-gradient(top,#1b6ec1 0,#2d87e2 100%);background-image:linear-gradient(to bottom,#1b6ec1 0,#2d87e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1b6ec1', endColorstr='#2d87e2', GradientType=0);background-repeat:repeat-x}.bs-docs-masthead .bs-docs-booticon{margin:0 auto 30px}.bs-docs-masthead h1{font-weight:300;line-height:1;color:#fff}.bs-docs-masthead .lead{margin:0 auto 30px;font-size:20px;color:#fff}.bs-docs-masthead .version{margin-top:-15px;color:#b3d4f4}.bs-docs-masthead .btn{width:100%;padding:15px 30px;font-size:20px}@media (min-width:480px){.bs-docs-masthead .btn{width:auto}}@media (min-width:768px){.bs-docs-masthead{padding:80px 0}.bs-docs-masthead h1{font-size:60px}.bs-docs-masthead .lead{font-size:24px}}@media (min-width:992px){.bs-docs-masthead .lead{width:80%;font-size:30px}}.bs-docs-header{margin-bottom:40px;font-size:20px}.bs-docs-header h1{margin-top:0;color:#fff}.bs-docs-header p{margin-bottom:0;font-weight:300;line-height:1.4}.bs-docs-header .container{position:relative}@media (min-width:768px){.bs-docs-header{padding-top:60px;padding-bottom:60px;font-size:24px;text-align:left}.bs-docs-header h1{font-size:60px;line-height:1}}@media (min-width:992px){.bs-docs-header h1,.bs-docs-header p{margin-right:380px}}.bs-docs-featurette{padding-top:40px;padding-bottom:40px;font-size:16px;line-height:1.5;color:#555;text-align:center;background-color:#fff;border-bottom:1px solid #e5e5e5}.bs-docs-featurette+.bs-docs-footer{margin-top:0;border-top:0}.bs-docs-featurette-title{margin-bottom:5px;font-size:30px;font-weight:400;color:#333}.half-rule{width:100px;margin:40px auto}.bs-docs-featurette h3{margin-bottom:5px;font-weight:400;color:#333}.bs-docs-featurette-img{display:block;margin-bottom:20px;color:#333}.bs-docs-featurette-img:hover{color:#337ab7;text-decoration:none}.bs-docs-featurette-img img{display:block;margin-bottom:15px}@media (min-width:480px){.bs-docs-featurette .img-responsive{margin-top:30px}}@media (min-width:768px){.bs-docs-featurette{padding-top:100px;padding-bottom:100px}.bs-docs-featurette-title{font-size:40px}.bs-docs-featurette .lead{max-width:80%;margin-right:auto;margin-left:auto}.bs-docs-featurette .img-responsive{margin-top:0}}.bs-docs-featured-sites{margin-right:-1px;margin-left:-1px}.bs-docs-featured-sites .col-xs-6{padding:1px}.bs-docs-featured-sites .img-responsive{margin-top:0}@media (min-width:768px){.bs-docs-featured-sites .col-sm-3:first-child img{border-top-left-radius:4px;border-bottom-left-radius:4px}.bs-docs-featured-sites .col-sm-3:last-child img{border-top-right-radius:4px;border-bottom-right-radius:4px}}.bs-examples .thumbnail{margin-bottom:10px}.bs-examples h4{margin-bottom:5px}.bs-examples p{margin-bottom:20px}@media (max-width:480px){.bs-examples{margin-right:-10px;margin-left:-10px}.bs-examples>[class^=col-]{padding-right:10px;padding-left:10px}}.bs-docs-sidebar.affix{position:static}@media (min-width:768px){.bs-docs-sidebar{padding-left:20px}}.bs-docs-sidenav{margin-top:50px;margin-bottom:20px}.bs-docs-sidebar .nav>li>a{display:block;padding:5px 20px;font-size:13px;font-weight:500;color:#222}.bs-docs-sidebar .nav>li>a:focus,.bs-docs-sidebar .nav>li>a:hover{text-decoration:none;background-color:#eee}.bs-docs-sidebar .nav>.active:focus>a,.bs-docs-sidebar .nav>.active:hover>a,.bs-docs-sidebar .nav>.active>a{color:#dd4b39;background-color:transparent}.bs-docs-sidebar .nav .nav{display:none;margin-bottom:8px}.bs-docs-sidebar .nav .nav>li>a{padding-top:1px;padding-bottom:1px;padding-left:30px;font-size:12px}.back-to-top,.bs-docs-theme-toggle{display:none;padding:4px 10px;margin-top:10px;margin-left:10px;font-size:12px;font-weight:500;color:#999}.back-to-top:hover,.bs-docs-theme-toggle:hover{color:#563d7c;text-decoration:none}.bs-docs-theme-toggle{margin-top:0}@media (min-width:768px){.back-to-top,.bs-docs-theme-toggle{display:block}}@media (min-width:992px){.bs-docs-sidebar .nav>.active>ul{display:block}.bs-docs-sidebar.affix,.bs-docs-sidebar.affix-bottom{width:213px}.bs-docs-sidebar.affix{position:fixed;top:80px}.bs-docs-sidebar.affix-bottom{position:absolute}.bs-docs-sidebar.affix .bs-docs-sidenav,.bs-docs-sidebar.affix-bottom .bs-docs-sidenav{margin-top:0;margin-bottom:0}}@media (min-width:1200px){.bs-docs-sidebar.affix,.bs-docs-sidebar.affix-bottom{width:263px}}.bs-docs-section{margin-bottom:60px}.bs-docs-section:last-child{margin-bottom:0}h1[id]{padding-top:20px;margin-top:0}.bs-callout{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-radius:3px}.bs-callout h4{margin-top:0;margin-bottom:5px}.bs-callout p:last-child{margin-bottom:0}.bs-callout code{border-radius:3px}.bs-callout+.bs-callout{margin-top:-5px}.bs-callout-danger{border-left-color:#dd4b39}.bs-callout-danger h4{color:#c23321}.bs-callout-warning{border-left-color:#f1e7bc}.bs-callout-warning h4{color:#ba9e27}.bs-callout-info{border-left-color:#d0e3f0}.bs-callout-info h4{color:#3b86b9}.color-swatches{margin:0 -5px;overflow:hidden}.color-swatch{float:left;width:60px;height:60px;margin:0 5px;border-radius:3px}@media (min-width:768px){.color-swatch{width:100px;height:100px}}.color-swatches .gray-darker{background-color:#222}.color-swatches .gray-dark{background-color:#333}.color-swatches .gray{background-color:#555}.color-swatches .gray-light{background-color:#999}.color-swatches .gray-lighter{background-color:#eee}.color-swatches .brand-primary{background-color:#4d90fe}.color-swatches .brand-success{background-color:#35aa47}.color-swatches .brand-warning{background-color:#faa937}.color-swatches .brand-danger{background-color:#d84a38}.color-swatches .brand-info{background-color:#5bc0de}.color-swatches .bs-purple{background-color:#1b6ec1}.color-swatches .bs-purple-light{background-color:#c7bfd3}.color-swatches .bs-purple-lighter{background-color:#e5e1ea}.color-swatches .bs-gray{background-color:#f9f9f9}.bs-team .team-member{line-height:32px;color:#555}.bs-team .team-member:hover{color:#333;text-decoration:none}.bs-team .github-btn{float:right;width:180px;height:20px;margin-top:6px;border:none}.bs-team img{float:left;width:32px;margin-right:10px;border-radius:4px}.bs-docs-browser-bugs td p{margin-bottom:0}.bs-docs-browser-bugs th:first-child{width:18%}.show-grid{margin-bottom:15px}.show-grid [class^=col-]{padding-top:10px;padding-bottom:10px;background-color:#f9f9f9;border:1px solid #ddd}.bs-example{position:relative;padding:45px 15px 15px;margin:0 -15px 15px;border-color:#e5e5e5 #eee #eee;border-style:solid;border-width:1px 0;-webkit-box-shadow:inset 0 3px 6px rgba(0,0,0,.05);box-shadow:inset 0 3px 6px rgba(0,0,0,.05)}.bs-example:after{position:absolute;top:15px;left:15px;font-size:12px;font-weight:700;color:#959595;text-transform:uppercase;letter-spacing:1px;content:"Example"}.bs-example-padded-bottom{padding-bottom:24px}.bs-example+.highlight,.bs-example+.zero-clipboard+.highlight{margin:-15px -15px 15px;border-width:0 0 1px;border-radius:0}@media (min-width:768px){.bs-example{margin-right:0;margin-left:0;background-color:#fff;border-color:#ddd;border-width:1px;border-radius:4px 4px 0 0;-webkit-box-shadow:none;box-shadow:none}.bs-example+.highlight,.bs-example+.zero-clipboard+.highlight{margin-top:-16px;margin-right:0;margin-left:0;border-width:1px;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.bs-example-standalone{border-radius:4px}}.bs-example .container{width:auto}.bs-example>.alert:last-child,.bs-example>.form-control:last-child,.bs-example>.jumbotron:last-child,.bs-example>.list-group:last-child,.bs-example>.navbar:last-child,.bs-example>.panel:last-child,.bs-example>.progress:last-child,.bs-example>.table-responsive:last-child>.table,.bs-example>.table:last-child,.bs-example>.well:last-child,.bs-example>blockquote:last-child,.bs-example>ol:last-child,.bs-example>p:last-child,.bs-example>ul:last-child{margin-bottom:0}.bs-example>p>.close{float:none}.bs-example-type .table .type-info{color:#999;vertical-align:middle}.bs-example-type .table td{padding:15px 0;border-color:#eee}.bs-example-type .table tr:first-child td{border-top:0}.bs-example-type h1,.bs-example-type h2,.bs-example-type h3,.bs-example-type h4,.bs-example-type h5,.bs-example-type h6{margin:0}.bs-example-bg-classes p{padding:15px}.bs-example>.img-circle,.bs-example>.img-rounded,.bs-example>.img-thumbnail{margin:5px}.bs-example>.table-responsive>.table{background-color:#fff}.bs-example>.btn,.bs-example>.btn-group{margin-top:5px;margin-bottom:5px}.bs-example>.btn-toolbar+.btn-toolbar{margin-top:10px}.bs-example .select2-container.form-control,.bs-example-control-sizing input[type=text]+input[type=text],.bs-example-control-sizing select{margin-top:10px}.bs-example-form .input-group{margin-bottom:10px}.bs-example>textarea.form-control{resize:vertical}.bs-example>.list-group{max-width:400px}.bs-example .navbar:last-child{margin-bottom:0}.bs-navbar-bottom-example,.bs-navbar-top-example{z-index:1;padding:0;overflow:hidden}.bs-navbar-bottom-example .navbar-header,.bs-navbar-top-example .navbar-header{margin-left:0}.bs-navbar-bottom-example .navbar-fixed-bottom,.bs-navbar-top-example .navbar-fixed-top{position:relative;margin-right:0;margin-left:0}.bs-navbar-top-example{padding-bottom:90px}.bs-navbar-top-example:after{top:auto;bottom:15px}.bs-navbar-top-example .navbar-fixed-top{top:-1px}.bs-navbar-bottom-example{padding-top:90px}.bs-navbar-bottom-example .navbar-fixed-bottom{bottom:-1px}.bs-navbar-bottom-example .navbar{margin-bottom:0}@media (min-width:768px){.bs-navbar-bottom-example .navbar-fixed-bottom,.bs-navbar-top-example .navbar-fixed-top{position:absolute}}.bs-example .pagination{margin-top:10px;margin-bottom:10px}.bs-example>.pager{margin-top:0}.bs-example>.scrollable{height:200px;overflow-y:auto}.bs-example-modal{background-color:#f5f5f5}.bs-example-modal .modal{position:relative;top:auto;right:auto;bottom:auto;left:auto;z-index:1;display:block}.bs-example-modal .modal-dialog{left:auto;margin-right:auto;margin-left:auto}.bs-example .dropup>.dropdown-toggle,.bs-example>.dropdown>.dropdown-toggle{float:left}.bs-example-submenu .dropdown>.dropdown-menu,.bs-example-submenu .dropup>.dropdown-menu,.bs-example>.dropdown>.dropdown-menu{position:static;display:block;margin-bottom:5px;clear:left}.bs-example-submenu .dropdown-menu{margin-right:20px}.bs-example-tabs .nav-tabs{margin-bottom:15px}.bs-example-tooltips{text-align:center}.bs-example-tooltips>.btn{margin-top:5px;margin-bottom:5px}.bs-example-tooltip .tooltip{position:relative;display:inline-block;margin:10px 20px;opacity:1}.bs-example-popover{padding-bottom:24px;background-color:#f9f9f9}.bs-example-popover .popover{position:relative;display:block;float:left;width:260px;margin:20px}.scrollspy-example{position:relative;height:200px;margin-top:10px;overflow:auto}.bs-example>.nav-pills-stacked-example{max-width:300px}#collapseExample .well{margin-bottom:0}.bs-events-table>tbody>tr>td:first-child,.bs-events-table>thead>tr>th:first-child{white-space:nowrap}.bs-events-table>thead>tr>th:first-child{width:150px}.js-options-table>thead>tr>th:nth-child(1),.js-options-table>thead>tr>th:nth-child(2){width:100px}.js-options-table>thead>tr>th:nth-child(3){width:50px}.highlight{padding:9px 14px;margin-bottom:14px;background-color:#f7f7f9;border:1px solid #e1e1e8;border-radius:4px}.highlight pre{padding:0;margin-top:0;margin-bottom:0;word-break:normal;white-space:nowrap;background-color:transparent;border:0}.highlight pre code{font-size:inherit;color:#333}.highlight pre code:first-child{display:inline-block;padding-right:45px}.table-responsive .highlight pre{white-space:normal}.bs-table th small,.responsive-utilities th small{display:block;font-weight:400;color:#999}.responsive-utilities tbody th{font-weight:400}.responsive-utilities td{text-align:center}.responsive-utilities td.is-visible{color:#468847;background-color:#dff0d8!important}.responsive-utilities td.is-hidden{color:#ccc;background-color:#f9f9f9!important}.responsive-utilities-test{margin-top:5px}.responsive-utilities-test .col-xs-6{margin-bottom:10px}.responsive-utilities-test span{display:block;padding:15px 10px;font-size:14px;font-weight:700;line-height:1.1;text-align:center;border-radius:4px}.hidden-on .col-xs-6 .hidden-lg,.hidden-on .col-xs-6 .hidden-md,.hidden-on .col-xs-6 .hidden-sm,.hidden-on .col-xs-6 .hidden-xs,.visible-on .col-xs-6 .hidden-lg,.visible-on .col-xs-6 .hidden-md,.visible-on .col-xs-6 .hidden-sm,.visible-on .col-xs-6 .hidden-xs{color:#999;border:1px solid #ddd}.hidden-on .col-xs-6 .visible-lg-block,.hidden-on .col-xs-6 .visible-md-block,.hidden-on .col-xs-6 .visible-sm-block,.hidden-on .col-xs-6 .visible-xs-block,.visible-on .col-xs-6 .visible-lg-block,.visible-on .col-xs-6 .visible-md-block,.visible-on .col-xs-6 .visible-sm-block,.visible-on .col-xs-6 .visible-xs-block{color:#468847;background-color:#dff0d8;border:1px solid #d6e9c6}.bs-glyphicons{margin:0 -10px 20px;overflow:hidden}.bs-glyphicons-list{padding-left:0;list-style:none}.bs-glyphicons li{float:left;width:25%;height:115px;padding:10px;margin:0 -1px -1px 0;font-size:10px;line-height:1.4;text-align:center;border:1px solid #ddd}.bs-glyphicons .glyphicon{margin-top:5px;margin-bottom:10px;font-size:24px}.bs-glyphicons .glyphicon-class{display:block;text-align:center;word-wrap:break-word}.bs-glyphicons li:hover{background-color:#eee}@media (min-width:768px){.bs-glyphicons{margin-right:0;margin-left:0}.bs-glyphicons li{width:12.5%;font-size:12px}}.bs-customizer .toggle{float:right;margin-top:25px}.bs-customizer label{margin-top:10px;font-weight:500;color:#555}.bs-customizer h2{padding-top:30px;margin-top:0;margin-bottom:5px}.bs-customizer h3{margin-bottom:0}.bs-customizer h4{margin-top:15px;margin-bottom:0}.bs-customizer .bs-callout h4{margin-top:0;margin-bottom:5px}.bs-customizer input[type=text]{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#fafafa}.bs-customizer .help-block{margin-bottom:5px;font-size:12px}#less-section label{font-weight:400}.bs-customize-download .btn-outline{padding:20px}.bs-customizer-alert{position:fixed;top:0;right:0;left:0;z-index:1030;padding:15px 0;color:#fff;background-color:#d9534f;border-bottom:1px solid #b94441;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25);box-shadow:inset 0 1px 0 rgba(255,255,255,.25)}.bs-customizer-alert .close{margin-top:-4px;font-size:24px}.bs-customizer-alert p{margin-bottom:0}.bs-customizer-alert .glyphicon{margin-right:5px}.bs-customizer-alert pre{margin:10px 0 0;color:#fff;background-color:#a83c3a;border-color:#973634;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 2px 4px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)}.bs-dropzone{position:relative;padding:20px;margin-bottom:20px;color:#777;text-align:center;border:2px dashed #eee;border-radius:4px}.bs-dropzone .import-header{margin-bottom:5px}.bs-dropzone .glyphicon-download-alt{font-size:40px}.bs-dropzone hr{width:100px}.bs-dropzone .lead{margin-bottom:10px;font-weight:400;color:#333}#import-manual-trigger{cursor:pointer}.bs-dropzone p:last-child{margin-bottom:0}.bs-brand-logos{display:table;width:100%;margin-bottom:15px;overflow:hidden;color:#1b6ec1;background-color:#f9f9f9;border-radius:4px}.bs-brand-item{padding:60px 0;text-align:center}.bs-brand-item+.bs-brand-item{border-top:1px solid #fff}.bs-brand-logos .inverse{color:#fff;background-color:#1b6ec1}.bs-brand-item h1,.bs-brand-item h3{margin-top:0;margin-bottom:0}.bs-brand-item .bs-docs-booticon{margin-right:auto;margin-left:auto}.bs-brand-item .glyphicon{width:30px;height:30px;margin:10px auto -10px;line-height:30px;color:#fff;border-radius:50%}.bs-brand-item .glyphicon-ok{background-color:#5cb85c}.bs-brand-item .glyphicon-remove{background-color:#d9534f}@media (min-width:768px){.bs-brand-item{display:table-cell;width:1%}.bs-brand-item+.bs-brand-item{border-top:0;border-left:1px solid #fff}.bs-brand-item h1{font-size:60px}}.zero-clipboard{position:relative;display:none}.btn-clipboard{position:absolute;top:0;right:0;z-index:10;display:block;padding:5px 8px;font-size:12px;color:#777;cursor:pointer;background-color:#fff;border:1px solid #e1e1e8;border-radius:0 4px 0 4px}.btn-clipboard-hover{color:#fff;background-color:#563d7c;border-color:#563d7c}@media (min-width:768px){.zero-clipboard{display:block}.bs-example+.zero-clipboard .btn-clipboard{top:-16px;border-top-right-radius:0}}.anchorjs-link{color:inherit}@media (max-width:480px){.anchorjs-link{display:none}}:hover>.anchorjs-link{opacity:.75;-webkit-transition:color .16s linear;-o-transition:color .16s linear;transition:color .16s linear}.anchorjs-link:focus,:hover>.anchorjs-link:hover{text-decoration:none;opacity:1}#focusedInput{border:1px solid #4d90fe!important;outline:0;outline:thin dotted\9;-webkit-box-shadow:none;box-shadow:none}.v4-tease{position:fixed;top:0;right:0;left:0;z-index:1030;display:block;padding:15px 20px;font-weight:700;color:#fff;text-align:center;background-color:#1b6ec1}.v4-tease:hover{color:#fff;text-decoration:none;background-color:#2d87e2}@media print{a[href]:after{content:""!important}}.bs-docs-navbar-masthead{top:48px}.bs-docs-dl-options h4{margin-top:15px;margin-bottom:5px}
+/*# sourceMappingURL=docs.min.css.map */
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/assets/css/ie10-viewport-bug-workaround.css b/doc/DjangoBlog/blog/static/assets/css/ie10-viewport-bug-workaround.css
new file mode 100644
index 0000000..4b9518e
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/css/ie10-viewport-bug-workaround.css
@@ -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; }
diff --git a/doc/DjangoBlog/blog/static/assets/css/signin.css b/doc/DjangoBlog/blog/static/assets/css/signin.css
new file mode 100644
index 0000000..121fb0d
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/css/signin.css
@@ -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;
+}
diff --git a/doc/DjangoBlog/blog/static/assets/css/todc-bootstrap.min.css b/doc/DjangoBlog/blog/static/assets/css/todc-bootstrap.min.css
new file mode 100644
index 0000000..66c9cb2
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/css/todc-bootstrap.min.css
@@ -0,0 +1,6 @@
+/*!
+ * TODC Bootstrap v3.3.7-3.3.7 (http://todc.github.com/todc-bootstrap/)
+ * Copyright 2011-2016 Tim O'Donnell
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license
+ */.panel-group .panel-heading a.collapsed:before,.panel-group .panel-heading a:before{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.caret-left,.caret-right,.collapse-caret.collapsed:before,.collapse-caret:before,.dropdown-submenu>a:after{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}body{font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.4;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#15c}a:focus,a:hover{color:#15c}.img-rounded{border-radius:1px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:0;line-height:1.4;background-color:#fff;border:3px solid #fff;border-radius:0;-webkit-box-shadow:0 0 0 1px #aaa;box-shadow:0 0 0 1px #aaa;-webkit-transition:none;-o-transition:none;transition:none}.caret-left,.caret-right,.collapse-caret.collapsed:before,.dropdown-submenu>a:after{vertical-align:baseline;border-top:4px solid transparent;border-right:0 dotted;border-bottom:4px solid transparent;border-left:4px solid}.caret-left{margin-right:2px;margin-left:0;border-right:4px solid;border-left:0 dotted}.scrollable-shadow{background:-webkit-gradient(linear,left top,left bottom,color-stop(30%,#fff),to(rgba(255,255,255,0))),-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(70%,#fff)) 0 100%,radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-webkit-linear-gradient(white 30%,rgba(255,255,255,0)),-webkit-linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,-webkit-radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),-webkit-radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-o-linear-gradient(white 30%,rgba(255,255,255,0)),-o-linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,-o-radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),-o-radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:linear-gradient(white 30%,rgba(255,255,255,0)),linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-webkit-gradient(linear,left top,left bottom,color-stop(30%,#fff),to(rgba(255,255,255,0))),-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(70%,#fff)) 0 100%,radial-gradient(farthest-side at 50% 0,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(farthest-side at 50% 100%,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:linear-gradient(white 30%,rgba(255,255,255,0)),linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,radial-gradient(farthest-side at 50% 0,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(farthest-side at 50% 100%,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background-repeat:no-repeat;background-attachment:local,local,scroll,scroll;-webkit-background-size:100% 40px,100% 40px,100% 6px,100% 6px;background-size:100% 40px,100% 40px,100% 6px,100% 6px}.mark,mark{background-color:#f9edbe}.text-primary{color:#4d90fe}a.text-primary:focus,a.text-primary:hover{color:#1a70fe}.text-warning{color:#333}a.text-warning:focus,a.text-warning:hover{color:#1a1a1a}.bg-primary{color:#fff;background-color:#4d90fe}a.bg-primary:focus,a.bg-primary:hover{background-color:#1a70fe}.bg-warning{background-color:#f9edbe}a.bg-warning:focus,a.bg-warning:hover{background-color:#f5e08f}code{padding:2px 4px;border-radius:0}kbd{border-radius:1px}pre{padding:9px;margin:0 0 9px;font-size:12px;line-height:1.4;border-radius:0}table{background-color:transparent}caption{color:#999}.table{margin-bottom:18px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{line-height:1.4;border-top:1px solid #ddd}.table>thead>tr>th{border-bottom:2px solid #ddd}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#ffc}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#f9edbe}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#f7e7a7}@media screen and (max-width:767px){.table-responsive{margin-bottom:13.5px;border:1px solid #ddd}}legend{margin-bottom:18px;font-size:19.5px}input[type=radio],input[type=checkbox]{margin:2px 0 0}output{padding-top:6px;font-size:13px;line-height:1.4;color:#555}.form-control{height:30px;-webkit-appearance:none;padding:5px 8px;font-size:13px;line-height:1.4;background-color:#fff;border:1px solid #d9d9d9;border-top-color:silver;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:none;-o-transition:none;transition:none}.form-control:hover{border:1px solid #b9b9b9;border-top-color:#a0a0a0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.form-control:focus{border-color:#4d90fe;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(77,144,254,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(77,144,254,.6)}.form-control:focus{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.form-control::-ms-expand{background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#f1f1f1;border:1px solid #e5e5e5}.form-control[disabled]:active,.form-control[disabled]:focus,.form-control[disabled]:hover,.form-control[readonly]:active,.form-control[readonly]:focus,.form-control[readonly]:hover,fieldset[disabled] .form-control:active,fieldset[disabled] .form-control:focus,fieldset[disabled] .form-control:hover{border:1px solid #e5e5e5;-webkit-box-shadow:none;box-shadow:none}.form-control[readonly] .form-control{border:1px solid #d9d9d9}.form-control[readonly] .form-control:active,.form-control[readonly] .form-control:focus,.form-control[readonly] .form-control:hover{border:1px solid #d9d9d9}textarea.form-control{padding-right:4px}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:30px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:26px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:38px}}.checkbox label,.radio label{min-height:18px}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio],input[type=radio],input[type=checkbox]{position:relative;width:13px;width:16px\9;height:13px;height:16px\9;-webkit-appearance:none;background:#fff;border:1px solid #dcdcdc;border:1px solid transparent\9;border-radius:1px}.checkbox input[type=checkbox]:focus,.checkbox-inline input[type=checkbox]:focus,.radio input[type=radio]:focus,.radio-inline input[type=radio]:focus,input[type=radio]:focus,input[type=checkbox]:focus{border-color:#4d90fe;outline:0}.checkbox input[type=checkbox]:active,.checkbox-inline input[type=checkbox]:active,.radio input[type=radio]:active,.radio-inline input[type=radio]:active,input[type=radio]:active,input[type=checkbox]:active{background-color:#ebebeb;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffffffff', GradientType=0);border-color:#c6c6c6}.checkbox input[type=checkbox]:checked,.checkbox-inline input[type=checkbox]:checked,.radio input[type=radio]:checked,.radio-inline input[type=radio]:checked,input[type=radio]:checked,input[type=checkbox]:checked{background:#fff}.radio input[type=radio],.radio-inline input[type=radio],input[type=radio]{width:15px;width:18px\9;height:15px;height:18px\9;border-radius:1em}.radio input[type=radio]:checked::after,.radio-inline input[type=radio]:checked::after,input[type=radio]:checked::after{position:relative;top:3px;left:3px;display:block;width:7px;height:7px;content:'';background:#666;border-radius:1em}.checkbox input[type=checkbox]:hover,.checkbox-inline input[type=checkbox]:hover,input[type=checkbox]:hover{border-color:#c6c6c6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.1);-webkit-box-shadow:none\9;box-shadow:inset 0 1px 1px rgba(0,0,0,.1);box-shadow:none\9}.checkbox input[type=checkbox]:checked::after,.checkbox-inline input[type=checkbox]:checked::after,input[type=checkbox]:checked::after{position:absolute;top:-6px;left:-5px;display:block;content:url(../img/checkmark.png)}.form-control-static{min-height:31px;padding-top:6px;padding-bottom:6px}.input-sm{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}select.input-sm{height:26px;line-height:26px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}.form-group-sm select.form-control{height:26px;line-height:26px}.form-group-sm .form-control-static{height:26px;min-height:30px;padding:4px 8px;font-size:12px;line-height:1.5}.input-lg{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}select.input-lg{height:38px;line-height:38px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}.form-group-lg select.form-control{height:38px;line-height:38px}.form-group-lg .form-control-static{height:38px;min-height:32px;padding:10px 14px;font-size:14px;line-height:1.3}.has-feedback .form-control{padding-right:37.5px}.form-control-feedback{top:23px;width:30px;height:30px;line-height:30px}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:38px;height:38px;line-height:38px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:26px;height:26px;line-height:26px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-success .form-control{-webkit-box-shadow:none;box-shadow:none}.has-success .form-control:hover{border-color:#3c763d;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-success .form-control:focus{border-color:#3c763d;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#e09b17}.has-warning .form-control{border-color:#e09b17;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#b27b12;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f0c36d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f0c36d}.has-warning .input-group-addon{color:#e09b17;background-color:#f9edbe;border-color:#e09b17}.has-warning .form-control-feedback{color:#e09b17}.has-warning .form-control{-webkit-box-shadow:none;box-shadow:none}.has-warning .form-control:hover{border-color:#e09b17;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-warning .form-control:focus{border-color:#e09b17;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#dd4b39}.has-error .form-control{border-color:#dd4b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#c23321;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ec9a90;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ec9a90}.has-error .input-group-addon{color:#dd4b39;background-color:#f2dede;border-color:#dd4b39}.has-error .form-control-feedback{color:#dd4b39}.has-error .form-control{-webkit-box-shadow:none;box-shadow:none}.has-error .form-control:hover{border-color:#dd4b39;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-error .form-control:focus{border-color:#dd4b39;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-feedback label~.form-control-feedback{top:23px}.help-block{color:#777}.form-horizontal .checkbox-inline,.form-horizontal .control-label,.form-horizontal .radio-inline{padding-top:5px}@media (min-width:768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static,.navbar-form .form-control-static{display:inline-block}.form-inline .input-group,.navbar-form .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio,.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label,.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio],.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-bottom:-2px;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:6px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:24px}@media (min-width:768px){.form-horizontal .control-label{padding-top:6px}.form-horizontal .has-feedback .form-control-feedback{top:0}}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:10px;font-size:14px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:4px;font-size:12px}}.btn{padding:5px 12px;font-size:13px;font-weight:700;line-height:18px;cursor:default;-webkit-background-clip:border-box;background-clip:border-box;border-radius:2px;-webkit-box-shadow:none;box-shadow:none}.btn:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.btn.active,.btn:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-default{color:#333;text-shadow:0 1px rgba(0,0,0,.1);text-shadow:0 1px 0 #fff;background-color:#f3f3f3;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #dcdcdc}.btn-default:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-default.active,.btn-default.focus,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e4e4e4;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e4e4e4 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e4e4e4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e4e4e4));background-image:linear-gradient(to bottom,#f5f5f5 0,#e4e4e4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe4e4e4', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #cfcfcf}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#f5f5f5 0,#d8d8d8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#d8d8d8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#d8d8d8));background-image:linear-gradient(to bottom,#f5f5f5 0,#d8d8d8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffd8d8d8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c3c3c3;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-default.focus,.btn-default:focus{border:1px solid #dcdcdc;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #dcdcdc;-webkit-box-shadow:none;box-shadow:none}.btn-default .badge{color:#dcdcdc;background-color:#333}.btn-default:hover{text-shadow:none;background-image:-webkit-linear-gradient(top,#f8f8f8 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f8f8f8 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f8f8f8),to(#f1f1f1));background-image:linear-gradient(to bottom,#f8f8f8 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8f8f8', endColorstr='#fff1f1f1', GradientType=0);background-repeat:repeat-x;background-position:0 0;border-color:#c6c6c6;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);-webkit-transition:none;-o-transition:none;transition:none}.btn-default.active,.btn-default:active,.open .dropdown-toggle.btn-default{text-shadow:0 1px 0 #fff;background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#f1f1f1));background-image:linear-gradient(to bottom,#f6f6f6 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);background-repeat:repeat-x;border:1px solid #dcdcdc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-default.focus,.btn-default:focus{background-color:#f3f3f3;border-color:#4d90fe;outline-style:none}.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{text-shadow:none;background-color:#f3f3f3}.btn-default .badge{color:#f3f3f3;text-shadow:none}.btn-primary{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed));background-image:linear-gradient(to bottom,#4d90fe 0,#4787ed 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff4787ed', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #3079ed}.btn-primary:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3078eb;background-image:-webkit-linear-gradient(top,#4d90fe 0,#3078eb 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#3078eb 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#3078eb));background-image:linear-gradient(to bottom,#4d90fe 0,#3078eb 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff3078eb', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #196aeb}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#4d90fe 0,#1969e8 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#1969e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#1969e8));background-image:linear-gradient(to bottom,#4d90fe 0,#1969e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff1969e8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #135fd7;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-primary.focus,.btn-primary:focus{border:1px solid #3079ed;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#4d90fe;background-image:-webkit-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed));background-image:linear-gradient(to bottom,#4d90fe 0,#4787ed 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff4787ed', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #3079ed;-webkit-box-shadow:none;box-shadow:none}.btn-primary .badge{color:#3079ed;background-color:#fff}.btn-success{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-o-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#35aa47));background-image:linear-gradient(to bottom,#35aa47 0,#35aa47 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff35aa47', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #359947}.btn-success:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#2f973f;background-image:-webkit-linear-gradient(top,#35aa47 0,#2f973f 100%);background-image:-o-linear-gradient(top,#35aa47 0,#2f973f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#2f973f));background-image:linear-gradient(to bottom,#35aa47 0,#2f973f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff2f973f', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #2e863e}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#35aa47 0,#298337 100%);background-image:-o-linear-gradient(top,#35aa47 0,#298337 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#298337));background-image:linear-gradient(to bottom,#35aa47 0,#298337 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff298337', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #287335;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-success.focus,.btn-success:focus{border:1px solid #359947;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#35aa47;background-image:-webkit-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-o-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#35aa47));background-image:linear-gradient(to bottom,#35aa47 0,#35aa47 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff35aa47', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #359947;-webkit-box-shadow:none;box-shadow:none}.btn-success .badge{color:#359947;background-color:#fff}.btn-info{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#5bc0de));background-image:linear-gradient(to bottom,#5bc0de 0,#5bc0de 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff5bc0de', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #46b8da}.btn-info:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#46b8da;background-image:-webkit-linear-gradient(top,#5bc0de 0,#46b8da 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#46b8da 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#46b8da));background-image:linear-gradient(to bottom,#5bc0de 0,#46b8da 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff46b8da', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #31b0d5}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #28a1c5;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-info.focus,.btn-info:focus{border:1px solid #46b8da;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;background-image:-webkit-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#5bc0de));background-image:linear-gradient(to bottom,#5bc0de 0,#5bc0de 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff5bc0de', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #46b8da;-webkit-box-shadow:none;box-shadow:none}.btn-info .badge{color:#46b8da;background-color:#fff}.btn-warning{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-o-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#faa937));background-image:linear-gradient(to bottom,#fbb450 0,#faa937 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fffaa937', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #faa328}.btn-warning:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#f99e1e;background-image:-webkit-linear-gradient(top,#fbb450 0,#f99e1e 100%);background-image:-o-linear-gradient(top,#fbb450 0,#f99e1e 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#f99e1e));background-image:linear-gradient(to bottom,#fbb450 0,#f99e1e 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff99e1e', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #f9980f}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#fbb450 0,#f89306 100%);background-image:-o-linear-gradient(top,#fbb450 0,#f89306 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#f89306));background-image:linear-gradient(to bottom,#fbb450 0,#f89306 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89306', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #e98b06;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-warning.focus,.btn-warning:focus{border:1px solid #faa328;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#fbb450;background-image:-webkit-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-o-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#faa937));background-image:linear-gradient(to bottom,#fbb450 0,#faa937 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fffaa937', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #faa328;-webkit-box-shadow:none;box-shadow:none}.btn-warning .badge{color:#faa328;background-color:#fff}.btn-danger{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#d14836));background-image:linear-gradient(to bottom,#dd4b39 0,#d14836 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffd14836', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c6322a}.btn-danger:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c13e2c;background-image:-webkit-linear-gradient(top,#dd4b39 0,#c13e2c 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#c13e2c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#c13e2c));background-image:linear-gradient(to bottom,#dd4b39 0,#c13e2c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffc13e2c', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #b12d26}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#dd4b39 0,#ad3727 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#ad3727 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#ad3727));background-image:linear-gradient(to bottom,#dd4b39 0,#ad3727 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffad3727', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #9c2721;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-danger.focus,.btn-danger:focus{border:1px solid #c6322a;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#dd4b39;background-image:-webkit-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#d14836));background-image:linear-gradient(to bottom,#dd4b39 0,#d14836 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffd14836', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c6322a;-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge{color:#c6322a;background-color:#fff}.btn-link{color:#15c}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link.focus,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link.focus,.btn-link:focus,.btn-link:hover{color:#15c;background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link[disabled]:focus .btn-link[disabled].focus,.btn-link[disabled]:focus fieldset[disabled] .btn-link.focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus .btn-link[disabled].focus,fieldset[disabled] .btn-link:focus fieldset[disabled] .btn-link.focus,fieldset[disabled] .btn-link:hover{color:#333}.btn-group-lg>.btn,.btn-lg{padding:9px 14px;font-size:14px;line-height:1.3;border-radius:2px}.btn-group-sm>.btn,.btn-sm{padding:3px 8px;font-size:12px;line-height:1.5;border-radius:2px}.btn-group-xs>.btn,.btn-xs{padding:2px 6px;font-size:11px;line-height:1.25;border-radius:1px}.dropdown-menu{padding:6px 0;margin:1px 0 0;font-size:13px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:0;-webkit-box-shadow:0 2px 4px rgba(0,0,0,.2);box-shadow:0 2px 4px rgba(0,0,0,.2)}.dropdown-menu .divider{height:1px;margin:8px 0;overflow:hidden;background-color:#ebebeb}.dropdown-menu>li>a{position:relative;padding:3px 30px}.dropdown-menu>li>a .glyphicon{position:absolute;top:4px;left:7px}.dropdown-menu li>a:focus,.dropdown-menu li>a:hover,.dropdown-submenu:focus>a,.dropdown-submenu:hover>a{color:#333;background-color:#eee;background-image:-webkit-linear-gradient(top,#eee 0,#eee 100%);background-image:-o-linear-gradient(top,#eee 0,#eee 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#eee));background-image:linear-gradient(to bottom,#eee 0,#eee 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeeeee', endColorstr='#ffeeeeee', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#333;background-color:#eee;background-image:-webkit-linear-gradient(top,#eee 0,#eee 100%);background-image:-o-linear-gradient(top,#eee 0,#eee 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#eee));background-image:linear-gradient(to bottom,#eee 0,#eee 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeeeee', endColorstr='#ffeeeeee', GradientType=0);background-repeat:repeat-x}.dropdown-header{color:#999}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-left:-1px;border-radius:0}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;border-radius:0}.dropdown-submenu>a:after{position:absolute;right:10px;margin-top:5px;content:""}.dropdown-submenu.dropdown-menu-left,.dropdown-submenu.pull-left{float:none!important}.dropdown-submenu.dropdown-menu-left>.dropdown-menu,.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:18px;border-radius:0}.btn-group-vertical>.btn:focus,.btn-group>.btn:focus{z-index:3}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:16px}.btn-group>.btn+.dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.btn-group>.dropdown-toggle:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.btn-group>.btn-danger.dropdown-toggle:hover,.btn-group>.btn-info.dropdown-toggle:hover,.btn-group>.btn-primary.dropdown-toggle:hover,.btn-group>.btn-success.dropdown-toggle:hover,.btn-group>.btn-warning.dropdown-toggle:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-group>.btn.dropdown-toggle.active,.btn-group>.btn.dropdown-toggle:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-group>.btn-danger.dropdown-toggle.active,.btn-group>.btn-danger.dropdown-toggle:active,.btn-group>.btn-info.dropdown-toggle.active,.btn-group>.btn-info.dropdown-toggle:active,.btn-group>.btn-primary.dropdown-toggle.active,.btn-group>.btn-primary.dropdown-toggle:active,.btn-group>.btn-success.dropdown-toggle.active,.btn-group>.btn-success.dropdown-toggle:active,.btn-group>.btn-warning.dropdown-toggle.active,.btn-group>.btn-warning.dropdown-toggle:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group>.btn-sm.dropdown-toggle{padding:5px 7px}.btn-group>.btn-lg.dropdown-toggle{padding:9px 9px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 1px 6px rgba(0,0,0,.15);box-shadow:inset 0 1px 6px rgba(0,0,0,.15)}.btn-group.open .btn.dropdown-toggle{background-color:#f3f3f3;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-group.open .btn-primary.dropdown-toggle{background-color:#4d90fe;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-warning.dropdown-toggle{background-color:#faa937;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-danger.dropdown-toggle{background-color:#d84a38;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-success.dropdown-toggle{background-color:#35aa47;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-info.dropdown-toggle{background-color:#5bc0de;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:2px;border-top-right-radius:2px}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-right-radius:2px;border-bottom-left-radius:2px}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:38px;line-height:38px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:26px;line-height:26px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{margin:0;border-radius:0}.input-group-addon{padding:5px 8px;font-size:13px;color:#555;border:1px solid #d9d9d9;border-top-color:silver;border-radius:2px}.input-group-addon.input-sm{padding:3px 8px;font-size:12px;border-radius:1px}.input-group-addon.input-lg{padding:9px 14px;font-size:14px;border-radius:1px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-bottom:-3px}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#999}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{color:#fff;background-color:#999;border-color:#999}.nav-tabs>li>a{color:#666;border-radius:2px 2px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{font-weight:700;color:#333}.nav-tabs-google>li{margin:0 -1px 0 0}.nav-tabs-google>li>a{padding:12px 8px;margin:0 8px;line-height:1.4;color:#777;border:3px solid transparent;border-width:3px 0;border-radius:0}.nav-tabs-google>li>a:first-of-type{margin-left:0}.nav-tabs-google>li>a:focus,.nav-tabs-google>li>a:hover{background-color:transparent;border-top-color:transparent}.nav-tabs-google>li>a:hover{color:#000;border-bottom-color:transparent}.nav-tabs-google>li>a:active{color:#dd4b39}.nav-tabs-google>li>a:focus{color:#000;outline:0}.nav-tabs-google>li.active>a,.nav-tabs-google>li.active>a:focus,.nav-tabs-google>li.active>a:hover{color:#dd4b39;border:3px solid transparent;border-width:3px 0;border-bottom-color:#dd4b39}.nav-pills>li>a{border-radius:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#4d90fe}.navbar{min-height:28px;margin-bottom:18px}@media (min-width:768px){.navbar{border-radius:2px}}.navbar-brand{height:28px;padding:5px 15px;font-size:14px;line-height:18px}.navbar-brand>.glyphicon{margin-top:0}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{padding:5px 10px;margin-top:1px;margin-right:15px;margin-bottom:1px;border-radius:2px}.navbar-nav{margin:2px -15px}.navbar-nav>li>a{padding-top:5px;padding-bottom:5px;line-height:18px}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a{line-height:18px}}@media (min-width:768px){.navbar-nav{margin:0}.navbar-nav>li>a{padding-top:5px;padding-bottom:5px}}.navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px;-webkit-box-shadow:none;box-shadow:none}.navbar-form>.input-group .form-control{margin-top:1px;margin-bottom:1px}@media (min-width:768px){.navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-form .form-control{height:26px;padding:3px 8px}.navbar .btn,.navbar-btn{padding:3px 8px;margin-top:1px;margin-bottom:1px}.navbar .btn.btn-sm,.navbar-btn.btn-sm{margin-top:1px;margin-bottom:1px}.navbar .btn.btn-xs,.navbar-btn.btn-xs{padding:2px 6px;margin-top:4px;margin-bottom:4px}.navbar-text{margin-top:5px;margin-bottom:5px}.navbar-default{background-color:#2d2d2d;border-color:#000}.navbar-default .navbar-brand{color:#999}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-default .navbar-brand>.caret{border-top-color:#999;border-bottom-color:#999}.navbar-default .navbar-text{color:#999}.navbar-default .navbar-nav>li>a{color:#999}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#fff;background-color:#141414}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#555;background-color:transparent}.navbar-default .navbar-toggle{border-color:#222}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#333}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#000}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#fff;background-color:#141414}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#141414}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#555;background-color:transparent}}.navbar-default .navbar-link{color:#999}.navbar-default .navbar-link:hover{color:#fff}.navbar-default .btn-link{color:#999}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#fff}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#555}.navbar-inverse{background-color:#fafafa;border-color:#dbdbdb}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:grey;background-color:transparent}.navbar-inverse .navbar-brand>.caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#333;background-color:#e1e1e1}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#ddd}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#ddd}.navbar-inverse .navbar-toggle .icon-bar{background-color:#888}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#e8e8e8}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#333;background-color:#e1e1e1}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#dbdbdb}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#dbdbdb}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#e1e1e1}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#333}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#333}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#ccc}.navbar-masthead{min-height:44px;margin-bottom:18px}@media (min-width:768px){.navbar-masthead{border-radius:2px}}.navbar-masthead .navbar-static-top{z-index:1005}.navbar-masthead .navbar-fixed-bottom,.navbar-masthead .navbar-fixed-top{z-index:1029}.navbar-masthead .navbar-brand{height:44px;padding:13px 15px;font-size:20px}.navbar-masthead .navbar-brand>.glyphicon{margin-top:-3px}@media (min-width:768px){.navbar>.container .navbar-masthead .navbar-brand,.navbar>.container-fluid .navbar-masthead .navbar-brand{margin-left:-15px}}.navbar-masthead .navbar-toggle{margin-top:7px;margin-right:15px;margin-bottom:7px}.navbar-masthead .navbar-nav{margin:6px -15px}@media (min-width:768px){.navbar-masthead .navbar-nav{margin:6px 0}.navbar-masthead .navbar-nav>li>a{padding-top:8px;padding-bottom:6px}}.navbar-masthead .navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px}.navbar-masthead .navbar-form>.input-group .form-control{margin-top:7px;margin-bottom:7px}@media (max-width:767px){.navbar-masthead .navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-masthead .navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-masthead .navbar-form .form-control{height:30px;padding:5px 8px}.navbar-masthead.navbar .btn,.navbar-masthead.navbar-btn{padding:5px 8px;margin-top:7px;margin-bottom:7px}.navbar-masthead.navbar .btn.btn-sm,.navbar-masthead.navbar-btn.btn-sm{padding:3px 8px;margin-top:9px;margin-bottom:9px}.navbar-masthead.navbar .btn.btn-xs,.navbar-masthead.navbar-btn.btn-xs{padding:2px 6px;margin-top:12px;margin-bottom:12px}.navbar-masthead .navbar-text{margin-top:13px;margin-bottom:13px}.navbar-masthead.navbar-default{background-color:#f1f1f1;border-color:#e5e5e5}.navbar-masthead.navbar-default .navbar-brand{color:#777}.navbar-masthead.navbar-default .navbar-brand:focus,.navbar-masthead.navbar-default .navbar-brand:hover{color:#777;background-color:transparent}.navbar-masthead.navbar-default .navbar-brand>.caret{border-top-color:#777;border-bottom-color:#777}.navbar-masthead.navbar-default .navbar-text{color:#777}.navbar-masthead.navbar-default .navbar-nav>li>a{color:#777}.navbar-masthead.navbar-default .navbar-nav>li>a:focus,.navbar-masthead.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-masthead.navbar-default .navbar-nav>.active>a,.navbar-masthead.navbar-default .navbar-nav>.active>a:focus,.navbar-masthead.navbar-default .navbar-nav>.active>a:hover{color:#333;background-color:#f1f1f1}.navbar-masthead.navbar-default .navbar-nav>.disabled>a,.navbar-masthead.navbar-default .navbar-nav>.disabled>a:focus,.navbar-masthead.navbar-default .navbar-nav>.disabled>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-default .navbar-toggle{border-color:#dcdcdc}.navbar-masthead.navbar-default .navbar-toggle:focus,.navbar-masthead.navbar-default .navbar-toggle:hover{background-color:#e4e4e4}.navbar-masthead.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-masthead.navbar-default .navbar-collapse,.navbar-masthead.navbar-default .navbar-form{border-color:#dfdfdf}.navbar-masthead.navbar-default .navbar-nav>.open>a,.navbar-masthead.navbar-default .navbar-nav>.open>a:focus,.navbar-masthead.navbar-default .navbar-nav>.open>a:hover{color:#333;background-color:#f1f1f1}@media (max-width:767px){.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#f1f1f1}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#bbb;background-color:transparent}}.navbar-masthead.navbar-default .navbar-link{color:#777}.navbar-masthead.navbar-default .navbar-link:hover{color:#333}.navbar-masthead.navbar-default .btn-link{color:#777}.navbar-masthead.navbar-default .btn-link:focus,.navbar-masthead.navbar-default .btn-link:hover{color:#333}.navbar-masthead.navbar-default .btn-link[disabled]:focus,.navbar-masthead.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-masthead.navbar-default .btn-link:focus,fieldset[disabled] .navbar-masthead.navbar-default .btn-link:hover{color:#bbb}.navbar-masthead.navbar-inverse{background-color:#444;border-color:#333}.navbar-masthead.navbar-inverse .navbar-brand{color:#fff}.navbar-masthead.navbar-inverse .navbar-brand:focus,.navbar-masthead.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-brand>.caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-masthead.navbar-inverse .navbar-text{color:#999}.navbar-masthead.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-masthead.navbar-inverse .navbar-nav>li>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>li>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-nav>.active>a,.navbar-masthead.navbar-inverse .navbar-nav>.active>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.active>a:hover{color:#bbb;background-color:#444}.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a,.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a:hover{color:#777;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-toggle{border-color:#222}.navbar-masthead.navbar-inverse .navbar-toggle:focus,.navbar-masthead.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-masthead.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-masthead.navbar-inverse .navbar-collapse,.navbar-masthead.navbar-inverse .navbar-form{border-color:#323232}.navbar-masthead.navbar-inverse .navbar-nav>.open>a,.navbar-masthead.navbar-inverse .navbar-nav>.open>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.open>a:hover{color:#bbb;background-color:#444}@media (max-width:767px){.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#333}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#333}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#bbb;background-color:#444}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#777;background-color:transparent}}.navbar-masthead.navbar-inverse .navbar-link{color:#fff}.navbar-masthead.navbar-inverse .navbar-link:hover{color:#bbb}.navbar-masthead.navbar-inverse .btn-link{color:#fff}.navbar-masthead.navbar-inverse .btn-link:focus,.navbar-masthead.navbar-inverse .btn-link:hover{color:#bbb}.navbar-masthead.navbar-inverse .btn-link[disabled]:focus,.navbar-masthead.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-masthead.navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-masthead.navbar-inverse .btn-link:hover{color:#777}.navbar-toolbar{min-height:36px;margin-bottom:18px}@media (min-width:768px){.navbar-toolbar{border-radius:2px}}.navbar-toolbar .navbar-static-top{z-index:1008}.navbar-toolbar .navbar-fixed-bottom,.navbar-toolbar .navbar-fixed-top{z-index:1028}.navbar-toolbar .navbar-brand{height:36px;padding:9px 15px;font-size:16px;font-weight:700}@media (min-width:768px){.navbar>.container .navbar-toolbar .navbar-brand,.navbar>.container-fluid .navbar-toolbar .navbar-brand{margin-left:-15px}}.navbar-toolbar .navbar-toggle{margin-top:3px;margin-right:15px;margin-bottom:3px}.navbar-toolbar .navbar-nav{margin:4px -15px}.navbar-toolbar .navbar-nav>li{position:relative}.navbar-toolbar .navbar-nav>li>a{padding:9px 15px}.navbar-toolbar .navbar-nav>li>a:focus,.navbar-toolbar .navbar-nav>li>a:hover{text-decoration:underline}.navbar-toolbar .navbar-nav>li>.dropdown-menu{margin-top:1px}.navbar-toolbar .navbar-nav>.active>a{font-weight:700}.navbar-toolbar .navbar-nav>.active>a:before{position:absolute;bottom:-1px;left:50%;display:inline-block;margin-left:-8px;content:'';border-right:8px solid transparent;border-bottom:8px solid transparent;border-left:8px solid transparent}.navbar-toolbar .navbar-nav>.active>a:after{position:absolute;bottom:-1px;left:50%;display:inline-block;margin-left:-7px;content:'';border-right:7px solid transparent;border-bottom:7px solid transparent;border-left:7px solid transparent}@media (min-width:768px){.navbar-toolbar .navbar-nav{margin:0}.navbar-toolbar .navbar-nav>li>a{padding-top:9px;padding-bottom:9px}}.navbar-toolbar .navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px}.navbar-toolbar .navbar-form>.input-group .form-control{margin-top:3px;margin-bottom:3px}@media (max-width:767px){.navbar-toolbar .navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-toolbar .navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-toolbar .navbar-form .form-control{height:30px;padding:5px 8px}.navbar-toolbar .dropdown-menu{border-top:1px none}.navbar-toolbar.navbar .btn,.navbar-toolbar.navbar-btn{padding:5px 8px;margin-top:3px;margin-bottom:3px}.navbar-toolbar.navbar .btn.btn-sm,.navbar-toolbar.navbar-btn.btn-sm{padding:3px 8px;margin-top:5px;margin-bottom:5px}.navbar-toolbar.navbar .btn.btn-xs,.navbar-toolbar.navbar-btn.btn-xs{padding:2px 6px;margin-top:8px;margin-bottom:8px}.navbar-toolbar .navbar-text{margin-top:9px;margin-bottom:9px}.navbar-toolbar.navbar-default{background-color:#fff;border-color:#ebebeb}.navbar-toolbar.navbar-default .navbar-brand{color:#dd4b39}.navbar-toolbar.navbar-default .navbar-brand:focus,.navbar-toolbar.navbar-default .navbar-brand:hover{color:#dd4b39;background-color:transparent}.navbar-toolbar.navbar-default .navbar-brand>.caret{border-top-color:#dd4b39;border-bottom-color:#dd4b39}.navbar-toolbar.navbar-default .navbar-text{color:#777}.navbar-toolbar.navbar-default .navbar-nav>li>a{color:#777}.navbar-toolbar.navbar-default .navbar-nav>li>a:focus,.navbar-toolbar.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-toolbar.navbar-default .navbar-nav>.active>a,.navbar-toolbar.navbar-default .navbar-nav>.active>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.active>a:hover{color:#333;background-color:#f2f2f2}.navbar-toolbar.navbar-default .navbar-nav>.active>a:before{border-bottom:8px solid #ebebeb}.navbar-toolbar.navbar-default .navbar-nav>.active>a:after{border-bottom:7px solid #fff}.navbar-toolbar.navbar-default .navbar-nav>.disabled>a,.navbar-toolbar.navbar-default .navbar-nav>.disabled>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.disabled>a:hover{color:#bbb;background-color:transparent}.navbar-toolbar.navbar-default .navbar-toggle{border-color:#dcdcdc}.navbar-toolbar.navbar-default .navbar-toggle:focus,.navbar-toolbar.navbar-default .navbar-toggle:hover{background-color:#e4e4e4}.navbar-toolbar.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-toolbar.navbar-default .navbar-collapse,.navbar-toolbar.navbar-default .navbar-form{border-color:#ededed}.navbar-toolbar.navbar-default .navbar-nav>.open>a,.navbar-toolbar.navbar-default .navbar-nav>.open>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.open>a:hover{color:#333;background-color:#f2f2f2}@media (max-width:767px){.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#f2f2f2}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#bbb;background-color:transparent}}.navbar-toolbar.navbar-default .navbar-link{color:#777}.navbar-toolbar.navbar-default .navbar-link:hover{color:#333}.navbar-toolbar.navbar-default .btn-link{color:#777}.navbar-toolbar.navbar-default .btn-link:focus,.navbar-toolbar.navbar-default .btn-link:hover{color:#333}.navbar-toolbar.navbar-default .btn-link[disabled]:focus,.navbar-toolbar.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-toolbar.navbar-default .btn-link:focus,fieldset[disabled] .navbar-toolbar.navbar-default .btn-link:hover{color:#bbb}.navbar-toolbar.navbar-inverse{background-color:#444;border-color:#333}.navbar-toolbar.navbar-inverse .navbar-brand{color:#fff}.navbar-toolbar.navbar-inverse .navbar-brand:focus,.navbar-toolbar.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-brand>.caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-toolbar.navbar-inverse .navbar-text{color:#999}.navbar-toolbar.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-toolbar.navbar-inverse .navbar-nav>li>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a,.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#444}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:before{border-bottom:8px solid #333}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:after{border-bottom:7px solid #fff}.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a,.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a:hover{color:#777;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-toggle{border-color:#222}.navbar-toolbar.navbar-inverse .navbar-toggle:focus,.navbar-toolbar.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-toolbar.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-toolbar.navbar-inverse .navbar-collapse,.navbar-toolbar.navbar-inverse .navbar-form{border-color:#323232}.navbar-toolbar.navbar-inverse .navbar-nav>.open>a,.navbar-toolbar.navbar-inverse .navbar-nav>.open>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#444}@media (max-width:767px){.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#333}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#333}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#444}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#777;background-color:transparent}}.navbar-toolbar.navbar-inverse .navbar-link{color:#fff}.navbar-toolbar.navbar-inverse .navbar-link:hover{color:#fff}.navbar-toolbar.navbar-inverse .btn-link{color:#fff}.navbar-toolbar.navbar-inverse .btn-link:focus,.navbar-toolbar.navbar-inverse .btn-link:hover{color:#fff}.navbar-toolbar.navbar-inverse .btn-link[disabled]:focus,.navbar-toolbar.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-toolbar.navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-toolbar.navbar-inverse .btn-link:hover{color:#777}.navbar-static-top{border-radius:0}.navbar-fixed-top,.navbar-static-top{border-width:1px 0}.navbar-fixed-bottom{border-width:1px 0}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;border-radius:0}.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0;margin-bottom:0}.navbar-btn{padding:3px 8px;margin-top:1px}.btn.navbar-masthead-btn{margin-top:7px}.btn.navbar-toolbar-btn{margin-top:3px}.navbar-link{color:#999}.navbar-link:hover{color:#fff}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#333}.navbar-form .checkbox-inline,.navbar-form .radio-inline{color:#999}.breadcrumb{padding:13px 15px;margin-bottom:18px;background-color:#f3f3f3;border-radius:2px}.breadcrumb>li+li{position:relative;display:inline-block;margin-left:20px}.breadcrumb>li+li:before{border-radius:5px}.breadcrumb>li+li:after,.breadcrumb>li+li:before{position:absolute;width:0;height:0;content:""}.breadcrumb>li+li:before{border:7px solid transparent}.breadcrumb>li+li:after{border:5px solid transparent}.breadcrumb>li+li:after,.breadcrumb>li+li:before{top:9px;left:100%}.breadcrumb>li+li:before{margin-top:-7px;border-left:7px solid;border-left-color:#777}.breadcrumb>li+li:after{margin-top:-5px;border-left:5px solid #f3f3f3}.breadcrumb>li+li:after,.breadcrumb>li+li:before{left:-16px}.breadcrumb>li+li:before{color:#999;content:""}.breadcrumb>li>a{color:#999}.breadcrumb>li>a:hover{color:#000}.breadcrumb>.active,.breadcrumb>.active>a{color:#000}.breadcrumb-inverse{background-color:#393832}.breadcrumb-inverse>li+li{position:relative;display:inline-block}.breadcrumb-inverse>li+li:before{border-radius:5px}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{position:absolute;width:0;height:0;content:""}.breadcrumb-inverse>li+li:before{border:7px solid transparent}.breadcrumb-inverse>li+li:after{border:5px solid transparent}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{top:9px;left:100%}.breadcrumb-inverse>li+li:before{margin-top:-7px;border-left:7px solid;border-left-color:#666}.breadcrumb-inverse>li+li:after{margin-top:-5px;border-left:5px solid #393832}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{left:-16px}.breadcrumb-inverse>li>a{color:#999}.breadcrumb-inverse>li>a:hover{color:#fff}.breadcrumb-inverse>.active,.breadcrumb-inverse>.active>a{color:#fff}.breadcrumb-sm{padding:4px 15px;background-color:#fff;border-bottom:1px solid #ebebeb}.breadcrumb-sm.breadcrumb-inverse{background-color:#393832}.pagination{margin:18px 0;border-radius:2px}.pagination>li>a,.pagination>li>span{padding:5px 12px;line-height:1.4;color:#333;background-color:#f3f3f3;border:1px solid #dcdcdc}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:2px;border-bottom-left-radius:2px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:2px;border-bottom-right-radius:2px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#333;background-color:#f5f5f5;border-color:#c6c6c6;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.pagination>li>a:active{background-color:#f4f4f4;background-image:-webkit-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#f1f1f1));background-image:linear-gradient(to bottom,#f6f6f6 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{color:#4d90fe;background-color:#f5f5f5;border-color:#c6c6c6;-webkit-box-shadow:none;box-shadow:none}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#b3b3b3;text-shadow:none;background-color:#f3f3f3;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}.pagination-lg>li>a,.pagination-lg>li>span{padding:9px 14px;font-size:14px;line-height:1.3}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:1px;border-bottom-left-radius:1px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:1px;border-bottom-right-radius:1px}.pagination-sm>li>a,.pagination-sm>li>span{padding:3px 8px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:1px;border-bottom-left-radius:1px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:1px;border-bottom-right-radius:1px}.pager{margin:18px 0}.pager li>a,.pager li>span{padding:11px 24px;overflow:visible;font-size:14px;color:#777;text-decoration:none;white-space:nowrap;cursor:default;background-color:#fff;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border:1px solid #5b5b5b;border:1px solid rgba(0,0,0,.1);border-radius:2px;outline:0;-webkit-box-shadow:0 2px 1px rgba(0,0,0,.1),0 0 1px rgba(0,0,0,.1);box-shadow:0 2px 1px rgba(0,0,0,.1),0 0 1px rgba(0,0,0,.1)}.pager li>a:focus,.pager li>a:hover{color:#444;background-color:#fff}.pager li>a:active{color:#444;background-color:#fff}.pager li .icon-prev{position:relative;display:inline-block;padding-right:8px}.pager li .icon-prev:before{border-radius:5px}.pager li .icon-prev:after,.pager li .icon-prev:before{position:absolute;width:0;height:0;content:""}.pager li .icon-prev:before{border:7px solid transparent}.pager li .icon-prev:after{border:4px solid transparent}.pager li .icon-prev:after,.pager li .icon-prev:before{top:-5px;right:100%}.pager li .icon-prev:before{margin-top:-7px;border-right:7px solid;border-right-color:inherit}.pager li .icon-prev:after{margin-top:-4px;border-right:4px solid #fff}.pager li .icon-next{position:relative;display:inline-block;padding-left:8px}.pager li .icon-next:before{border-radius:5px}.pager li .icon-next:after,.pager li .icon-next:before{position:absolute;width:0;height:0;content:""}.pager li .icon-next:before{border:7px solid transparent}.pager li .icon-next:after{border:4px solid transparent}.pager li .icon-next:after,.pager li .icon-next:before{top:-5px;left:100%}.pager li .icon-next:before{margin-top:-7px;border-left:7px solid;border-left-color:inherit}.pager li .icon-next:after{margin-top:-4px;border-left:4px solid #fff}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#b3b3b3;background-color:#fafafa;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}.pager .disabled .icon-prev{position:relative;display:inline-block;padding-right:8px}.pager .disabled .icon-prev:before{border-radius:5px}.pager .disabled .icon-prev:after,.pager .disabled .icon-prev:before{position:absolute;width:0;height:0;content:""}.pager .disabled .icon-prev:before{border:7px solid transparent}.pager .disabled .icon-prev:after{border:4px solid transparent}.pager .disabled .icon-prev:after,.pager .disabled .icon-prev:before{top:-5px;right:100%}.pager .disabled .icon-prev:before{margin-top:-7px;border-right:7px solid;border-right-color:#b3b3b3}.pager .disabled .icon-prev:after{margin-top:-4px;border-right:4px solid #fafafa}.pager .disabled .icon-next{position:relative;display:inline-block;padding-left:8px}.pager .disabled .icon-next:before{border-radius:5px}.pager .disabled .icon-next:after,.pager .disabled .icon-next:before{position:absolute;width:0;height:0;content:""}.pager .disabled .icon-next:before{border:7px solid transparent}.pager .disabled .icon-next:after{border:4px solid transparent}.pager .disabled .icon-next:after,.pager .disabled .icon-next:before{top:-5px;left:100%}.pager .disabled .icon-next:before{margin-top:-7px;border-left:7px solid;border-left-color:#b3b3b3}.pager .disabled .icon-next:after{margin-top:-4px;border-left:4px solid #fafafa}.label{font-size:80%;border-radius:0}.label-default{background-color:#999}.label-default[href]:focus,.label-default[href]:hover{background-color:grey}.label-primary{background-color:#4d90fe}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#1a70fe}.label-success{background-color:#35aa47}.label-success[href]:focus,.label-success[href]:hover{background-color:#298337}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#faa937}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#f89306}.label-danger{background-color:#d84a38}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#b93524}.badge{font-size:12px}.btn-group-xs>.btn .badge,.btn-xs .badge{font-size:11px}.list-group-item.active>.badge,li.list-group-item.active a>.badge{color:#fff;background-color:#dd4b39}.nav-pills>.active>a>.badge{color:#15c;background-color:#fff}.jumbotron{color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{font-size:20px}.container .jumbotron,.container-fluid .jumbotron{border-radius:1px}@media screen and (min-width:768px){.jumbotron .h1,.jumbotron h1{font-size:59px}}.thumbnail{display:block;padding:0;margin-bottom:18px;line-height:1.4;background-color:#fff;border:1px solid #fff;border-radius:0}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#fff;-webkit-box-shadow:0 0 0 1px #dedede;box-shadow:0 0 0 1px #dedede}.thumbnail .caption{padding:9px 4px;color:#000}.alert{padding:8px;margin-bottom:18px;border-radius:2px}.alert .alert-link{font-weight:700}.alert-dismissable,.alert-dismissible{padding-right:28px}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#a3d48e}.alert-success hr{border-top-color:#93cd7c}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#85c5e5}.alert-info hr{border-top-color:#70bbe1}.alert-info .alert-link{color:#245269}.alert-warning{color:#333;background-color:#f9edbe;border-color:#f0c36d}.alert-warning hr{border-top-color:#eeb956}.alert-warning .alert-link{color:#1a1a1a}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#d59595}.alert-danger hr{border-top-color:#ce8383}.alert-danger .alert-link{color:#843534}.alert-danger,.alert-info,.alert-success,.alert-warning{text-shadow:0 1px 0 rgba(255,255,255,.5)}.progress{height:14px;height:18px;padding:1px;margin-bottom:18px;font-size:12px;background-color:transparent;background-image:none;border:1px solid #999;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.progress-bar{line-height:1.25;background-color:#6188f5;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar-success{background-color:#2f973f}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#53bddc}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#fbb450}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#c13e2c}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group-item{color:#222;background-color:#fff;border:1px solid #e5e5e5}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item .dropdown{display:none}.list-group-item .dropdown-toggle{display:inline-block;padding:5px 6px 5px 5px;color:#222}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{font-weight:700;color:#dd4b39;background-color:transparent;border-color:#e5e5e5;border-left:4px solid #dd4b39;border-left-color:#dd4b39}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{font-weight:400;color:#888}.list-group-item.active:focus,.list-group-item.active:hover{background-color:#eee}a.list-group-item:focus,a.list-group-item:hover,li.list-group-item a:focus,li.list-group-item a:hover{color:#555;text-decoration:none;background-color:#eee}li.list-group-item{padding:0;margin-bottom:0;border:0 none}li.list-group-item>a{display:block;padding:5px 17px;margin:0 0 0 14px;color:#222}li.list-group-item.active,li.list-group-item.active:focus,li.list-group-item.active:hover{background-color:transparent}li.list-group-item.active:focus>a,li.list-group-item.active:hover>a,li.list-group-item.active>a{margin-left:10px;color:#dd4b39}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#333;background-color:#f9edbe}a.list-group-item-warning,button.list-group-item-warning{color:#333}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#333;background-color:#f7e7a7}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#333;border-color:#333}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-wrapper{margin-left:14px}.list-group-item-wrapper:hover>.dropdown{display:block}.list-group-item-wrapper>a{display:block;padding:5px 17px;margin:0;color:#222}.list-group-item-wrapper>.dropdown:hover+a{background-color:#eee}.list-group-item-wrapper>.dropdown.open{display:block}.list-group-item-wrapper>.dropdown.open+a{background-color:#eee}.list-group-item-wrapper>.dropdown>.dropdown-menu{margin-top:0}.list-group-header{display:block;padding:10px 30px 10px 15px;font-size:11px;font-weight:700;line-height:1.4;color:#999;text-shadow:0 1px 0 rgba(255,255,255,.5);text-transform:uppercase}li.list-group-header{padding:3px 15px}.list-group .list-group-header{margin-top:9px}.list-group-item-menu{padding:0;margin:0;border:0 none;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.list-group-item-menu .list-group-item-wrapper>a{padding-left:30px}.list-group-item-menu .list-group-item-menu .list-group-item-wrapper>a{padding-left:44px}.list-group-item-menu>.list-group-item .collapse-caret{margin-left:28px}.collapse-caret{position:absolute;z-index:1;display:inline-block;width:17px;height:28px;margin-left:14px}.collapse-caret:before{position:absolute;top:12px;left:5px;margin-left:0;content:'';border-bottom:0 dotted}.collapse-caret:hover{background-color:#eee}.collapse-caret.collapsed:before{top:10px;left:6px}.list-group .divider{height:1px;margin:8px 0;margin-right:15px;margin-left:15px;overflow:hidden;background-color:#e5e5e5}.panel{word-wrap:break-word;background-color:#fff;border:1px solid transparent;border-bottom-width:2px;border-radius:3px;-webkit-box-shadow:none;box-shadow:none}.panel-body{padding:15px 20px}.panel-heading{padding:15px 20px;border-top-left-radius:3px;border-top-right-radius:3px}.panel-title{font-size:16px}.panel-footer{padding:15px 20px;background-color:#f8f8f8;border-top:1px solid #e5e5e5;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{padding:15px 20px;padding-top:0}.panel>.list-group:first-child .list-group-item:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px 20px;padding-left:15px 20px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:2px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:2px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel-default{border-color:#d8d8d8}.panel-default>.panel-heading{color:#333;background-color:#fff;border-color:#fff}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d8d8d8}.panel-default>.panel-heading .badge{color:#fff;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d8d8d8}.panel-primary{border-color:#4d90fe}.panel-primary>.panel-heading{color:#fff;background-color:#4d90fe;border-color:#4d90fe}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#4d90fe}.panel-primary>.panel-heading .badge{color:#4d90fe;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#4d90fe}.panel-success{border-color:#a3d48e}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#a3d48e}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#a3d48e}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#a3d48e}.panel-info{border-color:#85c5e5}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#85c5e5}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#85c5e5}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#85c5e5}.panel-warning{border-color:#f0c36d}.panel-warning>.panel-heading{color:#333;background-color:#f9edbe;border-color:#f0c36d}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0c36d}.panel-warning>.panel-heading .badge{color:#f9edbe;background-color:#333}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0c36d}.panel-danger{border-color:#d59595}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#d59595}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d59595}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d59595}.panel-group{margin-bottom:18px}.panel-group .panel{border-color:transparent;border-radius:0}.panel-group .panel+.panel{margin-top:-3px}.panel-group .panel-heading{padding:0 15px;background-color:#fafafa;border-top:1px dashed #ccc;border-bottom:1px dashed #ccc}.panel-group .panel-heading a{display:block;padding:10px 0 9px;color:#444;text-decoration:none}.panel-group .panel-heading a:before{margin-right:7px;content:"\e082"}.panel-group .panel-heading a:hover{background-color:#f5f5f5}.panel-group .panel-heading a:focus{outline:0}.panel-group .panel-heading a.collapsed:before{margin-right:7px;content:"\e081"}.panel-group .panel-heading .panel-title{font-size:13px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:0 none}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:0 none}.well{background-color:#f1f1f1;border:1px solid #e5e5e5;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.well-lg{border-radius:0}.well-sm{border-radius:0}.scrollable::-webkit-scrollbar{width:10px;height:16px}.scrollable::-webkit-scrollbar:hover{background-color:#f3f3f3;border:1px solid #dbdbdb}.scrollable::-webkit-scrollbar-button:end:increment,.scrollable::-webkit-scrollbar-button:start:decrement{display:block;height:0;background-color:transparent}.scrollable::-webkit-scrollbar-track{-webkit-background-clip:padding-box;background-clip:padding-box;border:solid transparent;border-width:0 0 0 4px}.scrollable::-webkit-scrollbar-track-piece{background-color:transparent;border-radius:0}.scrollable::-webkit-scrollbar-thumb{background-color:#515151;background-color:rgba(0,0,0,.2);-webkit-background-clip:padding-box;background-clip:padding-box;border:solid transparent;border-width:0;-webkit-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07)}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#949494}.scrollable::-webkit-scrollbar-thumb:active{background-color:#3b3b3b;background-color:rgba(0,0,0,.5);-webkit-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);box-shadow:inset 1px 1px 3px rgba(0,0,0,.35)}.scrollable::-webkit-scrollbar-thumb:horizontal,.scrollable::-webkit-scrollbar-thumb:vertical{background-color:#c6c6c6;border-radius:0}.modal-content{color:#222;border:1px solid #aaa;border:1px solid rgba(0,0,0,.333);border-radius:0;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2);box-shadow:0 4px 16px rgba(0,0,0,.2)}.modal-backdrop{background-color:#fff}.modal-header .close{font-weight:400;filter:alpha(opacity=40);opacity:.4}.modal-body{padding:15px}.tooltip{font-family:Arial,Helvetica,sans-serif;font-size:11px;font-style:normal;font-weight:400;font-weight:700;line-height:1.4;line-height:1.25;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-break:break-word;word-spacing:normal;word-wrap:normal;white-space:normal;line-break:auto}.tooltip.in{filter:alpha(opacity=100);opacity:1}.tooltip-inner{padding:7px 9px;background-color:#2a2a2a;border:1px solid #fff;border-radius:0}.tooltip-arrow:before{position:absolute;z-index:-1;content:" ";border:7px solid transparent}.tooltip.top .tooltip-arrow,.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:1px;border-top-color:#2a2a2a}.tooltip.top .tooltip-arrow:before,.tooltip.top-left .tooltip-arrow:before,.tooltip.top-right .tooltip-arrow:before{top:-5px;left:-7px;border-top-color:#fff;border-bottom:0 dotted}.tooltip.right .tooltip-arrow{left:1px;border-right-color:#2a2a2a}.tooltip.right .tooltip-arrow:before{top:-7px;right:-5px;border-right-color:#fff;border-left:0 dotted}.tooltip.left .tooltip-arrow{right:1px;border-left-color:#2a2a2a}.tooltip.left .tooltip-arrow:before{top:-7px;left:-5px;border-right:0 dotted;border-left-color:#fff}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{top:1px;border-bottom-color:#2a2a2a}.tooltip.bottom .tooltip-arrow:before,.tooltip.bottom-left .tooltip-arrow:before,.tooltip.bottom-right .tooltip-arrow:before{bottom:-5px;left:-7px;border-top:0 dotted;border-bottom-color:#fff}.popover{padding:0;font-family:Arial,Helvetica,sans-serif;font-size:13px;font-style:normal;font-weight:400;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;border-radius:2px;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);line-break:auto}.popover-footer,.popover-title{padding:10px;font-size:13px;background-color:#f5f5f5;border-bottom:1px solid #ccc;border-bottom:1px solid rgba(0,0,0,.2);border-radius:0}.popover-footer{border-top:1px solid #ccc;border-top:1px solid rgba(0,0,0,.2);border-bottom:none}.popover-content{padding:10px}.carousel{width:100%;padding:50px;overflow:hidden;background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#eee 0,#f5f5f5 100%),-webkit-linear-gradient(bottom,#eee 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#eee 0,#f5f5f5 100%),-o-linear-gradient(bottom,#eee 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#f5f5f5)),-webkit-gradient(linear,left bottom,left top,from(#eee),to(#f5f5f5));background-image:linear-gradient(to bottom,#eee 0,#f5f5f5 100%),linear-gradient(to top,#eee 0,#f5f5f5 100%);background-repeat:no-repeat;background-position:0 0,0 100%;-webkit-background-size:100% 10px;background-size:100% 10px}.carousel-control{width:100px;color:#777;text-shadow:none;filter:alpha(opacity=33);opacity:.33}.carousel-control.left{background-image:none}.carousel-control.right{background-image:none}.carousel-control:focus,.carousel-control:hover{color:#777}.carousel-control .icon-next:before,.carousel-control .icon-prev:before{content:''}.carousel-control .icon-prev{position:relative;position:absolute;right:0;display:inline-block}.carousel-control .icon-prev:before{border-radius:20px}.carousel-control .icon-prev:after,.carousel-control .icon-prev:before{position:absolute;width:0;height:0;content:""}.carousel-control .icon-prev:before{border:22px solid transparent}.carousel-control .icon-prev:after{border:19px solid transparent}.carousel-control .icon-prev:after,.carousel-control .icon-prev:before{top:8px;right:100%}.carousel-control .icon-prev:before{margin-top:-22px;border-right:22px solid;border-right-color:#777}.carousel-control .icon-prev:after{margin-top:-19px;border-right:19px solid #f5f5f5}.carousel-control .icon-next{position:relative;position:absolute;right:0;left:50%;display:inline-block}.carousel-control .icon-next:before{border-radius:20px}.carousel-control .icon-next:after,.carousel-control .icon-next:before{position:absolute;width:0;height:0;content:""}.carousel-control .icon-next:before{border:22px solid transparent}.carousel-control .icon-next:after{border:19px solid transparent}.carousel-control .icon-next:after,.carousel-control .icon-next:before{top:8px;left:100%}.carousel-control .icon-next:before{margin-top:-22px;border-left:22px solid;border-left-color:#777}.carousel-control .icon-next:after{margin-top:-19px;border-left:19px solid #f5f5f5}.carousel-control .icon-next:after,.carousel-control .icon-next:before{left:50%}.carousel-indicators{bottom:5px;left:0;width:100%;margin-left:0}.carousel-indicators li{background-color:#c2c2c2;border:1px solid #c2c2c2}.carousel-indicators .active{width:10px;height:10px;margin:1px;background-color:#444;border:1px solid #444}.carousel-caption{right:0;bottom:0;left:0;padding:10px;color:#fff;text-shadow:none;background-color:#262626;background-color:rgba(0,0,0,.55)}
+/*# sourceMappingURL=todc-bootstrap.min.css.map */
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/assets/img/checkmark.png b/doc/DjangoBlog/blog/static/assets/img/checkmark.png
new file mode 100644
index 0000000..4bd0eb3
Binary files /dev/null and b/doc/DjangoBlog/blog/static/assets/img/checkmark.png differ
diff --git a/doc/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js b/doc/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js
new file mode 100644
index 0000000..3f97ba5
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/js/ie-emulation-modes-warning.js
@@ -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!')
+ }
+})();
diff --git a/doc/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js b/doc/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js
new file mode 100644
index 0000000..479a6eb
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/assets/js/ie10-viewport-bug-workaround.js
@@ -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)
+ }
+
+})();
diff --git a/doc/DjangoBlog/blog/static/blog/css/ie.css b/doc/DjangoBlog/blog/static/blog/css/ie.css
new file mode 100644
index 0000000..706f510
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/css/ie.css
@@ -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 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;
+}
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/blog/css/nprogress.css b/doc/DjangoBlog/blog/static/blog/css/nprogress.css
new file mode 100644
index 0000000..90c7b6c
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/css/nprogress.css
@@ -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); }
+}
+
diff --git a/doc/DjangoBlog/blog/static/blog/css/oauth_style.css b/doc/DjangoBlog/blog/static/blog/css/oauth_style.css
new file mode 100644
index 0000000..8af78af
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/css/oauth_style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/blog/css/style.css b/doc/DjangoBlog/blog/static/blog/css/style.css
new file mode 100644
index 0000000..1e539db
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/css/style.css
@@ -0,0 +1,2899 @@
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+
+body {
+ line-height: 1;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+caption,
+th,
+td {
+ font-weight: normal;
+ text-align: left;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ clear: both;
+}
+
+html {
+ overflow-y: scroll;
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+a:focus {
+ outline: thin dotted;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+audio:not([controls]) {
+ display: none;
+}
+
+del {
+ color: #333;
+}
+
+ins {
+ background: #fff9c0;
+ text-decoration: none;
+}
+
+hr {
+ background-color: #ccc;
+ border: 0;
+ height: 1px;
+ margin: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+small {
+ font-size: smaller;
+}
+
+img {
+ border: 0;
+ -ms-interpolation-mode: bicubic;
+}
+
+/* Clearing floats */
+.clear:after,
+.wrapper:after,
+.format-status .entry-header:after {
+ clear: both;
+}
+
+.clear:before,
+.clear:after,
+.wrapper:before,
+.wrapper:after,
+.format-status .entry-header:before,
+.format-status .entry-header:after {
+ display: table;
+ content: "";
+}
+
+
+/* =Repeatable patterns
+-------------------------------------------------------------- */
+
+/* Small headers */
+.archive-title,
+.page-title,
+.widget-title,
+.entry-content th,
+.comment-content th {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: #636363;
+}
+
+/* Shared Post Format styling */
+article.format-quote footer.entry-meta,
+article.format-link footer.entry-meta,
+article.format-status footer.entry-meta {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+}
+
+/* Form fields, general styles first */
+button,
+input,
+select,
+textarea {
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ font-family: inherit;
+ padding: 6px;
+ padding: 0.428571429rem;
+}
+
+button,
+input {
+ line-height: normal;
+}
+
+textarea {
+ font-size: 100%;
+ overflow: auto;
+ vertical-align: top;
+}
+
+/* Reset non-text input types */
+input[type="checkbox"],
+input[type="radio"],
+input[type="file"],
+input[type="hidden"],
+input[type="image"],
+input[type="color"] {
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+}
+
+/* Buttons */
+.menu-toggle,
+input[type="submit"],
+input[type="button"],
+input[type="reset"],
+article.post-password-required input[type=submit],
+.bypostauthor cite span {
+ padding: 6px 10px;
+ padding: 0.428571429rem 0.714285714rem;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 1.428571429;
+ font-weight: normal;
+ color: #7c7c7c;
+ background-color: #e6e6e6;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -ms-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -webkit-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -o-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: linear-gradient(to bottom, #f4f4f4, #e6e6e6);
+ border: 1px solid #d2d2d2;
+ border-radius: 3px;
+ box-shadow: 0 1px 2px rgba(64, 64, 64, 0.1);
+}
+
+.menu-toggle,
+button,
+input[type="submit"],
+input[type="button"],
+input[type="reset"] {
+ cursor: pointer;
+}
+
+button[disabled],
+input[disabled] {
+ cursor: default;
+}
+
+.menu-toggle:hover,
+.menu-toggle:focus,
+button:hover,
+input[type="submit"]:hover,
+input[type="button"]:hover,
+input[type="reset"]:hover,
+article.post-password-required input[type=submit]:hover {
+ color: #5e5e5e;
+ background-color: #ebebeb;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -ms-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -webkit-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -o-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: linear-gradient(to bottom, #f9f9f9, #ebebeb);
+}
+
+.menu-toggle:active,
+.menu-toggle.toggled-on,
+button:active,
+input[type="submit"]:active,
+input[type="button"]:active,
+input[type="reset"]:active {
+ color: #757575;
+ background-color: #e1e1e1;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -ms-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -webkit-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -o-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: linear-gradient(to bottom, #ebebeb, #e1e1e1);
+ box-shadow: inset 0 0 8px 2px #c6c6c6, 0 1px 0 0 #f4f4f4;
+ border-color: transparent;
+}
+
+.bypostauthor cite span {
+ color: #fff;
+ background-color: #21759b;
+ background-image: none;
+ border: 1px solid #1f6f93;
+ border-radius: 2px;
+ box-shadow: none;
+ padding: 0;
+}
+
+/* Responsive images */
+.entry-content img,
+.comment-content img,
+.widget img {
+ max-width: 100%; /* Fluid images for posts, comments, and widgets */
+}
+
+img[class*="align"],
+img[class*="wp-image-"],
+img[class*="attachment-"] {
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+
+img.size-full,
+img.size-large,
+img.header-image,
+img.wp-post-image {
+ max-width: 100%;
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+
+/* Make sure videos and embeds fit their containers */
+embed,
+iframe,
+object,
+video {
+ max-width: 100%;
+}
+
+.entry-content .twitter-tweet-rendered {
+ max-width: 100% !important; /* Override the Twitter embed fixed width */
+}
+
+/* Images */
+.alignleft {
+ float: left;
+}
+
+.alignright {
+ float: right;
+}
+
+.aligncenter {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.entry-content img,
+.comment-content img,
+.widget img,
+img.header-image,
+.author-avatar img,
+img.wp-post-image {
+ /* Add fancy borders to all WordPress-added images but not things like badges and icons and the like */
+ border-radius: 3px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+}
+
+.wp-caption {
+ max-width: 100%; /* Keep wide captions from overflowing their container. */
+ padding: 4px;
+}
+
+.wp-caption .wp-caption-text,
+.gallery-caption,
+.entry-caption {
+ font-style: italic;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ color: #757575;
+}
+
+img.wp-smiley,
+.rsswidget img {
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ margin-bottom: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.entry-content dl.gallery-item {
+ margin: 0;
+}
+
+.gallery-item a,
+.gallery-caption {
+ width: 90%;
+}
+
+.gallery-item a {
+ display: block;
+}
+
+.gallery-caption a {
+ display: inline;
+}
+
+.gallery-columns-1 .gallery-item a {
+ max-width: 100%;
+ width: auto;
+}
+
+.gallery .gallery-icon img {
+ height: auto;
+ max-width: 90%;
+ padding: 5%;
+}
+
+.gallery-columns-1 .gallery-icon img {
+ padding: 3%;
+}
+
+/* Navigation */
+.site-content nav {
+ clear: both;
+ line-height: 2;
+ overflow: hidden;
+}
+
+#nav-above {
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+#nav-above {
+ display: none;
+}
+
+.paged #nav-above {
+ display: block;
+}
+
+.nav-previous,
+.previous-image {
+ float: left;
+ width: 50%;
+}
+
+.nav-next,
+.next-image {
+ float: right;
+ text-align: right;
+ width: 50%;
+}
+
+.nav-single + .comments-area,
+#comment-nav-above {
+ margin: 48px 0;
+ margin: 3.428571429rem 0;
+}
+
+/* Author profiles */
+.author .archive-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.author-info {
+ border-top: 1px solid #ededed;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ padding-top: 24px;
+ padding-top: 1.714285714rem;
+ overflow: hidden;
+}
+
+.author-description p {
+ color: #757575;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.author.archive .author-info {
+ border-top: 0;
+ margin: 0 0 48px;
+ margin: 0 0 3.428571429rem;
+}
+
+.author.archive .author-avatar {
+ margin-top: 0;
+}
+
+
+/* =Basic structure
+-------------------------------------------------------------- */
+
+/* Body, links, basics */
+html {
+ font-size: 87.5%;
+}
+
+body {
+ font-size: 14px;
+ font-size: 1rem;
+ font-family: Helvetica, Arial, sans-serif;
+ text-rendering: optimizeLegibility;
+ color: #444;
+}
+
+body.custom-font-enabled {
+ font-family: "Open Sans", Helvetica, Arial, sans-serif;
+}
+
+a {
+ outline: none;
+ color: #21759b;
+}
+
+a:hover {
+ color: #0f3647;
+}
+
+/* Assistive text */
+.assistive-text,
+.site .screen-reader-text {
+ position: absolute !important;
+ clip: rect(1px, 1px, 1px, 1px);
+ overflow: hidden;
+ height: 1px;
+ width: 1px;
+}
+
+.main-navigation .assistive-text:focus,
+.site .screen-reader-text:hover,
+.site .screen-reader-text:active,
+.site .screen-reader-text:focus {
+ background: #fff;
+ border: 2px solid #333;
+ border-radius: 3px;
+ clip: auto !important;
+ color: #000;
+ display: block;
+ font-size: 12px;
+ height: auto;
+ padding: 12px;
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ width: auto;
+ z-index: 100000; /* Above WP toolbar */
+}
+
+/* Page structure */
+.site {
+ padding: 0 24px;
+ padding: 0 1.714285714rem;
+ background-color: #fff;
+}
+
+.site-content {
+ margin: 24px 0 0;
+ margin: 1.714285714rem 0 0;
+}
+
+.widget-area {
+ margin: 24px 0 0;
+ margin: 1.714285714rem 0 0;
+}
+
+/* Header */
+.site-header {
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+.site-header h1,
+.site-header h2 {
+ text-align: center;
+}
+
+.site-header h1 a,
+.site-header h2 a {
+ color: #515151;
+ display: inline-block;
+ text-decoration: none;
+}
+
+.site-header h1 a:hover,
+.site-header h2 a:hover {
+ color: #21759b;
+}
+
+.site-header h1 {
+ font-size: 24px;
+ font-size: 1.714285714rem;
+ line-height: 1.285714286;
+ margin-bottom: 14px;
+ margin-bottom: 1rem;
+}
+
+.site-header h2 {
+ font-weight: normal;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.header-image {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+}
+
+/* Navigation Menu */
+.main-navigation {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ text-align: center;
+}
+
+.main-navigation li {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.42857143;
+}
+
+.main-navigation a {
+ color: #5e5e5e;
+}
+
+.main-navigation a:hover,
+.main-navigation a:focus {
+ color: #21759b;
+}
+
+.main-navigation ul.nav-menu,
+.main-navigation div.nav-menu > ul {
+ display: none;
+}
+
+.main-navigation ul.nav-menu.toggled-on,
+.menu-toggle {
+ display: inline-block;
+}
+
+/* Banner */
+section[role="banner"] {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+}
+
+/* Sidebar */
+.widget-area .widget {
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ word-wrap: break-word;
+}
+
+.widget-area .widget h3 {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.widget-area .widget p,
+.widget-area .widget li,
+.widget-area .widget .textwidget {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.widget-area .widget p {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.widget-area .textwidget ul,
+.widget-area .textwidget ol {
+ list-style: disc outside;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+}
+
+.widget-area .textwidget li > ul,
+.widget-area .textwidget li > ol {
+ margin-bottom: 0;
+}
+
+.widget-area .textwidget ol {
+ list-style: decimal;
+}
+
+.widget-area .textwidget li {
+ margin-left: 36px;
+ margin-left: 2.571428571rem;
+}
+
+.widget-area .widget a {
+ color: #757575;
+}
+
+.widget-area .widget a:hover {
+ color: #21759b;
+}
+
+.widget-area .widget a:visited {
+ color: #9f9f9f;
+}
+
+.widget-area #s {
+ width: 53.66666666666%; /* define a width to avoid dropping a wider submit button */
+}
+
+/* Footer */
+footer[role="contentinfo"] {
+ border-top: 1px solid #ededed;
+ clear: both;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ max-width: 960px;
+ max-width: 68.571428571rem;
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+footer[role="contentinfo"] a {
+ color: #686868;
+}
+
+footer[role="contentinfo"] a:hover {
+ color: #21759b;
+}
+
+.site-info span[role=separator] {
+ padding: 0 0.3em 0 0.6em;
+}
+
+.site-info span[role=separator]::before {
+ content: '\002f';
+}
+
+
+/* =Main content and comment content
+-------------------------------------------------------------- */
+
+.entry-meta {
+ clear: both;
+}
+
+.entry-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-header img.wp-post-image {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-header .entry-title {
+ font-size: 20px;
+ font-size: 1.428571429rem;
+ line-height: 1.2;
+ font-weight: normal;
+}
+
+.entry-header .entry-title a {
+ text-decoration: none;
+}
+
+.entry-header .entry-format {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-weight: normal;
+}
+
+.entry-header .comments-link {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.comments-link a,
+.entry-meta a {
+ color: #757575;
+}
+
+.comments-link a:hover,
+.entry-meta a:hover {
+ color: #21759b;
+}
+
+article.sticky .featured-post {
+ border-top: 4px double #ededed;
+ border-bottom: 4px double #ededed;
+ color: #757575;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 3.692307692;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ text-align: center;
+}
+
+.entry-content,
+.entry-summary,
+.mu_register {
+ line-height: 1.714285714;
+}
+
+.entry-content h1,
+.comment-content h1,
+.entry-content h2,
+.comment-content h2,
+.entry-content h3,
+.comment-content h3,
+.entry-content h4,
+.comment-content h4,
+.entry-content h5,
+.comment-content h5,
+.entry-content h6,
+.comment-content h6 {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ line-height: 1.714285714;
+}
+
+.entry-content h1,
+.comment-content h1 {
+ font-size: 21px;
+ font-size: 1.5rem;
+ line-height: 1.5;
+}
+
+.entry-content h2,
+.comment-content h2,
+.mu_register h2 {
+ font-size: 18px;
+ font-size: 1.285714286rem;
+ line-height: 1.6;
+}
+
+.entry-content h3,
+.comment-content h3 {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h4,
+.comment-content h4 {
+ font-size: 14px;
+ font-size: 1rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h5,
+.comment-content h5 {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h6,
+.comment-content h6 {
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.846153846;
+}
+
+.entry-content p,
+.entry-summary p,
+.comment-content p,
+.mu_register p {
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ line-height: 1.714285714;
+}
+
+.entry-content a:visited,
+.comment-content a:visited {
+ color: #9f9f9f;
+}
+
+.entry-content .more-link {
+ white-space: nowrap;
+}
+
+.entry-content ol,
+.comment-content ol,
+.entry-content ul,
+.comment-content ul,
+.mu_register ul {
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ line-height: 1.714285714;
+}
+
+.entry-content ul ul,
+.comment-content ul ul,
+.entry-content ol ol,
+.comment-content ol ol,
+.entry-content ul ol,
+.comment-content ul ol,
+.entry-content ol ul,
+.comment-content ol ul {
+ margin-bottom: 0;
+}
+
+.entry-content ul,
+.comment-content ul,
+.mu_register ul {
+ list-style: disc outside;
+}
+
+.entry-content ol,
+.comment-content ol {
+ list-style: decimal outside;
+}
+
+.entry-content li,
+.comment-content li,
+.mu_register li {
+ margin: 0 0 0 36px;
+ margin: 0 0 0 2.571428571rem;
+}
+
+.entry-content blockquote,
+.comment-content blockquote {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ padding: 24px;
+ padding: 1.714285714rem;
+ font-style: italic;
+}
+
+.entry-content blockquote p:last-child,
+.comment-content blockquote p:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content code,
+.comment-content code {
+ font-family: Consolas, Monaco, Lucida Console, monospace;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+}
+
+.entry-content pre,
+.comment-content pre {
+ border: 1px solid #ededed;
+ color: #666;
+ font-family: Consolas, Monaco, Lucida Console, monospace;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.714285714;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ overflow: auto;
+ padding: 24px;
+ padding: 1.714285714rem;
+}
+
+.entry-content pre code,
+.comment-content pre code {
+ display: block;
+}
+
+.entry-content abbr,
+.comment-content abbr,
+.entry-content dfn,
+.comment-content dfn,
+.entry-content acronym,
+.comment-content acronym {
+ border-bottom: 1px dotted #666;
+ cursor: help;
+}
+
+.entry-content address,
+.comment-content address {
+ display: block;
+ line-height: 1.714285714;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+}
+
+img.alignleft,
+.wp-caption.alignleft {
+ margin: 12px 24px 12px 0;
+ margin: 0.857142857rem 1.714285714rem 0.857142857rem 0;
+}
+
+img.alignright,
+.wp-caption.alignright {
+ margin: 12px 0 12px 24px;
+ margin: 0.857142857rem 0 0.857142857rem 1.714285714rem;
+}
+
+img.aligncenter,
+.wp-caption.aligncenter {
+ clear: both;
+ margin-top: 12px;
+ margin-top: 0.857142857rem;
+ margin-bottom: 12px;
+ margin-bottom: 0.857142857rem;
+}
+
+.entry-content embed,
+.entry-content iframe,
+.entry-content object,
+.entry-content video {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-content dl,
+.comment-content dl {
+ margin: 0 24px;
+ margin: 0 1.714285714rem;
+}
+
+.entry-content dt,
+.comment-content dt {
+ font-weight: bold;
+ line-height: 1.714285714;
+}
+
+.entry-content dd,
+.comment-content dd {
+ line-height: 1.714285714;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-content table,
+.comment-content table {
+ border-bottom: 1px solid #ededed;
+ color: #757575;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ width: 100%;
+}
+
+.entry-content table caption,
+.comment-content table caption {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+.entry-content td,
+.comment-content td {
+ border-top: 1px solid #ededed;
+ padding: 6px 10px 6px 0;
+}
+
+.site-content article {
+ border-bottom: 4px double #ededed;
+ margin-bottom: 72px;
+ margin-bottom: 5.142857143rem;
+ padding-bottom: 24px;
+ padding-bottom: 1.714285714rem;
+ word-wrap: break-word;
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+}
+
+.page-links {
+ clear: both;
+ line-height: 1.714285714;
+}
+
+footer.entry-meta {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.single-author .entry-meta .by-author {
+ display: none;
+}
+
+.mu_register h2 {
+ color: #757575;
+ font-weight: normal;
+}
+
+
+/* =Archives
+-------------------------------------------------------------- */
+
+.archive-header,
+.page-header {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ padding-bottom: 22px;
+ padding-bottom: 1.571428571rem;
+ border-bottom: 1px solid #ededed;
+}
+
+.archive-meta {
+ color: #757575;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ margin-top: 22px;
+ margin-top: 1.571428571rem;
+}
+
+/* =Single audio/video attachment view
+-------------------------------------------------------------- */
+
+.attachment .entry-content .mejs-audio {
+ max-width: 400px;
+}
+
+.attachment .entry-content .mejs-container {
+ margin-bottom: 24px;
+}
+
+
+/* =Single image attachment view
+-------------------------------------------------------------- */
+
+.article.attachment {
+ overflow: hidden;
+}
+
+.image-attachment div.attachment {
+ text-align: center;
+}
+
+.image-attachment div.attachment p {
+ text-align: center;
+}
+
+.image-attachment div.attachment img {
+ display: block;
+ height: auto;
+ margin: 0 auto;
+ max-width: 100%;
+}
+
+.image-attachment .entry-caption {
+ margin-top: 8px;
+ margin-top: 0.571428571rem;
+}
+
+
+/* =Aside post format
+-------------------------------------------------------------- */
+
+article.format-aside h1 {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+article.format-aside h1 a {
+ text-decoration: none;
+ color: #4d525a;
+}
+
+article.format-aside h1 a:hover {
+ color: #2e3542;
+}
+
+article.format-aside .aside {
+ padding: 24px 24px 0;
+ padding: 1.714285714rem;
+ background: #d2e0f9;
+ border-left: 22px solid #a8bfe8;
+}
+
+article.format-aside p {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #4a5466;
+}
+
+article.format-aside blockquote:last-child,
+article.format-aside p:last-child {
+ margin-bottom: 0;
+}
+
+
+/* =Post formats
+-------------------------------------------------------------- */
+
+/* Image posts */
+article.format-image footer h1 {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ font-weight: normal;
+}
+
+article.format-image footer h2 {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+}
+
+article.format-image footer a h2 {
+ font-weight: normal;
+}
+
+/* Link posts */
+article.format-link header {
+ padding: 0 10px;
+ padding: 0 0.714285714rem;
+ float: right;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ font-weight: bold;
+ font-style: italic;
+ text-transform: uppercase;
+ color: #848484;
+ background-color: #ebebeb;
+ border-radius: 3px;
+}
+
+article.format-link .entry-content {
+ max-width: 80%;
+ float: left;
+}
+
+article.format-link .entry-content a {
+ font-size: 22px;
+ font-size: 1.571428571rem;
+ line-height: 1.090909091;
+ text-decoration: none;
+}
+
+/* Quote posts */
+article.format-quote .entry-content p {
+ margin: 0;
+ padding-bottom: 24px;
+ padding-bottom: 1.714285714rem;
+}
+
+article.format-quote .entry-content blockquote {
+ display: block;
+ padding: 24px 24px 0;
+ padding: 1.714285714rem 1.714285714rem 0;
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ line-height: 1.6;
+ font-style: normal;
+ color: #6a6a6a;
+ background: #efefef;
+}
+
+/* Status posts */
+.format-status .entry-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.format-status .entry-header header {
+ display: inline-block;
+}
+
+.format-status .entry-header h1 {
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ font-weight: normal;
+ line-height: 1.6;
+ margin: 0;
+}
+
+.format-status .entry-header h2 {
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ font-weight: normal;
+ line-height: 2;
+ margin: 0;
+}
+
+.format-status .entry-header header a {
+ color: #757575;
+}
+
+.format-status .entry-header header a:hover {
+ color: #21759b;
+}
+
+.format-status .entry-header img {
+ float: left;
+ margin-right: 21px;
+ margin-right: 1.5rem;
+}
+
+
+/* =Comments
+-------------------------------------------------------------- */
+
+.comments-title {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.5;
+ font-weight: normal;
+}
+
+.comments-area article {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+.comments-area article header {
+ margin: 0 0 48px;
+ margin: 0 0 3.428571429rem;
+ overflow: hidden;
+ position: relative;
+}
+
+.comments-area article header img {
+ float: left;
+ padding: 0;
+ line-height: 0;
+}
+
+.comments-area article header cite,
+.comments-area article header time {
+ display: block;
+ margin-left: 85px;
+ margin-left: 6.071428571rem;
+}
+
+.comments-area article header cite {
+ font-style: normal;
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ line-height: 1.42857143;
+}
+
+.comments-area cite b {
+ font-weight: normal;
+}
+
+.comments-area article header time {
+ line-height: 1.714285714;
+ text-decoration: none;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ color: #5e5e5e;
+}
+
+.comments-area article header a {
+ text-decoration: none;
+ color: #5e5e5e;
+}
+
+.comments-area article header a:hover {
+ color: #21759b;
+}
+
+.comments-area article header cite a {
+ color: #444;
+}
+
+.comments-area article header cite a:hover {
+ text-decoration: underline;
+}
+
+.comments-area article header h4 {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 6px 12px;
+ padding: 0.428571429rem 0.857142857rem;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ font-weight: normal;
+ color: #fff;
+ background-color: #0088d0;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #009cee, #0088d0);
+ background-image: -ms-linear-gradient(top, #009cee, #0088d0);
+ background-image: -webkit-linear-gradient(top, #009cee, #0088d0);
+ background-image: -o-linear-gradient(top, #009cee, #0088d0);
+ background-image: linear-gradient(to bottom, #009cee, #0088d0);
+ border-radius: 3px;
+ border: 1px solid #007cbd;
+}
+
+.comments-area .bypostauthor cite span {
+ position: absolute;
+ margin-left: 5px;
+ margin-left: 0.357142857rem;
+ padding: 2px 5px;
+ padding: 0.142857143rem 0.357142857rem;
+ font-size: 10px;
+ font-size: 0.714285714rem;
+}
+
+.comments-area .bypostauthor cite b {
+ font-weight: bold;
+}
+
+a.comment-reply-link,
+a.comment-edit-link {
+ color: #686868;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+a.comment-reply-link:hover,
+a.comment-edit-link:hover {
+ color: #21759b;
+}
+
+.commentlist .pingback {
+ line-height: 1.714285714;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+/* Comment form */
+#respond {
+ margin-top: 48px;
+ margin-top: 3.428571429rem;
+}
+
+#respond h3#reply-title {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.5;
+}
+
+#respond h3#reply-title #cancel-comment-reply-link {
+ margin-left: 10px;
+ margin-left: 0.714285714rem;
+ font-weight: normal;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+}
+
+#respond form {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+#respond form p {
+ margin: 11px 0;
+ margin: 0.785714286rem 0;
+}
+
+#respond form p.logged-in-as {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+#respond form label {
+ display: block;
+ line-height: 1.714285714;
+}
+
+#respond form input[type="text"],
+#respond form textarea {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.714285714;
+ padding: 10px;
+ padding: 0.714285714rem;
+ width: 100%;
+}
+
+#respond form p.form-allowed-tags {
+ margin: 0;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ color: #5e5e5e;
+}
+
+#respond #wp-comment-cookies-consent {
+ margin: 0 10px 0 0;
+}
+
+#respond .comment-form-cookies-consent label {
+ display: inline;
+}
+
+.required {
+ color: red;
+}
+
+
+/* =Front page template
+-------------------------------------------------------------- */
+
+.entry-page-image {
+ margin-bottom: 14px;
+ margin-bottom: 1rem;
+}
+
+.template-front-page .site-content article {
+ border: 0;
+ margin-bottom: 0;
+}
+
+.template-front-page .widget-area {
+ clear: both;
+ float: none;
+ width: auto;
+ padding-top: 24px;
+ padding-top: 1.714285714rem;
+ border-top: 1px solid #ededed;
+}
+
+.template-front-page .widget-area .widget li {
+ margin: 8px 0 0;
+ margin: 0.571428571rem 0 0;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.714285714;
+ list-style-type: square;
+ list-style-position: inside;
+}
+
+.template-front-page .widget-area .widget li a {
+ color: #757575;
+}
+
+.template-front-page .widget-area .widget li a:hover {
+ color: #21759b;
+}
+
+.template-front-page .widget-area .widget_text img {
+ float: left;
+ margin: 8px 24px 8px 0;
+ margin: 0.571428571rem 1.714285714rem 0.571428571rem 0;
+}
+
+
+/* =Widgets
+-------------------------------------------------------------- */
+
+.widget select {
+ max-width: 100%;
+}
+
+.widget-area .widget ul ul {
+ margin-left: 12px;
+ margin-left: 0.857142857rem;
+}
+
+.widget_rss li {
+ margin: 12px 0;
+ margin: 0.857142857rem 0;
+}
+
+.widget_recent_entries .post-date,
+.widget_rss .rss-date {
+ color: #aaa;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ margin-left: 12px;
+ margin-left: 0.857142857rem;
+}
+
+.wp-calendar-nav,
+#wp-calendar {
+ margin: 0;
+ width: 100%;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #686868;
+}
+
+#wp-calendar th,
+#wp-calendar td,
+#wp-calendar caption {
+ text-align: left;
+}
+
+.wp-calendar-nav {
+ display: table;
+}
+
+.wp-calendar-nav span {
+ display: table-cell;
+}
+
+.wp-calendar-nav-next,
+#wp-calendar #next {
+ padding-right: 24px;
+ padding-right: 1.714285714rem;
+ text-align: right;
+}
+
+.widget_search label {
+ display: block;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.widget_twitter li {
+ list-style-type: none;
+}
+
+.widget_twitter .timesince {
+ display: block;
+ text-align: right;
+}
+
+.tagcloud ul {
+ list-style-type: none;
+}
+
+.tagcloud ul li {
+ display: inline-block;
+}
+
+.widget-area .widget.widget_tag_cloud li {
+ line-height: 1;
+}
+
+.template-front-page .widget-area .widget.widget_tag_cloud li {
+ margin: 0;
+}
+
+.widget-area .gallery-columns-2.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-3.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-4.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-5.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-6 .gallery-icon img,
+.widget-area .gallery-columns-7 .gallery-icon img,
+.widget-area .gallery-columns-8 .gallery-icon img,
+.widget-area .gallery-columns-9 .gallery-icon img {
+ height: auto;
+ max-width: 80%;
+}
+
+/* =Plugins
+----------------------------------------------- */
+
+img#wpstats {
+ display: block;
+ margin: 0 auto 24px;
+ margin: 0 auto 1.714285714rem;
+}
+
+
+/* =Media queries
+-------------------------------------------------------------- */
+
+/* Does the same thing as ,
+ * but in the future W3C standard way. -ms- prefix is required for IE10+ to
+ * render responsive styling in Windows 8 "snapped" views; IE10+ does not honor
+ * the meta tag. See https://core.trac.wordpress.org/ticket/25888.
+ */
+@-ms-viewport {
+ width: device-width;
+}
+
+@viewport {
+ width: device-width;
+}
+
+/* Minimum width of 600 pixels. */
+@media screen and (min-width: 600px) {
+ .author-avatar {
+ float: left;
+ margin-top: 8px;
+ margin-top: 0.571428571rem;
+ }
+
+ .author-description {
+ float: right;
+ width: 80%;
+ }
+
+ .site {
+ margin: 0 auto;
+ max-width: 960px;
+ max-width: 68.571428571rem;
+ overflow: hidden;
+ }
+
+ .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;
+ font-size: 1.857142857rem;
+ line-height: 1.846153846;
+ margin-bottom: 0;
+ }
+
+ .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;
+ }
+
+ .main-navigation li a {
+ border-bottom: 0;
+ color: #6a6a6a;
+ line-height: 3.692307692;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .main-navigation li a:hover,
+ .main-navigation li a:focus {
+ color: #000;
+ }
+
+ .main-navigation li {
+ margin: 0 40px 0 0;
+ margin: 0 2.857142857rem 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);
+ }
+
+ .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;
+ }
+
+ .main-navigation li ul li a {
+ background: #efefef;
+ border-bottom: 1px solid #ededed;
+ display: block;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ padding: 8px 10px;
+ padding: 0.571428571rem 0.714285714rem;
+ width: 180px;
+ width: 12.85714286rem;
+ white-space: normal;
+ }
+
+ .main-navigation li ul li a:hover,
+ .main-navigation li ul li a:focus {
+ 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;
+ }
+
+ .menu-toggle {
+ display: none;
+ }
+
+ .entry-header .entry-title {
+ font-size: 22px;
+ font-size: 1.571428571rem;
+ }
+
+ #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%;
+ }
+
+ .template-front-page .widget-area .widget,
+ .template-front-page.two-sidebars .widget-area .front-widgets {
+ float: left;
+ width: 51.875%;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ }
+
+ .template-front-page .widget-area .widget:nth-child(odd) {
+ clear: right;
+ }
+
+ .template-front-page .widget-area .widget:nth-child(even),
+ .template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
+ float: right;
+ width: 39.0625%;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ }
+
+ .template-front-page.two-sidebars .widget,
+ .template-front-page.two-sidebars .widget:nth-child(even) {
+ float: none;
+ width: auto;
+ }
+
+ .commentlist .children {
+ margin-left: 48px;
+ margin-left: 3.428571429rem;
+ }
+}
+
+/* mk:Minimum width of 960 pixels. */
+@media screen and (min-width: 960px) {
+ body {
+ background-color: #e6e6e6;
+ }
+
+ body .site {
+ padding: 0 40px;
+ padding: 0 2.857142857rem;
+ margin-top: 48px;
+ margin-top: 3.428571429rem;
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
+ }
+
+ body.custom-background-empty {
+ background-color: #fff;
+ }
+
+ body.custom-background-empty .site,
+ body.custom-background-white .site {
+ padding: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ box-shadow: none;
+ }
+}
+
+
+/* =Print
+----------------------------------------------- */
+
+@media print {
+ body {
+ background: none !important;
+ color: #000;
+ font-size: 10pt;
+ }
+
+ footer a[rel=bookmark]:link:after,
+ footer a[rel=bookmark]:visited:after {
+ content: " [" attr(href) "] "; /* Show URLs */
+ }
+
+ a {
+ text-decoration: none;
+ }
+
+ .entry-content img,
+ .comment-content img,
+ .author-avatar img,
+ img.wp-post-image {
+ border-radius: 0;
+ box-shadow: none;
+ }
+
+ .site {
+ clear: both !important;
+ display: block !important;
+ float: none !important;
+ max-width: 100%;
+ position: relative !important;
+ }
+
+ .site-header {
+ margin-bottom: 72px;
+ margin-bottom: 5.142857143rem;
+ text-align: left;
+ }
+
+ .site-header h1 {
+ font-size: 21pt;
+ line-height: 1;
+ text-align: left;
+ }
+
+ .site-header h2 {
+ color: #000;
+ font-size: 10pt;
+ text-align: left;
+ }
+
+ .site-header h1 a,
+ .site-header h2 a {
+ color: #000;
+ }
+
+ .author-avatar,
+ #colophon,
+ #respond,
+ .commentlist .comment-edit-link,
+ .commentlist .reply,
+ .entry-header .comments-link,
+ .entry-meta .edit-link a,
+ .page-link,
+ .site-content nav,
+ .widget-area,
+ img.header-image,
+ .main-navigation {
+ display: none;
+ }
+
+ .wrapper {
+ border-top: none;
+ box-shadow: none;
+ }
+
+ .site-content {
+ margin: 0;
+ width: auto;
+ }
+
+ .entry-header .entry-title,
+ .entry-title {
+ font-size: 21pt;
+ }
+
+ footer.entry-meta,
+ footer.entry-meta a {
+ color: #444;
+ font-size: 10pt;
+ }
+
+ .author-description {
+ float: none;
+ width: auto;
+ }
+
+ /* Comments */
+ .commentlist > li.comment {
+ background: none;
+ position: relative;
+ width: auto;
+ }
+
+
+
+ .comments-area article header cite,
+ .comments-area article header time {
+ margin-left: 50px;
+ margin-left: 3.57142857rem;
+ }
+}
+
+.breadcrumb
+div {
+ display: inline;
+ font-size: 13px;
+ margin-left: -3px;
+}
+
+#wp-auto-top {
+ position: fixed;
+ top: 45%;
+ right: 50%;
+ display: block;
+ margin-right: -540px;
+ z-index: 9999;
+}
+
+#wp-auto-top-top, #wp-auto-top-comment, #wp-auto-top-bottom {
+ background: url(https://www.lylinux.org/wp-content/plugins/wp-auto-top/img/1.png) no-repeat;
+ position: relative;
+ cursor: pointer;
+ height: 25px;
+ width: 29px;
+ margin: 10px 0 0;
+}
+
+#wp-auto-top-comment {
+ background-position: left -30px;
+ height: 32px;
+}
+
+#wp-auto-top-bottom {
+ background-position: left -68px;
+}
+
+#wp-auto-top-comment:hover {
+ background-position: right -30px;
+}
+
+#wp-auto-top-top:hover {
+ background-position: right 0;
+}
+
+#wp-auto-top-bottom:hover {
+ background-position: right -68px;
+}
+
+.widget-login {
+ margin-top: 15px !important;
+}
+
+/* ------------------------------------------------------------------------- *
+ * Comments
+/* ------------------------------------------------------------------------- */
+#comments {
+ margin-top: 20px;
+}
+
+#pinglist-container {
+ display: none;
+}
+
+.comment-tabs {
+ margin-bottom: 20px;
+ font-size: 15px;
+ border-bottom: 2px solid #e5e5e5;
+}
+
+.comment-tabs li {
+ float: left;
+ margin-bottom: -2px;
+}
+
+.comment-tabs li a {
+ display: block;
+ padding: 0 10px 10px;
+ font-weight: 600;
+ color: #aaa;
+ border-bottom: 2px solid #e5e5e5;
+}
+
+.comment-tabs li a:hover {
+ color: #444;
+ border-color: #ccc;
+}
+
+.comment-tabs li span {
+ margin-left: 8px;
+ padding: 0 6px;
+ border-radius: 4px;
+ background-color: #e5e5e5;
+}
+
+.comment-tabs li i {
+ margin-right: 6px;
+}
+
+.comment-tabs li.active a {
+ color: #e8554e;
+ border-bottom-color: #e8554e;
+}
+
+.commentlist, .pinglist {
+ margin-bottom: 20px;
+}
+
+.commentlist li, .pinglist li {
+ padding-left: 60px;
+ font-size: 14px;
+ line-height: 22px;
+ font-weight: 400;
+}
+
+.commentlist .comment-body, .pinglist li {
+ position: relative;
+ padding-bottom: 20px;
+ clear: both;
+ word-break: break-all;
+}
+
+/* mk:评论整体布局 - 使用相对定位实现头像左侧布局 */
+.commentlist .comment-body {
+ position: relative;
+ padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
+ min-height: 48px; /* 确保有足够高度容纳头像 */
+}
+
+/*mk: 评论作者信息 - 用户名和时间在同一行 */
+.commentlist .comment-author {
+ display: inline-block;
+ margin: 0 10px 5px 0;
+ font-size: 13px;
+ position: relative;
+}
+
+.commentlist .comment-meta {
+ display: inline-block;
+ margin: 0 0 8px 0;
+ font-size: 12px;
+ color: #666;
+}
+
+.commentlist .comment-awaiting-moderation {
+ display: block;
+ font-size: 13px;
+ line-height: 22px;
+}
+
+/*mk: 头像样式 - 绝对定位到左侧 */
+.commentlist .comment-author .avatar {
+ position: absolute !important;
+ left: -60px; /* 定位到容器左侧 */
+ top: 0;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+}
+
+/*mk: 评论作者名称样式 */
+.commentlist .comment-author .fn {
+ display: inline;
+ margin: 0;
+ font-weight: 600;
+ color: #2e7bb8;
+ font-size: 13px;
+}
+
+.commentlist .comment-author .fn a {
+ color: #2e7bb8;
+ text-decoration: none;
+}
+
+.commentlist .comment-author .fn a:hover {
+ text-decoration: underline;
+}
+
+/* mk:评论内容样式 */
+.commentlist .comment-body p {
+ margin: 5px 0 10px 0;
+ line-height: 1.5;
+}
+
+.commentlist .fn, .pinglist .ping-link {
+ color: #444;
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 600;
+}
+
+.commentlist .says {
+ display: none;
+}
+
+/* mk:通用头像样式 */
+.commentlist .avatar {
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+}
+
+.commentlist .comment-meta:before, .pinglist .ping-meta:before {
+
+ vertical-align: 4%;
+ margin-right: 3px;
+ font-size: 10px;
+ font-family: FontAwesome;
+ color: #ccc;
+}
+
+.commentlist .comment-meta a, .pinglist .ping-meta {
+ color: #aaa;
+}
+
+.commentlist .reply {
+ font-size: 13px;
+ line-height: 16px;
+}
+
+.commentlist .reply a,
+.commentlist .comment-reply-chain {
+ color: #aaa;
+}
+
+.commentlist .reply a:hover,
+.commentlist .comment-reply-chain:hover {
+ color: #444;
+}
+
+.comment-awaiting-moderation {
+ color: #e8554e;
+ font-style: normal;
+}
+
+/* mk:pings */
+.pinglist li {
+ padding-left: 0;
+}
+
+/*mk:comment text */
+.commentlist .comment-body p {
+ margin-bottom: 8px;
+ color: #777;
+ clear: both;
+}
+
+.commentlist .comment-body strong {
+ font-weight: 600;
+}
+
+.commentlist .comment-body ol li {
+ margin-left: 2em;
+ padding: 0;
+ list-style: decimal;
+}
+
+.commentlist .comment-body ul li {
+ margin-left: 2em;
+ padding: 0;
+ list-style: square;
+}
+
+/*mk: post author & admin comment */
+.commentlist li.bypostauthor > .comment-body:after,
+.commentlist li.comment-author-admin > .comment-body:after {
+ display: block;
+ position: absolute;
+ content: "\f040";
+ width: 12px;
+ line-height: 12px;
+ font-style: normal;
+ font-family: FontAwesome;
+ text-align: center;
+ color: #fff;
+ background-color: #e8554e;
+}
+
+.commentlist li.comment-author-admin > .comment-body:after {
+ content: "\f005"; /* star for admin */
+}
+
+.commentlist li.bypostauthor > .comment-body:after,
+.commentlist li.comment-author-admin > .comment-body:after {
+ padding: 3px;
+ top: 32px;
+ left: -28px;
+ font-size: 12px;
+ border-radius: 100%;
+}
+
+.commentlist li li.bypostauthor > .comment-body:after,
+.commentlist li li.comment-author-admin > .comment-body:after {
+ padding: 2px;
+ top: 22px;
+ left: -26px;
+ font-size: 10px;
+ border-radius: 100%;
+}
+
+/* mk:child comment */
+.commentlist li ul {
+}
+
+.commentlist li li {
+ margin: 0;
+ padding-left: 48px;
+}
+
+/*mk: 嵌套评论整体布局 */
+.commentlist li li .comment-body {
+ padding-left: 60px; /* mk:为48px头像 + 12px间距留出空间 */
+ min-height: 48px; /* mk:确保有足够高度容纳头像 */
+}
+
+/*mk: 嵌套评论作者信息 */
+.commentlist li li .comment-author {
+ display: inline-block;
+ margin: 0 8px 5px 0;
+ font-size: 12px; /*mk: 稍小一点 */
+}
+
+.commentlist li li .comment-meta {
+ display: inline-block;
+ margin: 0 0 8px 0;
+ font-size: 11px; /*mk: 稍小一点 */
+ color: #666;
+}
+
+/* mk:评论容器整体左移 - 使用更高优先级 */
+#comments #commentlist-container.comment-tab {
+ margin-left: -15px !important; /* mk:在小屏幕上向左移动15px */
+ padding-left: 0 !important; /* mk:移除左内边距 */
+ position: relative !important; /*mk: 确保定位正确 */
+}
+
+/* mk:在较大屏幕上进一步左移 */
+@media screen and (min-width: 600px) {
+ #comments #commentlist-container.comment-tab {
+ margin-left: -30px !important; /* mk:在大屏幕上向左移动30px */
+ }
+
+ /*mk: 响应式设计下的评论布局 - 保持48px头像 */
+ .commentlist .comment-body {
+ padding-left: 60px !important; /* mk:为48px头像 + 12px间距留出空间 */
+ min-height: 48px !important;
+ }
+
+ .commentlist .comment-author {
+ display: inline-block !important;
+ margin: 0 8px 5px 0 !important;
+ }
+
+ .commentlist .comment-meta {
+ display: inline-block !important;
+ margin: 0 0 8px 0 !important;
+ }
+
+ /* mk:响应式设计下头像保持48px */
+ .commentlist .comment-author .avatar {
+ left: -60px !important;
+ width: 48px !important;
+ height: 48px !important;
+ }
+
+ /* mk:嵌套评论在响应式设计下也保持48px头像 */
+ .commentlist li li .comment-body {
+ padding-left: 60px !important;
+ min-height: 48px !important;
+ }
+
+ .commentlist li li .comment-author .avatar {
+ left: -60px !important;
+ width: 48px !important;
+ height: 48px !important;
+ }
+}
+
+/* mk:嵌套评论头像 */
+.commentlist li li .comment-author .avatar {
+ position: absolute !important;
+ left: -60px; /* mk:定位到容器左侧 */
+ top: 0;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+}
+
+/* mk:comments : nav
+/* ------------------------------------ */
+.comments-nav {
+ margin-bottom: 20px;
+}
+
+.comments-nav a {
+ font-weight: 600;
+}
+
+.comments-nav .nav-previous {
+ float: left;
+}
+
+.comments-nav .nav-next {
+ float: right;
+}
+
+/* mk: comments : form
+/* ------------------------------------ */
+.logged-in-as,
+.comment-notes,
+.form-allowed-tags {
+ display: none;
+}
+
+#respond {
+ position: relative;
+}
+
+#reply-title {
+ margin-bottom: 20px;
+}
+
+li #reply-title {
+ margin: 0 !important;
+ padding: 0;
+ height: 0;
+ font-size: 0;
+ border-top: 0;
+}
+
+#cancel-comment-reply-link {
+ float: right;
+ bottom: 26px;
+ right: 20px;
+ font-size: 12px;
+ color: #999;
+}
+
+#cancel-comment-reply-link:hover {
+ color: #777;
+}
+
+#commentform {
+ margin-bottom: 20px;
+ padding: 10px 20px 20px;
+ border-radius: 4px;
+ background-color: #e5e5e5;
+}
+
+#commentform p.comment-form-author {
+ float: left;
+ width: 48%;
+}
+
+#commentform p.comment-form-email {
+ float: right;
+ width: 48%;
+}
+
+#commentform p.comment-form-url,
+#commentform p.comment-form-comment {
+ clear: both;
+}
+
+#commentform label {
+ display: block;
+ padding: 6px 0;
+ font-weight: 600;
+}
+
+#commentform input[type="text"],
+#commentform textarea {
+ max-width: 100%;
+ width: 100%;
+}
+
+#commentform textarea {
+ height: 100px;
+}
+
+#commentform p.form-submit {
+ margin-top: 10px;
+}
+
+.logged-in #reply-title {
+ margin-bottom: 20px;
+}
+
+.logged-in #commentform p.comment-form-comment {
+ margin-top: 10px;
+}
+
+.logged-in #commentform p.comment-form-comment label {
+ display: none;
+}
+
+.heading,
+#reply-title {
+ margin-bottom: 1em;
+ font-size: 18px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: #222;
+}
+
+.heading i {
+ margin-right: 6px;
+ font-size: 22px;
+}
+
+.group:before {
+ content: "";
+ display: table;
+}
+
+.group:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.cancel-comment {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+#rocket {
+ position: fixed;
+ right: 50px;
+ bottom: 50px;
+ display: block;
+ visibility: hidden;
+ width: 26px;
+ height: 48px;
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAB8CAYAAAB356CJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAbdSURBVHja5NlbbBRVGAfw5VID+LAK8cEoxqTgmw8kPPhwipTGxJTDUAVBQBMNKtZboiDE2ES8pFEjGhNkkCrin3JbZo4YCqloUOoKJCDIRWyRAgW6R3dobU2bJtj6+eCMTqczs2d3Zh6Mm3xpdvc7++vMnHNmzvlSRJQqJgA8B8AC8EQx7YoBxgD4CAC54i0Ao2KDAIwCsNGDOPF6nNBLAYgTiyNDAKYDGCwA/Q7gtpIhAKMBHC+AOPF5FGiBIuLEXaVCR4uEzKIhAHcViRCAP4OuVRi0pgSIACwvFurw/ohhGJTP56m7u5vy+TwZhuEHHVKGANzmh3R3d48IH2wQwPWq0CIv5ByJN/L5vN9RzVKF3vQ29kOcULlOQZAZ8YjWq0JHI1wjAvClKnTJr+sq9joCcEoV6itxDDmRU4UoYvT8f6GeiFCXKpSLCJ1XhU5GhI6oQs0RoT2qUENESFeFlkeEXlCFZkeEqlWhWyNCtxSE7GdsPSL0AYAxgRCACQB2xzAzEAABYMIIyEYOxIQ4sR/AOC+UiRlxYvM/EID5CSFO1DjQoYShFmfFMJgwdC0FYHzCCAEYck5dZ8LQWQdCwpAe19xWKCocqAzA1YSQiwBGuwfs2yHJpwDcEBJHQtqu9s4MU0KSHy+wBF0c1NsATPabVL/ye6IBML4AVAbgik/bvUGz9zyf5HrFTY9VPm0XBkFlAH7xrN5uVYQmAuh3P0Q6M3fQje81V/LWIne+1gY9oPglTwLQai+Wby8SugnAj/Y2W7nqqnyUz2cagDb7P24DoAXshI2Nsl9XZXdXb/etintjMBswVrJxQ0H3rMG4oYEAaOA/e+rqAqC6uKHyAKg8VsjGDnqQg7Hve9tQrQeqTQpKuybOfgDpRCDParAhkZKBC5pmQ9MShWysvtg2RSOZTKYu0WqLYRhjTdMUQghqbGxMrtpimuYuIQQJIWj79u3JVFsMw3jHQYQQfhuC0asthmFUCiGG3JAQgjZv3hxftaW5uXmMEOJnLyKEoK1bt8ZXbTEMY5kfIoSgHTt2xFdtEUK0BkE7d+6Mp9piGMY9QYgQgkzTjKfaYprmJvcPn/vhOHV8+D511j5EuUWzqXPZEmpd9x59/102WrVFCPGrG7myopZkzUyS2ox/Ijf3bjq/8mkvpl5tMQzjDvfRdKx7l+TcmZR7bAH1nThGf167Rn0njlHn0gcoV1NJrWvXlFZtMQzjaTfU+eQSknMqqP+n0+R+9Z05RXJOBXUsW1xatcUwjAY3lLu/iuScCvJ7SW0GXVlUXVq1xTTN/cOghfcGH5E2w++I1Kot3vFzceP6vy++5xrlli6gXM1MOvOxXlq1RQiR946by6tXkpw7vNfJmko698qL1NzUVFq1RQgx4DdIL2z7lDqfephyD2l05dlH6ELjRj9EvdoSNiMozA7qtQlVSAjx34H6IkJdqlBXROi86oBtjwgdUYUOR4T2qEJmREhXnVTrI0IvqEJLIg7YalWoXAUKqSwXrrZIzsZIzvSfT5woCTr2zdckOftAchZcbZGcTZCc7ZacUfu+vQWhTCYzAjq9vZEkZyQ5E5KzkdUWGzlgJ9GFjetLgtrerXcgkpztl5yN80IZVwJdWvVMQcizqiAAdPHZR90QSc7+rbZIzuZ7vqTcfZXUdvp0KOR9/j78bQvlaiq9EEnOahzokM+X1P7FnlBoy5Ytw69P4yd+CEnOWlKSs9GSs0G/hI41bxQ1WNtffj4IupaSnI0P+JJyD1bT8aNHlbr24ZYWys2rCoKGnFPXGYS1N+1S6nFnPtaDEJKcnXUgBCVdfrHWF9q2bdswqGPZ4jBId6DZIUnUnm0J7Qgnd5lhCEnOKhyoTHJ2NSjx0qurQifTCytqw5CLkrPR7gH7dkhy6HaZ5OzbkLarvTPDlJDkRQWg+UG9TXI22W9S/conWUrOrisAjbVPkbft3qDZe55P8qsqmx6SsxU+bRcGQWWSs19ciX9Izm5WhG6UnPW52vY4M3fQje81V3JR1RbJ2Vr32Cl0h50kOWuVnHVIzm4vErpJcvaj5MySnKlVWyRnw7bHLF1L9WbTWm823dabTZP9V7N0bUQ7yVnp1RZL16p69k0eshHqzaapZ9/kIUvX4q22WLqW7cpMJzfUlZlOlq5l44YGrQ3VwyBrQzVZujYYNzRg6Rr1tkz8G2qZSJaukaVrA7GfOkvX6LemqdSbTdNvTVMdKPZTV2fpGl3dNIt6s2m6ummWA9XFDZXbP0zdn93pIGTpWnncUMrStYMugOz3qSSgWg9UmxSUtnSt30b67feJQClL1xpsqMH5LClomg1NSxpKWbpW736v0v6vAQCo4CbBrd8RBQAAAABJRU5ErkJggg==") no-repeat 50% 0;
+ cursor: pointer;
+ -webkit-transition: all 0s;
+ transition: all 0s;
+}
+
+#rocket:hover {
+ background-position: 50% -62px;
+}
+
+#rocket.show {
+ visibility: visible;
+ opacity: 1;
+}
+
+#rocket.move {
+ background-position: 50% -62px;
+ -webkit-animation: toTop .8s ease-in;
+ animation: toTop .8s ease-in;
+ animation-fill-mode: forwards;
+ -webkit-animation-fill-mode: forwards;
+}
+
+.comment-markdown {
+ float: right;
+ font-size: small;
+}
+
+.breadcrumb {
+ margin-bottom: 20px;
+ list-style: none;
+ border-radius: 4px;
+}
+
+.breadcrumb > li {
+ display: inline-block;
+}
+
+.breadcrumb > li + li:before {
+ color: #ccc;
+ content: "/\00a0";
+}
+
+.breadcrumb > .active {
+ color: #777;
+}
+
+.break_line {
+ height: 1px;
+ border: none;
+ /*border-top: 1px dashed #f5d6d6;*/
+}
+
+/* =============================================================================
+mk:
+ 评论内容溢出修复样式
+ 解决代码块和长文本撑开页面布局的问题
+ ============================================================================= */
+
+/* mk:评论容器基础样式 */
+.comment-body {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* mk:修复评论中的代码块溢出 */
+.comment-content pre,
+.comment-body pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100% !important;
+ overflow-x: auto;
+ padding: 10px;
+ background-color: #f8f8f8;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.4;
+ margin: 10px 0;
+}
+
+/* mk:修复评论中的行内代码 */
+.comment-content code,
+.comment-body code {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ white-space: pre-wrap;
+ max-width: 100%;
+ display: inline-block;
+ vertical-align: top;
+}
+
+/* mk:修复评论中的长链接 */
+.comment-content a,
+.comment-body a {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ word-break: break-all;
+ max-width: 100%;
+}
+
+/* mk:修复评论段落 */
+.comment-content p,
+.comment-body p {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100%;
+ margin: 10px 0;
+}
+
+/* mk:特殊处理代码高亮块 - 关键修复! */
+.comment-content .codehilite,
+.comment-body .codehilite {
+ max-width: 100% !important;
+ overflow-x: auto;
+ margin: 10px 0;
+ background: #f8f8f8 !important;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 10px;
+ font-size: 12px;
+ line-height: 1.4;
+ /* 关键:防止内容撑开容器 */
+ width: 100%;
+ box-sizing: border-box;
+ display: block;
+}
+
+.comment-content .codehilite pre,
+.comment-body .codehilite pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: transparent !important;
+ border: none !important;
+ font-size: inherit;
+ line-height: inherit;
+ /* 确保pre标签不会超出父容器 */
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* mk:修复代码高亮中的span标签 */
+.comment-content .codehilite span,
+.comment-body .codehilite span {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ /* 防止行内元素导致的溢出 */
+ display: inline;
+ max-width: 100%;
+}
+
+/* mk:针对特定的代码高亮类 */
+.comment-content .codehilite .kt,
+.comment-content .codehilite .nf,
+.comment-content .codehilite .n,
+.comment-content .codehilite .p,
+.comment-body .codehilite .kt,
+.comment-body .codehilite .nf,
+.comment-body .codehilite .n,
+.comment-body .codehilite .p {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+}
+
+/* mk:搜索结果高亮样式 */
+.search-result {
+ margin-bottom: 30px;
+ padding: 20px;
+ border: 1px solid #e1e1e1;
+ border-radius: 5px;
+ background: #fff;
+}
+
+.search-result .entry-title {
+ margin: 0 0 10px 0;
+ font-size: 1.5em;
+}
+
+.search-result .entry-title a {
+ color: #2c3e50;
+ text-decoration: none;
+}
+
+.search-result .entry-title a:hover {
+ color: #3498db;
+}
+
+.search-result .entry-meta {
+ color: #7f8c8d;
+ font-size: 0.9em;
+ margin-bottom: 15px;
+}
+
+.search-result .entry-meta span {
+ margin-right: 15px;
+}
+
+.search-excerpt {
+ line-height: 1.6;
+ color: #555;
+}
+
+.search-excerpt p {
+ margin: 10px 0;
+}
+
+/* mk:搜索关键词高亮 */
+.search-excerpt em,
+.search-result .entry-title em {
+ background-color: #fff3cd;
+ color: #856404;
+ font-style: normal;
+ font-weight: bold;
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+
+.more-link {
+ color: #3498db;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.more-link:hover {
+ text-decoration: underline;
+}
+.comment-content .codehilite .w,
+.comment-content .codehilite .o,
+.comment-body .codehilite .kt,
+.comment-body .codehilite .nf,
+.comment-body .codehilite .n,
+.comment-body .codehilite .p,
+.comment-body .codehilite .w,
+.comment-body .codehilite .o {
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+/* mk:修复评论列表项 */
+.commentlist li {
+ max-width: 100%;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* mk:确保评论内容不超出容器 */
+.commentlist .comment-body {
+ max-width: calc(100% - 20px); /* 留出一些边距 */
+ margin-left: 10px;
+ margin-right: 10px;
+ overflow: hidden; /* 防止内容溢出 */
+ word-wrap: break-word;
+}
+
+/* mk:重要:限制评论列表项的最大宽度 */
+.commentlist li[style*="margin-left"] {
+ max-width: calc(100% - 2rem) !important;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* mk:特别处理深层嵌套的评论 */
+.commentlist li[style*="margin-left: 3rem"],
+.commentlist li[style*="margin-left: 6rem"],
+.commentlist li[style*="margin-left: 9rem"] {
+ max-width: calc(100% - 1rem) !important;
+}
+
+/*mk: 移动端优化 */
+@media (max-width: 768px) {
+ .comment-content pre,
+ .comment-body pre {
+ font-size: 11px;
+ padding: 8px;
+ margin: 8px 0;
+ }
+
+ .commentlist .comment-body {
+ max-width: calc(100% - 10px);
+ margin-left: 5px;
+ margin-right: 5px;
+ }
+
+ /* mk:移动端评论缩进调整 */
+ .commentlist li[style*="margin-left"] {
+ margin-left: 1rem !important;
+ max-margin-left: 2rem !important;
+ }
+}
+
+/* mk:防止表格溢出 */
+.comment-content table,
+.comment-body table {
+ max-width: 100%;
+ overflow-x: auto;
+ display: block;
+ white-space: nowrap;
+}
+
+/* mk:修复图片溢出 */
+.comment-content img,
+.comment-body img {
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+/* mk:修复引用块 */
+.comment-content blockquote,
+.comment-body blockquote {
+ max-width: 100%;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ padding: 10px 15px;
+ margin: 10px 0;
+ border-left: 4px solid #ddd;
+ background-color: #f9f9f9;
+}
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2
new file mode 100644
index 0000000..0fb066c
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2
new file mode 100644
index 0000000..bc2aea0
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2
new file mode 100644
index 0000000..fcce594
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2
new file mode 100644
index 0000000..ffc8e9c
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2
new file mode 100644
index 0000000..6375e9c
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2
new file mode 100644
index 0000000..2e849f6
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2
new file mode 100644
index 0000000..5de3fea
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2
new file mode 100644
index 0000000..e5c936b
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2
new file mode 100644
index 0000000..5cf8aff
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2
new file mode 100644
index 0000000..bdc12e8
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2
new file mode 100644
index 0000000..b5d54e7
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2
new file mode 100644
index 0000000..bed5b67
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2
new file mode 100644
index 0000000..9164ccb
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2
new file mode 100644
index 0000000..08bed85
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2
new file mode 100644
index 0000000..307b214
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2
new file mode 100644
index 0000000..0b0b3a4
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2
new file mode 100644
index 0000000..4bce1d0
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2
new file mode 100644
index 0000000..5bd7b8f
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2
new file mode 100644
index 0000000..b969602
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2
new file mode 100644
index 0000000..a804b10
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 differ
diff --git a/doc/DjangoBlog/blog/static/blog/fonts/open-sans.css b/doc/DjangoBlog/blog/static/blog/fonts/open-sans.css
new file mode 100644
index 0000000..c92e0c9
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/fonts/open-sans.css
@@ -0,0 +1,439 @@
+/*
+mk:
+ * 定义 Open Sans 字体的多种变体,包括不同的字体样式(正常/斜体)、字重(300、400、600)和字符集支持。
+ * 每个 @font-face 规则针对特定的语言字符集(如拉丁文、西里尔文、希腊文、越南文、希伯来文等)加载对应的字体文件。
+ * 使用 WOFF2 格式以优化加载性能,并通过 unicode-range 实现按需加载字体子集。
+ * font-display: swap 用于提升页面渲染性能,在字体加载完成前使用备用字体。
+ */
+
+/* mk: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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: cyrillic */
+@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+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/*mk: 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;
+}
+/* mk: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;
+}
+/*mk: 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;
+}
+/* mk: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;
+}
+/* mk: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;
+}
+/* mk: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;
+}
+/* mk: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;
+}
+/* mk: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;
+}
+/* mk: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;
+}
+
diff --git a/doc/DjangoBlog/blog/static/blog/img/avatar.png b/doc/DjangoBlog/blog/static/blog/img/avatar.png
new file mode 100644
index 0000000..320756f
Binary files /dev/null and b/doc/DjangoBlog/blog/static/blog/img/avatar.png differ
diff --git a/doc/DjangoBlog/blog/static/blog/img/icon-sn.svg b/doc/DjangoBlog/blog/static/blog/img/icon-sn.svg
new file mode 100644
index 0000000..2c2da0a
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/img/icon-sn.svg
@@ -0,0 +1 @@
+icon-sn
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/static/blog/js/blog.js b/doc/DjangoBlog/blog/static/blog/js/blog.js
new file mode 100644
index 0000000..0ead584
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/js/blog.js
@@ -0,0 +1,119 @@
+/**
+ * Created by liangliang on 2016/11/20.
+ */
+
+
+/**
+mk:
+ * 执行回复操作,设置父评论ID并将评论表单移动到指定位置
+ * @param {string|number} parentid - 父评论的ID
+ */
+function do_reply(parentid) {
+ console.log(parentid);
+ $("#id_parent_comment_id").val(parentid)
+ $("#commentform").appendTo($("#div-comment-" + parentid));
+ $("#reply-title").hide();
+ $("#cancel_comment").show();
+}
+
+/**
+mk:
+ * 取消回复操作,重置评论表单到初始状态
+ */
+function cancel_reply() {
+ $("#reply-title").show();
+ $("#cancel_comment").hide();
+ $("#id_parent_comment_id").val('')
+ $("#commentform").appendTo($("#respond"));
+}
+
+// mk:初始化页面加载进度条
+NProgress.start();
+NProgress.set(0.4);
+//Increment
+var interval = setInterval(function () {
+ NProgress.inc();
+}, 1000);
+
+// mk:页面加载完成后停止进度条
+$(document).ready(function () {
+ NProgress.done();
+ clearInterval(interval);
+});
+
+
+/**mk: 侧边栏回到顶部 */
+var rocket = $('#rocket');
+
+//mk: 监听窗口滚动事件,控制回到顶部按钮的显示
+$(window).on('scroll', debounce(slideTopSet, 300));
+
+/**
+mk:
+ * 防抖函数,用于限制函数执行频率
+ * @param {Function} func - 需要防抖的函数
+ * @param {number} wait - 延迟执行的时间间隔(毫秒)
+ * @returns {Function} 返回防抖后的函数
+ */
+function debounce(func, wait) {
+ var timeout;
+ return function () {
+ clearTimeout(timeout);
+ timeout = setTimeout(func, wait);
+ };
+}
+
+/**
+mk:
+ * 根据滚动位置控制回到顶部按钮的显示状态
+ */
+function slideTopSet() {
+ var top = $(document).scrollTop();
+
+ if (top > 200) {
+ rocket.addClass('show');
+ } else {
+ rocket.removeClass('show');
+ }
+}
+
+// mk:点击回到顶部按钮时执行动画滚动到顶部
+$(document).on('click', '#rocket', function (event) {
+ rocket.addClass('move');
+ $('body, html').animate({
+ scrollTop: 0
+ }, 800);
+});
+
+// mk:动画结束后的清理处理
+$(document).on('animationEnd', function () {
+ setTimeout(function () {
+ rocket.removeClass('move');
+ }, 400);
+
+});
+$(document).on('webkitAnimationEnd', function () {
+ setTimeout(function () {
+ rocket.removeClass('move');
+ }, 400);
+});
+
+
+// mk:页面加载完成后为所有回复链接绑定点击事件
+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);
+ };
+ }
+};
+
+//mk: $(document).ready(function () {
+// var form = $('#i18n-form');
+// var selector = $('.i18n-select');
+// selector.on('change', function () {
+// form.submit();
+// });
+// });
diff --git a/doc/DjangoBlog/blog/static/blog/js/html5.js b/doc/DjangoBlog/blog/static/blog/js/html5.js
new file mode 100644
index 0000000..2a7395f
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/js/html5.js
@@ -0,0 +1,60 @@
+/*
+ HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
+*/
+
+/**
+mk:
+ * 获取HTML5元素列表
+ * @returns {Array|string} 返回元素名称数组或字符串
+ */
+(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}
+
+/**
+mk:
+ * 获取与文档关联的缓存对象
+ * @param {Object} a - 文档对象
+ * @returns {Object} 返回对应的缓存对象
+ */
+function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}
+
+/**
+mk:
+ * 创建HTML元素
+ * @param {string} a - 元素标签名
+ * @param {Object} b - 文档对象(可选)
+ * @param {Object} c - 缓存对象(可选)
+ * @returns {Object} 返回创建的元素节点
+ */
+function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}
+
+/**
+mk:
+ * 重写文档的createElement和createDocumentFragment方法以支持HTML5元素
+ * @param {Object} a - 文档对象
+ * @param {Object} b - 缓存配置对象
+ */
+function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
+a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}
+
+/**
+mk:
+ * 初始化并应用HTML5 Shiv到指定文档
+ * @param {Object} a - 文档对象(可选,默认为当前文档)
+ * @returns {Object} 返回处理后的文档对象
+ */
+function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x";
+c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}
+
+// mk:初始化变量和配置
+var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;
+
+// mk:检测浏览器是否原生支持未知元素和相关API
+(function(){try{var a=f.createElement("a");a.innerHTML=" ";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
+"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();
+
+// mk:定义HTML5 Shiv的核心配置和公共接口
+var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
+if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d {
+ console.log('MathJax渲染完成');
+ // mk:触发自定义事件,通知其他脚本MathJax已就绪
+ document.dispatchEvent(new CustomEvent('mathjaxReady'));
+ }).catch(error => {
+ console.error('MathJax渲染失败:', error);
+ });
+ }
+ },
+ // mk:输出配置
+ chtml: {
+ scale: 1,
+ minScale: 0.5,
+ matchFontHeight: false,
+ displayAlign: 'center',
+ displayIndent: '0'
+ }
+ };
+ }
+
+ /**
+ mk:
+ * 加载MathJax库
+ */
+ function loadMathJax() {
+ console.log('检测到数学公式,开始加载MathJax...');
+
+ const script = document.createElement('script');
+ script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
+ script.async = true;
+ script.defer = true;
+
+ script.onload = function() {
+ console.log('MathJax库加载成功');
+ };
+
+ script.onerror = function() {
+ console.error('MathJax库加载失败,尝试备用CDN...');
+ // 备用CDN
+ const fallbackScript = document.createElement('script');
+ fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
+ fallbackScript.onload = function() {
+ const mathJaxScript = document.createElement('script');
+ mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
+ mathJaxScript.async = true;
+ document.head.appendChild(mathJaxScript);
+ };
+ document.head.appendChild(fallbackScript);
+ };
+
+ document.head.appendChild(script);
+ }
+
+ /**
+ mk:
+ * 初始化函数
+ */
+ function init() {
+ // mk:等待DOM完全加载
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ return;
+ }
+
+ // mk:检测是否需要加载MathJax
+ if (hasMathFormulas()) {
+ // mk:先配置,再加载
+ configureMathJax();
+ loadMathJax();
+ } else {
+ console.log('未检测到数学公式,跳过MathJax加载');
+ }
+ }
+
+ // mk:提供重新渲染的全局方法,供动态内容使用
+ window.rerenderMathJax = function(element) {
+ if (window.MathJax && window.MathJax.typesetPromise) {
+ const target = element || document.body;
+ return window.MathJax.typesetPromise([target]);
+ }
+ return Promise.resolve();
+ };
+
+ // mk:启动初始化
+ init();
+})();
diff --git a/doc/DjangoBlog/blog/static/blog/js/navigation.js b/doc/DjangoBlog/blog/static/blog/js/navigation.js
new file mode 100644
index 0000000..db52044
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/js/navigation.js
@@ -0,0 +1,71 @@
+/**
+mk:
+ * Handles toggling the navigation menu for small screens and
+ * accessibility for submenu items.
+ *
+ * This self-invoking function manages the mobile navigation toggle behavior.
+ * It adds click event to the menu button to show/hide the navigation menu
+ * by toggling CSS classes.
+ */
+( function() {
+ var nav = document.getElementById( 'site-navigation' ), button, menu;
+ if ( ! nav ) {
+ return;
+ }
+
+ button = nav.getElementsByTagName( 'button' )[0];
+ menu = nav.getElementsByTagName( 'ul' )[0];
+ if ( ! button ) {
+ return;
+ }
+
+ // mk:Hide button if menu is missing or empty.
+ if ( ! menu || ! menu.childNodes.length ) {
+ button.style.display = 'none';
+ return;
+ }
+
+ button.onclick = function() {
+ if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
+ menu.className = 'nav-menu';
+ }
+
+ if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
+ button.className = button.className.replace( ' toggled-on', '' );
+ menu.className = menu.className.replace( ' toggled-on', '' );
+ } else {
+ button.className += ' toggled-on';
+ menu.className += ' toggled-on';
+ }
+ };
+} )();
+
+/**
+mk:
+ * Enhances focus handling for submenu items to improve accessibility
+ * and handles touch events for mobile devices.
+ *
+ * @param {Object} $ - jQuery object
+ *
+ * This self-invoking function improves the accessibility of navigation menus
+ * by adding focus and blur event handlers to menu items. It also handles
+ * touch events for mobile devices to properly display submenus.
+ */
+( function( $ ) {
+ $( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
+ $( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
+ } );
+
+ // mk:Handle touch events for mobile devices to properly display submenus
+ if ( 'ontouchstart' in window ) {
+ $('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
+ var el = $( this ).parent( 'li' );
+
+ if ( ! el.hasClass( 'focus' ) ) {
+ e.preventDefault();
+ el.toggleClass( 'focus' );
+ el.siblings( '.focus').removeClass( 'focus' );
+ }
+ } );
+ }
+} )( jQuery );
diff --git a/doc/DjangoBlog/blog/static/blog/js/nprogress.js b/doc/DjangoBlog/blog/static/blog/js/nprogress.js
new file mode 100644
index 0000000..cfdbd6c
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/blog/js/nprogress.js
@@ -0,0 +1,500 @@
+/* mk:NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
+ * @license MIT */
+
+;(function(root, factory) {
+
+ if (typeof define === 'function' && define.amd) {
+ define(factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ } else {
+ root.NProgress = factory();
+ }
+
+})(this, function() {
+ var NProgress = {};
+
+ NProgress.version = '0.2.0';
+
+ var Settings = NProgress.settings = {
+ minimum: 0.08,
+ easing: 'linear',
+ positionUsing: '',
+ speed: 200,
+ trickle: true,
+ trickleSpeed: 200,
+ showSpinner: true,
+ barSelector: '[role="bar"]',
+ spinnerSelector: '[role="spinner"]',
+ parent: 'body',
+ template: ''
+ };
+
+ /**
+ mk:
+ * Updates configuration.
+ *
+ * NProgress.configure({
+ * minimum: 0.1
+ * });
+ */
+ NProgress.configure = function(options) {
+ var key, value;
+ for (key in options) {
+ value = options[key];
+ if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
+ }
+
+ return this;
+ };
+
+ /**
+ mk:
+ * Last number.
+ */
+
+ NProgress.status = null;
+
+ /**
+ mk:
+ * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
+ *
+ * NProgress.set(0.4);
+ * NProgress.set(1.0);
+ */
+
+ NProgress.set = function(n) {
+ var started = NProgress.isStarted();
+
+ n = clamp(n, Settings.minimum, 1);
+ NProgress.status = (n === 1 ? null : n);
+
+ var progress = NProgress.render(!started),
+ bar = progress.querySelector(Settings.barSelector),
+ speed = Settings.speed,
+ ease = Settings.easing;
+
+ progress.offsetWidth; /*mk:Repaint */
+
+ queue(function(next) {
+ // mk:Set positionUsing if it hasn't already been set
+ if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
+
+ // mk:Add transition
+ css(bar, barPositionCSS(n, speed, ease));
+
+ if (n === 1) {
+ // Fade out
+ css(progress, {
+ transition: 'none',
+ opacity: 1
+ });
+ progress.offsetWidth; /* Repaint */
+
+ setTimeout(function() {
+ css(progress, {
+ transition: 'all ' + speed + 'ms linear',
+ opacity: 0
+ });
+ setTimeout(function() {
+ NProgress.remove();
+ next();
+ }, speed);
+ }, speed);
+ } else {
+ setTimeout(next, speed);
+ }
+ });
+
+ return this;
+ };
+
+ NProgress.isStarted = function() {
+ return typeof NProgress.status === 'number';
+ };
+
+ /**
+ mk:
+ * Shows the progress bar.
+ * This is the same as setting the status to 0%, except that it doesn't go backwards.
+ *
+ * NProgress.start();
+ *
+ */
+ NProgress.start = function() {
+ if (!NProgress.status) NProgress.set(0);
+
+ var work = function() {
+ setTimeout(function() {
+ if (!NProgress.status) return;
+ NProgress.trickle();
+ work();
+ }, Settings.trickleSpeed);
+ };
+
+ if (Settings.trickle) work();
+
+ return this;
+ };
+
+ /**
+ mk:
+ * Hides the progress bar.
+ * This is the *sort of* the same as setting the status to 100%, with the
+ * difference being `done()` makes some placebo effect of some realistic motion.
+ *
+ * NProgress.done();
+ *
+ * If `true` is passed, it will show the progress bar even if its hidden.
+ *
+ * NProgress.done(true);
+ */
+
+ NProgress.done = function(force) {
+ if (!force && !NProgress.status) return this;
+
+ return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
+ };
+
+ /**
+ mk:
+ * Increments by a random amount.
+ */
+
+ NProgress.inc = function(amount) {
+ var n = NProgress.status;
+
+ if (!n) {
+ return NProgress.start();
+ } else if(n > 1) {
+
+ } else {
+ if (typeof amount !== 'number') {
+ if (n >= 0 && n < 0.2) { amount = 0.1; }
+ else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
+ else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
+ else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
+ else { amount = 0; }
+ }
+
+ n = clamp(n + amount, 0, 0.994);
+ return NProgress.set(n);
+ }
+ };
+
+ NProgress.trickle = function() {
+ return NProgress.inc();
+ };
+
+ /**
+ mk:
+ * Waits for all supplied jQuery promises and
+ * increases the progress as the promises resolve.
+ *
+ * @param $promise jQUery Promise
+ */
+ (function() {
+ var initial = 0, current = 0;
+
+ NProgress.promise = function($promise) {
+ if (!$promise || $promise.state() === "resolved") {
+ return this;
+ }
+
+ if (current === 0) {
+ NProgress.start();
+ }
+
+ initial++;
+ current++;
+
+ $promise.always(function() {
+ current--;
+ if (current === 0) {
+ initial = 0;
+ NProgress.done();
+ } else {
+ NProgress.set((initial - current) / initial);
+ }
+ });
+
+ return this;
+ };
+
+ })();
+
+ /**
+ mk:
+ * (Internal) renders the progress bar markup based on the `template`
+ * setting.
+ */
+
+ NProgress.render = function(fromStart) {
+ if (NProgress.isRendered()) return document.getElementById('nprogress');
+
+ addClass(document.documentElement, 'nprogress-busy');
+
+ var progress = document.createElement('div');
+ progress.id = 'nprogress';
+ progress.innerHTML = Settings.template;
+
+ var bar = progress.querySelector(Settings.barSelector),
+ perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
+ parent = document.querySelector(Settings.parent),
+ spinner;
+
+ css(bar, {
+ transition: 'all 0 linear',
+ transform: 'translate3d(' + perc + '%,0,0)'
+ });
+
+ if (!Settings.showSpinner) {
+ spinner = progress.querySelector(Settings.spinnerSelector);
+ spinner && removeElement(spinner);
+ }
+
+ if (parent != document.body) {
+ addClass(parent, 'nprogress-custom-parent');
+ }
+
+ parent.appendChild(progress);
+ return progress;
+ };
+
+ /**
+ mk:
+ * Removes the element. Opposite of render().
+ */
+
+ NProgress.remove = function() {
+ removeClass(document.documentElement, 'nprogress-busy');
+ removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
+ var progress = document.getElementById('nprogress');
+ progress && removeElement(progress);
+ };
+
+ /**
+ mk:
+ * Checks if the progress bar is rendered.
+ */
+
+ NProgress.isRendered = function() {
+ return !!document.getElementById('nprogress');
+ };
+
+ /**
+ mk:
+ * Determine which positioning CSS rule to use.
+ */
+
+ NProgress.getPositioningCSS = function() {
+ // mk:Sniff on document.body.style
+ var bodyStyle = document.body.style;
+
+ // mk:Sniff prefixes
+ var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
+ ('MozTransform' in bodyStyle) ? 'Moz' :
+ ('msTransform' in bodyStyle) ? 'ms' :
+ ('OTransform' in bodyStyle) ? 'O' : '';
+
+ if (vendorPrefix + 'Perspective' in bodyStyle) {
+ //mk: Modern browsers with 3D support, e.g. Webkit, IE10
+ return 'translate3d';
+ } else if (vendorPrefix + 'Transform' in bodyStyle) {
+ // mk:Browsers without 3D support, e.g. IE9
+ return 'translate';
+ } else {
+ // mk:Browsers without translate() support, e.g. IE7-8
+ return 'margin';
+ }
+ };
+
+ /**
+ mk:
+ * Helpers
+ */
+
+ function clamp(n, min, max) {
+ if (n < min) return min;
+ if (n > max) return max;
+ return n;
+ }
+
+ /**
+ mk:
+ * (Internal) converts a percentage (`0..1`) to a bar translateX
+ * percentage (`-100%..0%`).
+ */
+
+ function toBarPerc(n) {
+ return (-1 + n) * 100;
+ }
+
+
+ /**
+ mk:
+ * (Internal) returns the correct CSS for changing the bar's
+ * position given an n percentage, and speed and ease from Settings
+ */
+
+ function barPositionCSS(n, speed, ease) {
+ var barCSS;
+
+ if (Settings.positionUsing === 'translate3d') {
+ barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
+ } else if (Settings.positionUsing === 'translate') {
+ barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
+ } else {
+ barCSS = { 'margin-left': toBarPerc(n)+'%' };
+ }
+
+ barCSS.transition = 'all '+speed+'ms '+ease;
+
+ return barCSS;
+ }
+
+ /**
+ mk:
+ * (Internal) Queues a function to be executed.
+ */
+
+ var queue = (function() {
+ var pending = [];
+
+ function next() {
+ var fn = pending.shift();
+ if (fn) {
+ fn(next);
+ }
+ }
+
+ return function(fn) {
+ pending.push(fn);
+ if (pending.length == 1) next();
+ };
+ })();
+
+ /**
+ mk:
+ * (Internal) Applies css properties to an element, similar to the jQuery
+ * css method.
+ *
+ * While this helper does assist with vendor prefixed property names, it
+ * does not perform any manipulation of values prior to setting styles.
+ */
+
+ var css = (function() {
+ var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
+ cssProps = {};
+
+ function camelCase(string) {
+ return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
+ return letter.toUpperCase();
+ });
+ }
+
+ function getVendorProp(name) {
+ var style = document.body.style;
+ if (name in style) return name;
+
+ var i = cssPrefixes.length,
+ capName = name.charAt(0).toUpperCase() + name.slice(1),
+ vendorName;
+ while (i--) {
+ vendorName = cssPrefixes[i] + capName;
+ if (vendorName in style) return vendorName;
+ }
+
+ return name;
+ }
+
+ function getStyleProp(name) {
+ name = camelCase(name);
+ return cssProps[name] || (cssProps[name] = getVendorProp(name));
+ }
+
+ function applyCss(element, prop, value) {
+ prop = getStyleProp(prop);
+ element.style[prop] = value;
+ }
+
+ return function(element, properties) {
+ var args = arguments,
+ prop,
+ value;
+
+ if (args.length == 2) {
+ for (prop in properties) {
+ value = properties[prop];
+ if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
+ }
+ } else {
+ applyCss(element, args[1], args[2]);
+ }
+ }
+ })();
+
+ /**
+ mk:
+ * (Internal) Determines if an element or space separated list of class names contains a class name.
+ */
+
+ function hasClass(element, name) {
+ var list = typeof element == 'string' ? element : classList(element);
+ return list.indexOf(' ' + name + ' ') >= 0;
+ }
+
+ /**mk:
+ * (Internal) Adds a class to an element.
+ */
+
+ function addClass(element, name) {
+ var oldList = classList(element),
+ newList = oldList + name;
+
+ if (hasClass(oldList, name)) return;
+
+ // Trim the opening space.
+ element.className = newList.substring(1);
+ }
+
+ /**
+ mk:
+ * (Internal) Removes a class from an element.
+ */
+
+ function removeClass(element, name) {
+ var oldList = classList(element),
+ newList;
+
+ if (!hasClass(element, name)) return;
+
+ // mk:Replace the class name.
+ newList = oldList.replace(' ' + name + ' ', ' ');
+
+ // mk:Trim the opening and closing spaces.
+ element.className = newList.substring(1, newList.length - 1);
+ }
+
+ /**
+ mk:
+ * (Internal) Gets a space separated list of the class names on the element.
+ * The list is wrapped with a single space on each end to facilitate finding
+ * matches within the list.
+ */
+
+ function classList(element) {
+ return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' ');
+ }
+
+ /**
+ mk:
+ * (Internal) Removes an element from the DOM.
+ */
+
+ function removeElement(element) {
+ element && element.parentNode && element.parentNode.removeChild(element);
+ }
+
+ return NProgress;
+});
diff --git a/doc/DjangoBlog/blog/static/pygments/default.css b/doc/DjangoBlog/blog/static/pygments/default.css
new file mode 100644
index 0000000..d5f69f6
--- /dev/null
+++ b/doc/DjangoBlog/blog/static/pygments/default.css
@@ -0,0 +1,293 @@
+.codehilite .hll {
+ background-color: #ffffcc
+}
+
+.codehilite {
+ background: #ffffff;
+}
+
+.codehilite .c {
+ color: #177500
+}
+
+/*mk: Comment */
+.codehilite .err {
+ color: #000000
+}
+
+/*mk: Error */
+.codehilite .k {
+ color: #A90D91
+}
+
+/*mk: Keyword */
+.codehilite .l {
+ color: #1C01CE
+}
+
+/*mk: Literal */
+.codehilite .n {
+ color: #000000
+}
+
+/*mk: Name */
+.codehilite .o {
+ color: #000000
+}
+
+/*mk: Operator */
+.codehilite .ch {
+ color: #177500
+}
+
+/*mk: Comment.Hashbang */
+.codehilite .cm {
+ color: #177500
+}
+
+/*mk: Comment.Multiline */
+.codehilite .cp {
+ color: #633820
+}
+
+/*mk: Comment.Preproc */
+.codehilite .cpf {
+ color: #177500
+}
+
+/*mk: Comment.PreprocFile */
+.codehilite .c1 {
+ color: #177500
+}
+
+/*mk: Comment.Single */
+.codehilite .cs {
+ color: #177500
+}
+
+/*mk: Comment.Special */
+.codehilite .kc {
+ color: #A90D91
+}
+
+/*mk: Keyword.Constant */
+.codehilite .kd {
+ color: #A90D91
+}
+
+/*mk: Keyword.Declaration */
+.codehilite .kn {
+ color: #A90D91
+}
+
+/*mk: Keyword.Namespace */
+.codehilite .kp {
+ color: #A90D91
+}
+
+/*mk: Keyword.Pseudo */
+.codehilite .kr {
+ color: #A90D91
+}
+
+/*mk: Keyword.Reserved */
+.codehilite .kt {
+ color: #A90D91
+}
+
+/*mk: Keyword.Type */
+.codehilite .ld {
+ color: #1C01CE
+}
+
+/*mk: Literal.Date */
+.codehilite .m {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number */
+.codehilite .s {
+ color: #C41A16
+}
+
+/*mk: Literal.String */
+.codehilite .na {
+ color: #836C28
+}
+
+/*mk: Name.Attribute */
+.codehilite .nb {
+ color: #A90D91
+}
+
+/*mk: Name.Builtin */
+.codehilite .nc {
+ color: #3F6E75
+}
+
+/*mk: Name.Class */
+.codehilite .no {
+ color: #000000
+}
+
+/*mk: Name.Constant */
+.codehilite .nd {
+ color: #000000
+}
+
+/*mk: Name.Decorator */
+.codehilite .ni {
+ color: #000000
+}
+
+/*mk: Name.Entity */
+.codehilite .ne {
+ color: #000000
+}
+
+/*mk: Name.Exception */
+.codehilite .nf {
+ color: #000000
+}
+
+/*mk: Name.Function */
+.codehilite .nl {
+ color: #000000
+}
+
+/*mk: Name.Label */
+.codehilite .nn {
+ color: #000000
+}
+
+/*mk: Name.Namespace */
+.codehilite .nx {
+ color: #000000
+}
+
+/*mk: Name.Other */
+.codehilite .py {
+ color: #000000
+}
+
+/*mk: Name.Property */
+.codehilite .nt {
+ color: #000000
+}
+
+/*mk: Name.Tag */
+.codehilite .nv {
+ color: #000000
+}
+
+/*mk: Name.Variable */
+.codehilite .ow {
+ color: #000000
+}
+
+/*mk: Operator.Word */
+.codehilite .mb {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Bin */
+.codehilite .mf {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Float */
+.codehilite .mh {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Hex */
+.codehilite .mi {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Integer */
+.codehilite .mo {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Oct */
+.codehilite .sb {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Backtick */
+.codehilite .sc {
+ color: #2300CE
+}
+
+/*mk: Literal.String.Char */
+.codehilite .sd {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Doc */
+.codehilite .s2 {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Double */
+.codehilite .se {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Escape */
+.codehilite .sh {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Heredoc */
+.codehilite .si {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Interpol */
+.codehilite .sx {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Other */
+.codehilite .sr {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Regex */
+.codehilite .s1 {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Single */
+.codehilite .ss {
+ color: #C41A16
+}
+
+/*mk: Literal.String.Symbol */
+.codehilite .bp {
+ color: #5B269A
+}
+
+/*mk: Name.Builtin.Pseudo */
+.codehilite .vc {
+ color: #000000
+}
+
+/*mk: Name.Variable.Class */
+.codehilite .vg {
+ color: #000000
+}
+
+/*mk: Name.Variable.Global */
+.codehilite .vi {
+ color: #000000
+}
+
+/*mk: Name.Variable.Instance */
+.codehilite .il {
+ color: #1C01CE
+}
+
+/*mk: Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/doc/DjangoBlog/blog/templatetags/__init__.py b/doc/DjangoBlog/blog/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/blog/templatetags/blog_tags.py b/doc/DjangoBlog/blog/templatetags/blog_tags.py
new file mode 100644
index 0000000..757d2d3
--- /dev/null
+++ b/doc/DjangoBlog/blog/templatetags/blog_tags.py
@@ -0,0 +1,759 @@
+import hashlib
+import logging
+import random
+import urllib
+
+from django import template
+from django.conf import settings
+from django.db.models import Q
+from django.shortcuts import get_object_or_404
+from django.template.defaultfilters import stringfilter
+from django.templatetags.static import static
+from django.urls import reverse
+from django.utils.safestring import mark_safe
+
+from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
+from comments.models import Comment
+from djangoblog.utils import CommonMarkdown, sanitize_html
+from djangoblog.utils import cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+from djangoblog.plugin_manage import hooks
+
+logger = logging.getLogger(__name__)
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def head_meta(context):
+ """
+ mk:
+ 渲染页面头部元数据,通过插件系统扩展
+
+ Args:
+ context: 模板上下文
+
+ Returns:
+ 经过插件处理后的HTML字符串
+ """
+ return mark_safe(hooks.apply_filters('head_meta', '', context))
+
+
+@register.simple_tag
+def timeformat(data):
+ """
+ mk:
+ 格式化时间显示
+
+ Args:
+ data: 时间对象
+
+ Returns:
+ 格式化后的时间字符串
+ """
+ try:
+ return data.strftime(settings.TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.simple_tag
+def datetimeformat(data):
+ """
+ mk:
+ 格式化日期时间显示
+
+ Args:
+ data: 日期时间对象
+
+ Returns:
+ 格式化后的日期时间字符串
+ """
+ try:
+ return data.strftime(settings.DATE_TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.filter()
+@stringfilter
+def custom_markdown(content):
+ """
+ mk:
+ 通用markdown过滤器,应用文章内容插件
+ 主要用于文章内容处理
+
+ Args:
+ content: 原始markdown文本内容
+
+ Returns:
+ 经过markdown转换和插件优化后的安全HTML内容
+ """
+ html_content = CommonMarkdown.get_markdown(content)
+
+ # mk:然后应用插件过滤器优化HTML
+ from djangoblog.plugin_manage import hooks
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+ optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
+
+ return mark_safe(optimized_html)
+
+
+@register.filter()
+@stringfilter
+def sidebar_markdown(content):
+ """
+ mk:
+ 侧边栏专用markdown转换器
+
+ Args:
+ content: 原始markdown文本内容
+
+ Returns:
+ 转换后的安全HTML内容
+ """
+ html_content = CommonMarkdown.get_markdown(content)
+ return mark_safe(html_content)
+
+
+@register.simple_tag(takes_context=True)
+def render_article_content(context, article, is_summary=False):
+ """
+ mk:
+ 渲染文章内容,包含完整的上下文信息供插件使用
+
+ Args:
+ context: 模板上下文
+ article: 文章对象
+ is_summary: 是否为摘要模式(首页使用)
+
+ Returns:
+ 经过处理的安全HTML内容
+ """
+ if not article or not hasattr(article, 'body'):
+ return ''
+
+ # mk:先转换Markdown为HTML
+ html_content = CommonMarkdown.get_markdown(article.body)
+
+ # mk:如果是摘要模式,先截断内容再应用插件
+ if is_summary:
+ # mk:截断HTML内容到合适的长度(约300字符)
+ from django.utils.html import strip_tags
+ from django.template.defaultfilters import truncatechars
+
+ # mk:先去除HTML标签,截断纯文本,然后重新转换为HTML
+ plain_text = strip_tags(html_content)
+ truncated_text = truncatechars(plain_text, 300)
+
+ # mk:重新转换截断后的文本为HTML(简化版,避免复杂的插件处理)
+ html_content = CommonMarkdown.get_markdown(truncated_text)
+
+ # mk:然后应用插件过滤器,传递完整的上下文
+ from djangoblog.plugin_manage import hooks
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+ # mk:获取request对象
+ request = context.get('request')
+
+ # mk:应用所有文章内容相关的插件
+ # mk:注意:摘要模式下某些插件(如版权声明)可能不适用
+ optimized_html = hooks.apply_filters(
+ ARTICLE_CONTENT_HOOK_NAME,
+ html_content,
+ article=article,
+ request=request,
+ context=context,
+ is_summary=is_summary # mk:传递摘要标志,插件可以据此调整行为
+ )
+
+ return mark_safe(optimized_html)
+
+
+@register.simple_tag
+def get_markdown_toc(content):
+ """
+ mk:
+ 获取markdown内容的目录结构
+
+ Args:
+ content: markdown文本内容
+
+ Returns:
+ 目录HTML结构
+ """
+ from djangoblog.utils import CommonMarkdown
+ body, toc = CommonMarkdown.get_markdown_with_toc(content)
+ return mark_safe(toc)
+
+
+@register.filter()
+@stringfilter
+def comment_markdown(content):
+ """
+ mk:
+ 评论内容markdown转换器,带HTML净化功能
+
+ Args:
+ content: 原始markdown文本内容
+
+ Returns:
+ 转换并净化后的安全HTML内容
+ """
+ content = CommonMarkdown.get_markdown(content)
+ return mark_safe(sanitize_html(content))
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncatechars_content(content):
+ """
+ mk:
+ 获得文章内容的摘要
+
+ Args:
+ content: 原始文章内容
+
+ Returns:
+ 截断后的文章摘要
+ """
+ from django.template.defaultfilters import truncatechars_html
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ return truncatechars_html(content, blogsetting.article_sub_length)
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncate(content):
+ """
+ mk:
+ 截取纯文本内容前150个字符
+
+ Args:
+ content: 原始文本内容
+
+ Returns:
+ 截断后的文本内容
+ """
+ from django.utils.html import strip_tags
+
+ return strip_tags(content)[:150]
+
+
+@register.inclusion_tag('blog/tags/breadcrumb.html')
+def load_breadcrumb(article):
+ """
+ mk:
+ 获得文章面包屑导航
+
+ Args:
+ article: 文章对象
+
+ Returns:
+ 面包屑导航所需的数据字典
+ """
+ names = article.get_category_tree()
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ site = get_current_site().domain
+ names.append((blogsetting.site_name, '/'))
+ names = names[::-1]
+
+ return {
+ 'names': names,
+ 'title': article.title,
+ 'count': len(names) + 1
+ }
+
+
+@register.inclusion_tag('blog/tags/article_tag_list.html')
+def load_articletags(article):
+ """
+ mk:
+ 加载文章标签列表
+
+ Args:
+ article: 文章对象
+
+ Returns:
+ 文章标签列表所需的数据字典
+ """
+ tags = article.tags.all()
+ tags_list = []
+ for tag in tags:
+ url = tag.get_absolute_url()
+ count = tag.get_article_count()
+ tags_list.append((
+ url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
+ ))
+ return {
+ 'article_tags_list': tags_list
+ }
+
+
+@register.inclusion_tag('blog/tags/sidebar.html')
+def load_sidebar(user, linktype):
+ """
+ mk:
+ 加载侧边栏内容,包括缓存机制
+
+ Args:
+ user: 当前用户对象
+ linktype: 链接显示类型
+
+ Returns:
+ 侧边栏所需的所有数据字典
+ """
+ value = cache.get("sidebar" + linktype)
+ if value:
+ value['user'] = user
+ return value
+ else:
+ logger.info('load sidebar')
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ recent_articles = Article.objects.filter(
+ status='p')[:blogsetting.sidebar_article_count]
+ sidebar_categorys = Category.objects.all()
+ extra_sidebars = SideBar.objects.filter(
+ is_enable=True).order_by('sequence')
+ most_read_articles = Article.objects.filter(status='p').order_by(
+ '-views')[:blogsetting.sidebar_article_count]
+ dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
+ links = Links.objects.filter(is_enable=True).filter(
+ Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
+ commment_list = Comment.objects.filter(is_enable=True).order_by(
+ '-id')[:blogsetting.sidebar_comment_count]
+ # mk:标签云 计算字体大小
+ # mk:根据总数计算出平均值 大小为 (数目/平均值)*步长
+ increment = 5
+ tags = Tag.objects.all()
+ sidebar_tags = None
+ if tags and len(tags) > 0:
+ s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
+ count = sum([t[1] for t in s])
+ dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
+ import random
+ sidebar_tags = list(
+ map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
+ random.shuffle(sidebar_tags)
+
+ value = {
+ 'recent_articles': recent_articles,
+ 'sidebar_categorys': sidebar_categorys,
+ 'most_read_articles': most_read_articles,
+ 'article_dates': dates,
+ 'sidebar_comments': commment_list,
+ 'sidabar_links': links,
+ 'show_google_adsense': blogsetting.show_google_adsense,
+ 'google_adsense_codes': blogsetting.google_adsense_codes,
+ 'open_site_comment': blogsetting.open_site_comment,
+ 'show_gongan_code': blogsetting.show_gongan_code,
+ 'sidebar_tags': sidebar_tags,
+ 'extra_sidebars': extra_sidebars
+ }
+ cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
+ logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
+ value['user'] = user
+ return value
+
+
+@register.inclusion_tag('blog/tags/article_meta_info.html')
+def load_article_metas(article, user):
+ """
+ mk:
+ 获得文章meta信息
+
+ Args:
+ article: 文章对象
+ user: 当前用户对象
+
+ Returns:
+ 文章meta信息所需的数据字典
+ """
+ return {
+ 'article': article,
+ 'user': user
+ }
+
+
+@register.inclusion_tag('blog/tags/article_pagination.html')
+def load_pagination_info(page_obj, page_type, tag_name):
+ """
+ mk:
+ 加载分页信息
+
+ Args:
+ page_obj: 分页对象
+ page_type: 页面类型标识
+ tag_name: 标签名或分类名等
+
+ Returns:
+ 分页信息所需的数据字典
+ """
+ previous_url = ''
+ next_url = ''
+ if page_type == '':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse('blog:index_page', kwargs={'page': next_number})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:index_page', kwargs={
+ 'page': previous_number})
+ if page_type == '分类标签归档':
+ tag = get_object_or_404(Tag, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'tag_name': tag.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'tag_name': tag.slug})
+ if page_type == '作者文章归档':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'author_name': tag_name})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'author_name': tag_name})
+
+ if page_type == '分类目录归档':
+ category = get_object_or_404(Category, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'category_name': category.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'category_name': category.slug})
+
+ return {
+ 'previous_url': previous_url,
+ 'next_url': next_url,
+ 'page_obj': page_obj
+ }
+
+
+@register.inclusion_tag('blog/tags/article_info.html')
+def load_article_detail(article, isindex, user):
+ """
+ mk:
+ 加载文章详情
+
+ Args:
+ article: 文章对象
+ isindex: 是否为列表页(决定是否只显示摘要)
+ user: 当前用户对象
+
+ Returns:
+ 文章详情所需的数据字典
+ """
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+
+ return {
+ 'article': article,
+ 'isindex': isindex,
+ 'user': user,
+ 'open_site_comment': blogsetting.open_site_comment,
+ }
+
+
+# mk:返回用户头像URL
+# mk:模板使用方法: {{ email|gravatar_url:150 }}
+@register.filter
+def gravatar_url(email, size=40):
+ """
+ mk:
+ 获得用户头像 - 优先使用OAuth头像,否则使用默认头像
+
+ Args:
+ email: 用户邮箱地址
+ size: 头像尺寸,默认40px
+
+ Returns:
+ 用户头像URL地址
+ """
+ cachekey = 'avatar/' + email
+ url = cache.get(cachekey)
+ if url:
+ return url
+
+ # mk:检查OAuth用户是否有自定义头像
+ usermodels = OAuthUser.objects.filter(email=email)
+ if usermodels:
+ # mk:过滤出有头像的用户
+ users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
+ if users_with_picture:
+ # mk:获取默认头像路径用于比较
+ default_avatar_path = static('blog/img/avatar.png')
+
+ # mk:优先选择非默认头像的用户,否则选择第一个
+ non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
+ selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
+
+ url = selected_user.picture
+ cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
+
+ avatar_type = 'non-default' if non_default_users else 'default'
+ logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
+ return url
+
+ # mk:使用默认头像
+ url = static('blog/img/avatar.png')
+ cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
+ logger.info('Using default avatar for {}'.format(email))
+ return url
+
+
+@register.filter
+def gravatar(email, size=40):
+ """
+ mk:
+ 获得用户头像HTML标签
+
+ Args:
+ email: 用户邮箱地址
+ size: 头像尺寸,默认40px
+
+ Returns:
+ 包含用户头像的img标签HTML字符串
+ """
+ url = gravatar_url(email, size)
+ return mark_safe(
+ ' ' %
+ (url, size, size))
+
+
+@register.simple_tag
+def query(qs, **kwargs):
+ """
+ mk:
+ 模板标签允许查询集过滤。使用方法:
+ {% query books author=author as mybooks %}
+ {% for book in mybooks %}
+ ...
+ {% endfor %}
+
+ Args:
+ qs: 查询集对象
+ **kwargs: 过滤条件键值对
+
+ Returns:
+ 过滤后的查询集
+ """
+ return qs.filter(**kwargs)
+
+
+@register.filter
+def addstr(arg1, arg2):
+ """
+ mk:
+ 连接两个字符串参数
+
+ Args:
+ arg1: 第一个字符串
+ arg2: 第二个字符串
+
+ Returns:
+ 连接后的字符串
+ """
+ return str(arg1) + str(arg2)
+
+
+# mk:=== 插件系统模板标签 ===
+
+@register.simple_tag(takes_context=True)
+def render_plugin_widgets(context, position, **kwargs):
+ """
+ mk:
+ 渲染指定位置的所有插件组件
+
+ Args:
+ context: 模板上下文
+ position: 位置标识
+ **kwargs: 传递给插件的额外参数
+
+ Returns:
+ 按优先级排序的所有插件HTML内容
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ widgets = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ widget_data = plugin.render_position_widget(
+ position=position,
+ context=context,
+ **kwargs
+ )
+ if widget_data:
+ widgets.append(widget_data)
+ except Exception as e:
+ logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ # mk:按优先级排序(数字越小优先级越高)
+ widgets.sort(key=lambda x: x['priority'])
+
+ # mk:合并HTML内容
+ html_parts = [widget['html'] for widget in widgets]
+ return mark_safe(''.join(html_parts))
+
+
+@register.simple_tag(takes_context=True)
+def plugin_head_resources(context):
+ """
+ mk:
+ 渲染所有插件的head资源(仅自定义HTML,CSS已集成到压缩系统)
+
+ Args:
+ context: 模板上下文
+
+ Returns:
+ 所有插件head资源的HTML字符串
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ resources = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ # mk:只处理自定义head HTML(CSS文件已通过压缩系统处理)
+ head_html = plugin.get_head_html(context)
+ if head_html:
+ resources.append(head_html)
+
+ except Exception as e:
+ logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ return mark_safe('\n'.join(resources))
+
+
+@register.simple_tag(takes_context=True)
+def plugin_body_resources(context):
+ """
+ mk:
+ 渲染所有插件的body资源(仅自定义HTML,JS已集成到压缩系统)
+
+ Args:
+ context: 模板上下文
+
+ Returns:
+ 所有插件body资源的HTML字符串
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ resources = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ # mk:只处理自定义body HTML(JS文件已通过压缩系统处理)
+ body_html = plugin.get_body_html(context)
+ if body_html:
+ resources.append(body_html)
+
+ except Exception as e:
+ logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ return mark_safe('\n'.join(resources))
+
+
+@register.inclusion_tag('plugins/css_includes.html')
+def plugin_compressed_css():
+ """
+ mk:
+ 插件CSS压缩包含模板
+
+ Returns:
+ 插件CSS文件列表数据字典
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ css_files = []
+ for plugin in get_loaded_plugins():
+ for css_file in plugin.get_css_files():
+ css_url = plugin.get_static_url(css_file)
+ css_files.append(css_url)
+
+ return {'css_files': css_files}
+
+
+@register.inclusion_tag('plugins/js_includes.html')
+def plugin_compressed_js():
+ """
+ mk:
+ 插件JS压缩包含模板
+
+ Returns:
+ 插件JS文件列表数据字典
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ js_files = []
+ for plugin in get_loaded_plugins():
+ for js_file in plugin.get_js_files():
+ js_url = plugin.get_static_url(js_file)
+ js_files.append(js_url)
+
+ return {'js_files': js_files}
+
+
+@register.simple_tag(takes_context=True)
+def plugin_widget(context, plugin_name, widget_type='default', **kwargs):
+ """
+ mk:
+ 渲染指定插件的组件
+
+ 使用方式:
+ {% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %}
+
+ Args:
+ context: 模板上下文
+ plugin_name: 插件名称
+ widget_type: 组件类型,默认'default'
+ **kwargs: 传递给插件的额外参数
+
+ Returns:
+ 插件组件渲染后的HTML内容
+ """
+ from djangoblog.plugin_manage.loader import get_plugin_by_slug
+
+ plugin = get_plugin_by_slug(plugin_name)
+ if plugin and hasattr(plugin, 'render_template'):
+ try:
+ widget_context = {**context.flatten(), **kwargs}
+ template_name = f"{widget_type}.html"
+ return mark_safe(plugin.render_template(template_name, widget_context))
+ except Exception as e:
+ logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}")
+
+ return ""
diff --git a/doc/DjangoBlog/blog/tests.py b/doc/DjangoBlog/blog/tests.py
new file mode 100644
index 0000000..5667cf9
--- /dev/null
+++ b/doc/DjangoBlog/blog/tests.py
@@ -0,0 +1,291 @@
+import os
+
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.core.management import call_command
+from django.core.paginator import Paginator
+from django.templatetags.static import static
+from django.test import Client, RequestFactory, TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from accounts.models import BlogUser
+from blog.forms import BlogSearchForm
+from blog.models import Article, Category, Tag, SideBar, Links
+from blog.templatetags.blog_tags import load_pagination_info, load_articletags
+from djangoblog.utils import get_current_site, get_sha256
+from oauth.models import OAuthUser, OAuthConfig
+
+
+# Create your tests here.
+
+class ArticleTest(TestCase):
+ """
+ mk:
+ 文章相关功能测试类
+
+ 包含文章创建、验证、分页、搜索、图片上传等功能的集成测试
+ """
+
+ def setUp(self):
+ """
+ mk:
+ 测试初始化方法
+
+ 创建测试客户端和请求工厂实例,用于后续测试
+ """
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_validate_article(self):
+ """
+ mk:
+ 验证文章功能测试方法
+
+ 测试文章的创建、展示、分类、标签、搜索等核心功能,
+ 包括侧边栏、分类、标签、文章的增删改查操作
+ """
+ site = get_current_site().domain
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ user.set_password("liangliangyy")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+ response = self.client.get(user.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+ response = self.client.get('/admin/servermanager/emailsendlog/')
+ response = self.client.get('admin/admin/logentry/')
+
+ # mk:创建侧边栏
+ s = SideBar()
+ s.sequence = 1
+ s.name = 'test'
+ s.content = 'test content'
+ s.is_enable = True
+ s.save()
+
+ # mk:创建分类
+ category = Category()
+ category.name = "category"
+ category.creation_time = timezone.now()
+ category.last_mod_time = timezone.now()
+ category.save()
+
+ # mk:创建标签
+ tag = Tag()
+ tag.name = "nicetag"
+ tag.save()
+
+ # mk:创建文章
+ article = Article()
+ article.title = "nicetitle"
+ article.body = "nicecontent"
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+
+ article.save()
+ self.assertEqual(0, article.tags.count())
+ article.tags.add(tag)
+ article.save()
+ self.assertEqual(1, article.tags.count())
+
+ # mk:批量创建文章用于测试
+ for i in range(20):
+ article = Article()
+ article.title = "nicetitle" + str(i)
+ article.body = "nicetitle" + str(i)
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+ article.tags.add(tag)
+ article.save()
+
+ from blog.documents import ELASTICSEARCH_ENABLED
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index")
+ response = self.client.get('/search', {'q': 'nicetitle'})
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(article.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.notify(article.get_absolute_url())
+ response = self.client.get(tag.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(category.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/search', {'q': 'django'})
+ self.assertEqual(response.status_code, 200)
+ s = load_articletags(article)
+ self.assertIsNotNone(s)
+
+ self.client.login(username='liangliangyy', password='liangliangyy')
+
+ response = self.client.get(reverse('blog:archives'))
+ self.assertEqual(response.status_code, 200)
+
+ # mk:测试各种分页情况
+ p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
+ self.check_pagination(p, '', '')
+
+ p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类标签归档', tag.slug)
+
+ p = Paginator(
+ Article.objects.filter(
+ author__username='liangliangyy'), settings.PAGINATE_BY)
+ self.check_pagination(p, '作者文章归档', 'liangliangyy')
+
+ p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类目录归档', category.slug)
+
+ f = BlogSearchForm()
+ f.search()
+ # self.client.login(username='liangliangyy', password='liangliangyy')
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.baidu_notify([article.get_full_url()])
+
+ from blog.templatetags.blog_tags import gravatar_url, gravatar
+ u = gravatar_url('liangliangyy@gmail.com')
+ u = gravatar('liangliangyy@gmail.com')
+
+ # mk:测试链接功能
+ link = Links(
+ sequence=1,
+ name="lylinux",
+ link='https://wwww.lylinux.net')
+ link.save()
+ response = self.client.get('/links.html')
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/feed/')
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/sitemap.xml')
+ self.assertEqual(response.status_code, 200)
+
+ self.client.get("/admin/blog/article/1/delete/")
+ self.client.get('/admin/servermanager/emailsendlog/')
+ self.client.get('/admin/admin/logentry/')
+ self.client.get('/admin/admin/logentry/1/change/')
+
+ def check_pagination(self, p, type, value):
+ """
+ mk:
+ 检查分页功能
+
+ 遍历所有分页页面,验证分页信息加载和前后页链接的有效性
+
+ Args:
+ p (Paginator): Django分页器对象
+ type (str): 分页类型标识
+ value (str): 分页值(如标签slug、用户名等)
+ """
+ for page in range(1, p.num_pages + 1):
+ s = load_pagination_info(p.page(page), type, value)
+ self.assertIsNotNone(s)
+ if s['previous_url']:
+ response = self.client.get(s['previous_url'])
+ self.assertEqual(response.status_code, 200)
+ if s['next_url']:
+ response = self.client.get(s['next_url'])
+ self.assertEqual(response.status_code, 200)
+
+ def test_image(self):
+ """
+ mk:
+ 图片上传功能测试方法
+
+ 测试图片下载、上传权限验证、文件上传等流程
+ """
+ import requests
+ rsp = requests.get(
+ 'https://www.python.org/static/img/python-logo.png')
+ imagepath = os.path.join(settings.BASE_DIR, 'python.png')
+ with open(imagepath, 'wb') as file:
+ file.write(rsp.content)
+ rsp = self.client.post('/upload')
+ self.assertEqual(rsp.status_code, 403)
+ sign = get_sha256(get_sha256(settings.SECRET_KEY))
+ with open(imagepath, 'rb') as file:
+ imgfile = SimpleUploadedFile(
+ 'python.png', file.read(), content_type='image/jpg')
+ form_data = {'python.png': imgfile}
+ rsp = self.client.post(
+ '/upload?sign=' + sign, form_data, follow=True)
+ self.assertEqual(rsp.status_code, 200)
+ os.remove(imagepath)
+ from djangoblog.utils import save_user_avatar, send_email
+ send_email(['qq@qq.com'], 'testTitle', 'testContent')
+ save_user_avatar(
+ 'https://www.python.org/static/img/python-logo.png')
+
+ def test_errorpage(self):
+ """
+ mk:
+ 错误页面测试方法
+
+ 测试访问不存在页面时返回404状态码
+ """
+ rsp = self.client.get('/eee')
+ self.assertEqual(rsp.status_code, 404)
+
+ def test_commands(self):
+ """
+ mk:
+ Django管理命令测试方法
+
+ 测试各种自定义管理命令的执行,包括索引构建、百度推送、测试数据创建等
+ """
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ user.set_password("liangliangyy")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+
+ #mk: 创建OAuth配置和用户
+ c = OAuthConfig()
+ c.type = 'qq'
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+
+ u = OAuthUser()
+ u.type = 'qq'
+ u.openid = 'openid'
+ u.user = user
+ u.picture = static("/blog/img/avatar.png")
+ u.metadata = '''
+{
+"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+}'''
+ u.save()
+
+ u = OAuthUser()
+ u.type = 'qq'
+ u.openid = 'openid1'
+ u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
+ u.metadata = '''
+ {
+ "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+ }'''
+ u.save()
+
+ from blog.documents import ELASTICSEARCH_ENABLED
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index")
+ call_command("ping_baidu", "all")
+ call_command("create_testdata")
+ call_command("clear_cache")
+ call_command("sync_user_avatar")
+ call_command("build_search_words")
diff --git a/doc/DjangoBlog/blog/urls.py b/doc/DjangoBlog/blog/urls.py
new file mode 100644
index 0000000..8abe884
--- /dev/null
+++ b/doc/DjangoBlog/blog/urls.py
@@ -0,0 +1,90 @@
+from django.urls import path
+from django.views.decorators.cache import cache_page
+
+from . import views
+
+# mk:定义应用的命名空间,用于URL反向解析时区分不同应用的同名URL
+app_name = "blog"
+
+# mk:URL模式配置列表,定义了博客应用的所有路由规则
+urlpatterns = [
+ # mk:首页路由,显示文章列表,默认第一页
+ path(
+ r'',
+ views.IndexView.as_view(),
+ name='index'),
+
+ # mk:分页首页路由,显示指定页码的文章列表
+ path(
+ r'page//',
+ views.IndexView.as_view(),
+ name='index_page'),
+
+ # mk:文章详情页路由,通过年月日和文章ID访问具体文章
+ path(
+ r'article////.html',
+ views.ArticleDetailView.as_view(),
+ name='detailbyid'),
+
+ # mk:分类详情页路由,通过分类名称访问该分类下的所有文章
+ path(
+ r'category/.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail'),
+
+ # mk:分类详情页分页路由,通过分类名称和页码访问该分类下指定页码的文章
+ path(
+ r'category//.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail_page'),
+
+ # mk:作者详情页路由,通过作者名称访问该作者发布的所有文章
+ path(
+ r'author/.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail'),
+
+ # mk:作者详情页分页路由,通过作者名称和页码访问该作者发布指定页码的文章
+ path(
+ r'author//.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail_page'),
+
+ # mk:标签详情页路由,通过标签名称访问带有该标签的所有文章
+ path(
+ r'tag/.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail'),
+
+ # mk:标签详情页分页路由,通过标签名称和页码访问带有该标签的指定页码文章
+ path(
+ r'tag//.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail_page'),
+
+ # mk:归档页面路由,缓存1小时,显示所有文章的归档信息
+ path(
+ 'archives.html',
+ cache_page(
+ 60 * 60)(
+ views.ArchivesView.as_view()),
+ name='archives'),
+
+ # mk:友情链接页面路由,显示所有友情链接
+ path(
+ 'links.html',
+ views.LinkListView.as_view(),
+ name='links'),
+
+ # mk:文件上传路由,处理文件上传请求
+ path(
+ r'upload',
+ views.fileupload,
+ name='upload'),
+
+ # mk:清除缓存路由,处理清除缓存的请求
+ path(
+ r'clean',
+ views.clean_cache_view,
+ name='clean'),
+]
diff --git a/doc/DjangoBlog/blog/views.py b/doc/DjangoBlog/blog/views.py
new file mode 100644
index 0000000..2919809
--- /dev/null
+++ b/doc/DjangoBlog/blog/views.py
@@ -0,0 +1,638 @@
+import logging
+import os
+import uuid
+
+from django.conf import settings
+from django.core.paginator import Paginator
+from django.http import HttpResponse, HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.templatetags.static import static
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic.detail import DetailView
+from django.views.generic.list import ListView
+from haystack.views import SearchView
+
+from blog.models import Article, Category, LinkShowType, Links, Tag
+from comments.forms import CommentForm
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+from djangoblog.utils import cache, get_blog_setting, get_sha256
+
+logger = logging.getLogger(__name__)
+
+
+class ArticleListView(ListView):
+ """
+ mk:
+ 文章列表视图基类,继承自Django的ListView
+
+ Attributes:
+ template_name (str): 模板文件路径
+ context_object_name (str): 上下文对象名称
+ page_type (str): 页面类型标识
+ paginate_by (int): 每页显示的文章数量
+ page_kwarg (str): 分页参数名称
+ link_type (LinkShowType): 链接显示类型
+ """
+
+ # mk:template_name属性用于指定使用哪个模板进行渲染
+ template_name = 'blog/article_index.html'
+
+ # mk:context_object_name属性用于给上下文变量取名(在模板中使用该名字)
+ context_object_name = 'article_list'
+
+ # mk:页面类型,分类目录或标签列表等
+ page_type = ''
+ paginate_by = settings.PAGINATE_BY
+ page_kwarg = 'page'
+ link_type = LinkShowType.L
+
+ def get_view_cache_key(self):
+ """
+ mk:
+ 获取视图缓存键
+
+ Returns:
+ str: 缓存键值
+ """
+ return self.request.get['pages']
+
+ @property
+ def page_number(self):
+ """
+ mk:
+ 获取当前页码
+
+ Returns:
+ int: 当前页码
+ """
+ page_kwarg = self.page_kwarg
+ page = self.kwargs.get(
+ page_kwarg) or self.request.GET.get(page_kwarg) or 1
+ return page
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 子类重写.获得queryset的缓存key
+
+ Raises:
+ NotImplementedError: 子类必须实现此方法
+ """
+ raise NotImplementedError()
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 子类重写.获取queryset的数据
+
+ Raises:
+ NotImplementedError: 子类必须实现此方法
+ """
+ raise NotImplementedError()
+
+ def get_queryset_from_cache(self, cache_key):
+ """
+ mk:
+ 从缓存获取查询结果集
+
+ Args:
+ cache_key (str): 缓存键
+
+ Returns:
+ QuerySet: 查询结果集
+ """
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get view cache.key:{key}'.format(key=cache_key))
+ return value
+ else:
+ article_list = self.get_queryset_data()
+ cache.set(cache_key, article_list)
+ logger.info('set view cache.key:{key}'.format(key=cache_key))
+ return article_list
+
+ def get_queryset(self):
+ """
+ mk:
+ 重写默认方法,从缓存获取数据
+
+ Returns:
+ QuerySet: 查询结果集
+ """
+ key = self.get_queryset_cache_key()
+ value = self.get_queryset_from_cache(key)
+ return value
+
+ def get_context_data(self, **kwargs):
+ """
+ mk:
+ 获取上下文数据
+
+ Args:
+ **kwargs: 额外的关键字参数
+
+ Returns:
+ dict: 上下文字典
+ """
+ kwargs['linktype'] = self.link_type
+ return super(ArticleListView, self).get_context_data(**kwargs)
+
+
+class IndexView(ArticleListView):
+ """
+ mk:
+ 首页视图类,显示所有已发布的文章列表
+ """
+
+ # 友情链接类型
+ link_type = LinkShowType.I
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 获取首页文章数据
+
+ Returns:
+ QuerySet: 已发布文章的查询集
+ """
+ article_list = Article.objects.filter(type='a', status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 获取首页缓存键
+
+ Returns:
+ str: 首页缓存键
+ """
+ cache_key = 'index_{page}'.format(page=self.page_number)
+ return cache_key
+
+
+class ArticleDetailView(DetailView):
+ """
+ mk:
+ 文章详情页面视图类
+
+ Attributes:
+ template_name (str): 模板文件路径
+ model: 数据模型类
+ pk_url_kwarg (str): URL中的主键参数名
+ context_object_name (str): 上下文对象名称
+ """
+
+ template_name = 'blog/article_detail.html'
+ model = Article
+ pk_url_kwarg = 'article_id'
+ context_object_name = "article"
+
+ def get_context_data(self, **kwargs):
+ """
+ mk:
+ 获取文章详情页上下文数据,包括评论分页、前后文章等信息
+
+ Args:
+ **kwargs: 额外的关键字参数
+
+ Returns:
+ dict: 包含文章详情和相关数据的上下文字典
+ """
+ comment_form = CommentForm()
+
+ article_comments = self.object.comment_list()
+ parent_comments = article_comments.filter(parent_comment=None)
+ blog_setting = get_blog_setting()
+ paginator = Paginator(parent_comments, blog_setting.article_comment_count)
+ page = self.request.GET.get('comment_page', '1')
+ if not page.isnumeric():
+ page = 1
+ else:
+ page = int(page)
+ if page < 1:
+ page = 1
+ if page > paginator.num_pages:
+ page = paginator.num_pages
+
+ p_comments = paginator.page(page)
+ next_page = p_comments.next_page_number() if p_comments.has_next() else None
+ prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
+
+ if next_page:
+ kwargs[
+ 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
+ if prev_page:
+ kwargs[
+ 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
+ kwargs['form'] = comment_form
+ kwargs['article_comments'] = article_comments
+ kwargs['p_comments'] = p_comments
+ kwargs['comment_count'] = len(
+ article_comments) if article_comments else 0
+
+ kwargs['next_article'] = self.object.next_article
+ kwargs['prev_article'] = self.object.prev_article
+
+ context = super(ArticleDetailView, self).get_context_data(**kwargs)
+ article = self.object
+
+ # mk:触发文章详情加载钩子,让插件可以添加额外的上下文数据
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
+ hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
+
+ # mk:Action Hook, 通知插件"文章详情已获取"
+ hooks.run_action('after_article_body_get', article=article, request=self.request)
+ return context
+
+
+class CategoryDetailView(ArticleListView):
+ """
+ mk:
+ 分类目录列表视图类,显示指定分类下的文章列表
+ """
+
+ page_type = "分类目录归档"
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 根据分类名称获取该分类及其子分类下的所有文章
+
+ Returns:
+ QuerySet: 指定分类下的文章查询集
+ """
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
+
+ categoryname = category.name
+ self.categoryname = categoryname
+ categorynames = list(
+ map(lambda c: c.name, category.get_sub_categorys()))
+ article_list = Article.objects.filter(
+ category__name__in=categorynames, status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 获取分类页面缓存键
+
+ Returns:
+ str: 分类页面缓存键
+ """
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
+ categoryname = category.name
+ self.categoryname = categoryname
+ cache_key = 'category_list_{categoryname}_{page}'.format(
+ categoryname=categoryname, page=self.page_number)
+ return cache_key
+
+ def get_context_data(self, **kwargs):
+ """
+ mk:
+ 获取分类页面上下文数据
+
+ Args:
+ **kwargs: 额外的关键字参数
+
+ Returns:
+ dict: 包含分类信息的上下文字典
+ """
+ categoryname = self.categoryname
+ try:
+ categoryname = categoryname.split('/')[-1]
+ except BaseException:
+ pass
+ kwargs['page_type'] = CategoryDetailView.page_type
+ kwargs['tag_name'] = categoryname
+ return super(CategoryDetailView, self).get_context_data(**kwargs)
+
+
+class AuthorDetailView(ArticleListView):
+ """
+ mk:
+ 作者详情页视图类,显示指定作者的文章列表
+ """
+
+ page_type = '作者文章归档'
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 获取作者页面缓存键
+
+ Returns:
+ str: 作者页面缓存键
+ """
+ from uuslug import slugify
+ author_name = slugify(self.kwargs['author_name'])
+ cache_key = 'author_{author_name}_{page}'.format(
+ author_name=author_name, page=self.page_number)
+ return cache_key
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 根据作者用户名获取该作者的所有文章
+
+ Returns:
+ QuerySet: 指定作者的文章查询集
+ """
+ author_name = self.kwargs['author_name']
+ article_list = Article.objects.filter(
+ author__username=author_name, type='a', status='p')
+ return article_list
+
+ def get_context_data(self, **kwargs):
+ """
+ mk:
+ 获取作者页面上下文数据
+
+ Args:
+ **kwargs: 额外的关键字参数
+
+ Returns:
+ dict: 包含作者信息的上下文字典
+ """
+ author_name = self.kwargs['author_name']
+ kwargs['page_type'] = AuthorDetailView.page_type
+ kwargs['tag_name'] = author_name
+ return super(AuthorDetailView, self).get_context_data(**kwargs)
+
+
+class TagDetailView(ArticleListView):
+ """
+ mk:
+ 标签列表页面视图类,显示指定标签下的文章列表
+ """
+
+ page_type = '分类标签归档'
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 根据标签名称获取该标签下的所有文章
+
+ Returns:
+ QuerySet: 指定标签下的文章查询集
+ """
+ slug = self.kwargs['tag_name']
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name
+ self.name = tag_name
+ article_list = Article.objects.filter(
+ tags__name=tag_name, type='a', status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 获取标签页面缓存键
+
+ Returns:
+ str: 标签页面缓存键
+ """
+ slug = self.kwargs['tag_name']
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name
+ self.name = tag_name
+ cache_key = 'tag_{tag_name}_{page}'.format(
+ tag_name=tag_name, page=self.page_number)
+ return cache_key
+
+ def get_context_data(self, **kwargs):
+ """
+ mk:
+ 获取标签页面上下文数据
+
+ Args:
+ **kwargs: 额外的关键字参数
+
+ Returns:
+ dict: 包含标签信息的上下文字典
+ """
+ # tag_name = self.kwargs['tag_name']
+ tag_name = self.name
+ kwargs['page_type'] = TagDetailView.page_type
+ kwargs['tag_name'] = tag_name
+ return super(TagDetailView, self).get_context_data(**kwargs)
+
+
+class ArchivesView(ArticleListView):
+ """
+ mk:
+ 文章归档页面视图类,显示所有文章的时间归档
+ """
+
+ page_type = '文章归档'
+ paginate_by = None
+ page_kwarg = None
+ template_name = 'blog/article_archives.html'
+
+ def get_queryset_data(self):
+ """
+ mk:
+ 获取所有已发布文章的数据
+
+ Returns:
+ QuerySet: 所有已发布文章的查询集
+ """
+ return Article.objects.filter(status='p').all()
+
+ def get_queryset_cache_key(self):
+ """
+ mk:
+ 获取归档页面缓存键
+
+ Returns:
+ str: 归档页面缓存键
+ """
+ cache_key = 'archives'
+ return cache_key
+
+
+class LinkListView(ListView):
+ """
+ mk:
+ 友情链接列表视图类
+
+ Attributes:
+ model: 链接数据模型
+ template_name (str): 模板文件路径
+ """
+
+ model = Links
+ template_name = 'blog/links_list.html'
+
+ def get_queryset(self):
+ """
+ mk:
+ 获取启用的友情链接查询集
+
+ Returns:
+ QuerySet: 启用的友情链接查询集
+ """
+ return Links.objects.filter(is_enable=True)
+
+
+class EsSearchView(SearchView):
+ """
+ mk:
+ Elasticsearch搜索视图类,继承自haystack的SearchView
+ """
+
+ def get_context(self):
+ """
+ mk:
+ 获取搜索结果上下文数据
+
+ Returns:
+ dict: 包含搜索结果的上下文字典
+ """
+ paginator, page = self.build_page()
+ context = {
+ "query": self.query,
+ "form": self.form,
+ "page": page,
+ "paginator": paginator,
+ "suggestion": None,
+ }
+ if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
+ context["suggestion"] = self.results.query.get_spelling_suggestion()
+ context.update(self.extra_context())
+
+ return context
+
+
+@csrf_exempt
+def fileupload(request):
+ """
+ mk:
+ 文件上传处理函数,提供图床功能
+
+ Args:
+ request (HttpRequest): HTTP请求对象
+
+ Returns:
+ HttpResponse: 包含上传文件URL的响应对象
+ """
+ if request.method == 'POST':
+ sign = request.GET.get('sign', None)
+ if not sign:
+ return HttpResponseForbidden()
+ if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
+ return HttpResponseForbidden()
+ response = []
+ for filename in request.FILES:
+ timestr = timezone.now().strftime('%Y/%m/%d')
+ imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
+ fname = u''.join(str(filename))
+ isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
+ base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
+ if not os.path.exists(base_dir):
+ os.makedirs(base_dir)
+ savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
+ if not savepath.startswith(base_dir):
+ return HttpResponse("only for post")
+ with open(savepath, 'wb+') as wfile:
+ for chunk in request.FILES[filename].chunks():
+ wfile.write(chunk)
+ if isimage:
+ from PIL import Image
+ image = Image.open(savepath)
+ image.save(savepath, quality=20, optimize=True)
+ url = static(savepath)
+ response.append(url)
+ return HttpResponse(response)
+
+ else:
+ return HttpResponse("only for post")
+
+
+def page_not_found_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ """
+ mk:
+ 404页面未找到错误处理函数
+
+ Args:
+ request (HttpRequest): HTTP请求对象
+ exception (Exception): 异常对象
+ template_name (str): 错误页面模板名称
+
+ Returns:
+ HttpResponse: 404错误页面响应
+ """
+ if exception:
+ logger.error(exception)
+ url = request.get_full_path()
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
+ 'statuscode': '404'},
+ status=404)
+
+
+def server_error_view(request, template_name='blog/error_page.html'):
+ """
+ mk:
+ 500服务器内部错误处理函数
+
+ Args:
+ request (HttpRequest): HTTP请求对象
+ template_name (str): 错误页面模板名称
+
+ Returns:
+ HttpResponse: 500错误页面响应
+ """
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the server is busy, please click the home page to see other?'),
+ 'statuscode': '500'},
+ status=500)
+
+
+def permission_denied_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ """
+ mk:
+ 403权限拒绝错误处理函数
+
+ Args:
+ request (HttpRequest): HTTP请求对象
+ exception (Exception): 异常对象
+ template_name (str): 错误页面模板名称
+
+ Returns:
+ HttpResponse: 403错误页面响应
+ """
+ if exception:
+ logger.error(exception)
+ return render(
+ request, template_name, {
+ 'message': _('Sorry, you do not have permission to access this page?'),
+ 'statuscode': '403'}, status=403)
+
+
+def clean_cache_view(request):
+ """
+ mk:
+ 清除缓存视图函数
+
+ Args:
+ request (HttpRequest): HTTP请求对象
+
+ Returns:
+ HttpResponse: 返回'ok'表示清除成功
+ """
+ cache.clear()
+ return HttpResponse('ok')
diff --git a/doc/DjangoBlog/codecov.yml b/doc/DjangoBlog/codecov.yml
new file mode 100644
index 0000000..2298829
--- /dev/null
+++ b/doc/DjangoBlog/codecov.yml
@@ -0,0 +1,87 @@
+codecov:
+ require_ci_to_pass: yes
+
+coverage:
+ precision: 2
+ round: down
+ range: "70...100"
+
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 1%
+ informational: true
+ patch:
+ default:
+ target: auto
+ threshold: 1%
+ informational: true
+
+parsers:
+ gcov:
+ branch_detection:
+ conditional: yes
+ loop: yes
+ method: no
+ macro: no
+
+comment:
+ layout: "reach,diff,flags,tree"
+ behavior: default
+ require_changes: no
+
+ignore:
+ # Django 相关
+ - "*/migrations/*"
+ - "manage.py"
+ - "*/settings.py"
+ - "*/wsgi.py"
+ - "*/asgi.py"
+
+ # 测试相关
+ - "*/tests/*"
+ - "*/test_*.py"
+ - "*/*test*.py"
+
+ # 静态文件和模板
+ - "*/static/*"
+ - "*/templates/*"
+ - "*/collectedstatic/*"
+
+ # 国际化文件
+ - "*/locale/*"
+ - "**/*.po"
+ - "**/*.mo"
+
+ # 文档和部署
+ - "*/docs/*"
+ - "*/deploy/*"
+ - "README*.md"
+ - "LICENSE"
+ - "Dockerfile"
+ - "docker-compose*.yml"
+ - "*.yaml"
+ - "*.yml"
+
+ # 开发环境
+ - "*/venv/*"
+ - "*/__pycache__/*"
+ - "*.pyc"
+ - ".coverage"
+ - "coverage.xml"
+
+ # 日志文件
+ - "*/logs/*"
+ - "*.log"
+
+ # 特定文件
+ - "*/whoosh_cn_backend.py" # 搜索后端
+ - "*/elasticsearch_backend.py" # 搜索后端
+ - "*/MemcacheStorage.py" # 缓存存储
+ - "*/robot.py" # 机器人相关
+
+ # 配置文件
+ - "codecov.yml"
+ - ".coveragerc"
+ - "requirements*.txt"
diff --git a/doc/DjangoBlog/comments/admin.py b/doc/DjangoBlog/comments/admin.py
new file mode 100644
index 0000000..65252e9
--- /dev/null
+++ b/doc/DjangoBlog/comments/admin.py
@@ -0,0 +1,77 @@
+# FRR:该模块用于配置Django后台系统中评论(Comment)模型的管理界面,
+# 包括自定义列表展示、批量操作、字段过滤及关联对象链接等功能,
+# 方便管理员在后台对评论数据进行高效管理。
+
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+
+# FRR:自定义批量操作函数,用于将选中的评论设置为"禁用"状态
+def disable_commentstatus(modeladmin, request, queryset):
+ # FRR:通过queryset批量更新is_enable字段为False
+ queryset.update(is_enable=False)
+
+
+# FRR:自定义批量操作函数,用于将选中的评论设置为"启用"状态
+def enable_commentstatus(modeladmin, request, queryset):
+ # FRR:通过queryset批量更新is_enable字段为True
+ queryset.update(is_enable=True)
+
+
+# FRR:为批量操作函数设置在后台显示的名称(支持国际化)
+disable_commentstatus.short_description = _('Disable comments')
+enable_commentstatus.short_description = _('Enable comments')
+
+
+# FRR:评论模型(Comment)的后台管理配置类,继承自Django默认的ModelAdmin
+class CommentAdmin(admin.ModelAdmin):
+ # FRR:每页显示20条评论数据
+ list_per_page = 20
+ # FRR:列表页展示的字段,包括自定义的关联对象链接字段
+ list_display = (
+ 'id', # 评论ID
+ 'body', # 评论内容
+ 'link_to_userinfo', # 自定义字段:评论作者的链接
+ 'link_to_article', # 自定义字段:评论所属文章的链接
+ 'is_enable', # 是否启用
+ 'creation_time' # 创建时间
+ )
+ # FRR:列表页中可点击跳转详情页的字段
+ list_display_links = ('id', 'body', 'is_enable')
+ # LGM:右侧过滤栏,可按"是否启用"筛选评论
+ list_filter = ('is_enable',)
+ # FRR:编辑页中排除的字段(创建时间和最后修改时间通常自动生成,不允许手动编辑)
+ exclude = ('creation_time', 'last_modify_time')
+ # FRR:注册批量操作函数,在列表页顶部"动作"下拉框中显示
+ actions = [disable_commentstatus, enable_commentstatus]
+ # FRR:将外键字段(author和article)显示为输入框(而非下拉框),适合数据量大的场景
+ raw_id_fields = ('author', 'article')
+ # FRR:设置搜索框可搜索的字段(按评论内容搜索)
+ search_fields = ('body',)
+
+ # FRR:自定义列表字段,显示评论作者的链接(点击可跳转到作者的后台编辑页)
+ def link_to_userinfo(self, obj):
+ # FRR:获取作者模型(author)的元数据,用于生成admin链接
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ #FRR:生成作者在admin后台的编辑页URL
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ #FRR:返回HTML格式的链接,显示作者昵称(若无昵称则显示邮箱)
+ return format_html(
+ u'%s ' %
+ (link, obj.author.nickname if obj.author.nickname else obj.author.email))
+
+ # FRR:自定义列表字段,显示评论所属文章的链接(点击可跳转到文章的后台编辑页)
+ def link_to_article(self, obj):
+ # FRR:获取文章模型(article)的元数据,用于生成admin链接
+ info = (obj.article._meta.app_label, obj.article._meta.model_name)
+ # FRR:生成文章在admin后台的编辑页URL
+ link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
+ # FRR:返回HTML格式的链接,显示文章标题
+ return format_html(
+ u'%s ' % (link, obj.article.title))
+
+ # FRR:设置自定义字段在列表页的表头名称(支持国际化)
+ link_to_userinfo.short_description = _('User')
+ link_to_article.short_description = _('Article')
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/apps.py b/doc/DjangoBlog/comments/apps.py
new file mode 100644
index 0000000..1a50a87
--- /dev/null
+++ b/doc/DjangoBlog/comments/apps.py
@@ -0,0 +1,10 @@
+# FRR:该模块定义了Django应用"comments"的配置类,
+# 用于告诉Django如何识别和配置该应用,是Django应用的核心配置文件之一。
+
+from django.apps import AppConfig
+
+
+# FRR:评论应用(comments)的配置类,继承自Django的AppConfig基类
+class CommentsConfig(AppConfig):
+ # FRR:应用的名称,必须与应用目录名一致,Django通过该名称识别应用
+ name = 'comments'
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/forms.py b/doc/DjangoBlog/comments/forms.py
new file mode 100644
index 0000000..f82bb0f
--- /dev/null
+++ b/doc/DjangoBlog/comments/forms.py
@@ -0,0 +1,21 @@
+# FRR:该模块定义评论相关的表单类,用于处理用户提交评论的数据验证和交互,
+# 继承Django的ModelForm实现与Comment模型的绑定,简化表单开发流程。
+
+from django import forms
+from django.forms import ModelForm
+
+from .models import Comment # 导入评论模型,用于表单与模型的关联
+
+
+# FRR:评论表单类,继承ModelForm实现基于Comment模型的表单自动生成
+class CommentForm(ModelForm):
+ # FRR:定义父评论ID字段,用于支持评论回复功能
+ # 采用HiddenInput控件(隐藏输入框),用户不可见但表单会提交该值
+ # required=False表示允许为空(即该评论可以是一级评论,无父评论)
+ parent_comment_id = forms.IntegerField(
+ widget=forms.HiddenInput, required=False)
+
+ # FRR:Meta内部类用于配置表单与模型的关联信息
+ class Meta:
+ model = Comment # 指定关联的模型为Comment
+ fields = ['body'] # 表单中需要包含的模型字段,此处仅包含评论内容字段body
diff --git a/doc/DjangoBlog/comments/migrations/0001_initial.py b/doc/DjangoBlog/comments/migrations/0001_initial.py
new file mode 100644
index 0000000..61d1e53
--- /dev/null
+++ b/doc/DjangoBlog/comments/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('blog', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('body', models.TextField(max_length=300, verbose_name='正文')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
+ ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
+ ],
+ options={
+ 'verbose_name': '评论',
+ 'verbose_name_plural': '评论',
+ 'ordering': ['-id'],
+ 'get_latest_by': 'id',
+ },
+ ),
+ ]
diff --git a/doc/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py b/doc/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py
new file mode 100644
index 0000000..17c44db
--- /dev/null
+++ b/doc/DjangoBlog/comments/migrations/0002_alter_comment_is_enable.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.7 on 2023-04-24 13:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('comments', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='是否显示'),
+ ),
+ ]
diff --git a/doc/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/doc/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
new file mode 100644
index 0000000..a1ca970
--- /dev/null
+++ b/doc/DjangoBlog/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('blog', '0005_alter_article_options_alter_category_options_and_more'),
+ ('comments', '0002_alter_comment_is_enable'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='comment',
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='article',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='enable'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='parent_comment',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
+ ),
+ ]
diff --git a/doc/DjangoBlog/comments/migrations/__init__.py b/doc/DjangoBlog/comments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/comments/models.py b/doc/DjangoBlog/comments/models.py
new file mode 100644
index 0000000..5cfd014
--- /dev/null
+++ b/doc/DjangoBlog/comments/models.py
@@ -0,0 +1,61 @@
+# FRR:该模块定义了评论(Comment)数据模型,用于存储用户对文章的评论信息,
+# 包括评论内容、作者、关联文章、创建时间等字段,同时支持评论回复功能(父子评论关联)。
+
+from django.conf import settings
+from django.db import models
+from django.utils.timezone import now # 用于获取当前时间
+from django.utils.translation import gettext_lazy as _ # 用于国际化翻译
+
+from blog.models import Article # 导入文章模型,建立评论与文章的关联
+
+
+# FRR:评论模型类,继承自Django的Model基类,映射数据库中的评论表
+class Comment(models.Model):
+ # FRR:评论正文字段,TextField支持长文本,max_length限制最大长度为300字符
+ body = models.TextField('正文', max_length=300)
+
+ # FRR:评论创建时间字段,默认值为当前时间,verbose_name用于后台显示的字段名(国际化)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+
+ # FRR:评论最后修改时间字段,默认值为当前时间,用于记录评论更新时间
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ # FRR:评论作者外键,关联Django内置的用户模型(settings.AUTH_USER_MODEL)
+ # on_delete=models.CASCADE表示:若用户被删除,其评论也会被级联删除
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE)
+
+ # FRR:关联的文章外键,评论属于某篇文章
+ # on_delete=models.CASCADE表示:若文章被删除,其下所有评论也会被级联删除
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE)
+
+ # FRR:父评论外键,实现评论回复功能(自关联)
+ # 'self'表示关联自身模型,blank=True和null=True允许为空(即一级评论无父评论)
+ # on_delete=models.CASCADE表示:若父评论被删除,其下所有子评论也会被级联删除
+ parent_comment = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent comment'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+
+ # FRR:评论是否启用的状态字段,默认值为False(可能需要管理员审核后启用)
+ # blank=False和null=False表示该字段为必填项,不允许为空
+ is_enable = models.BooleanField(_('enable'),
+ default=False, blank=False, null=False)
+
+ # FRR:Meta内部类用于配置模型的元数据
+ class Meta:
+ ordering = ['-id'] # 数据查询时按id降序排列(最新评论在前)
+ verbose_name = _('comment') # 模型的单数显示名称(国际化)
+ verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同)
+ get_latest_by = 'id' # 指定通过id字段获取最新记录
+
+ # FRR:定义模型实例的字符串表示,返回评论正文(便于在后台和调试时识别对象)
+ def __str__(self):
+ return self.body
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/templatetags/__init__.py b/doc/DjangoBlog/comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/comments/templatetags/comments_tags.py b/doc/DjangoBlog/comments/templatetags/comments_tags.py
new file mode 100644
index 0000000..fde02b4
--- /dev/null
+++ b/doc/DjangoBlog/comments/templatetags/comments_tags.py
@@ -0,0 +1,30 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def parse_commenttree(commentlist, comment):
+ """获得当前评论子评论的列表
+ 用法: {% parse_commenttree article_comments comment as childcomments %}
+ """
+ datas = []
+
+ def parse(c):
+ childs = commentlist.filter(parent_comment=c, is_enable=True)
+ for child in childs:
+ datas.append(child)
+ parse(child)
+
+ parse(comment)
+ return datas
+
+
+@register.inclusion_tag('comments/tags/comment_item.html')
+def show_comment_item(comment, ischild):
+ """评论"""
+ depth = 1 if ischild else 2
+ return {
+ 'comment_item': comment,
+ 'depth': depth
+ }
diff --git a/doc/DjangoBlog/comments/tests.py b/doc/DjangoBlog/comments/tests.py
new file mode 100644
index 0000000..9305494
--- /dev/null
+++ b/doc/DjangoBlog/comments/tests.py
@@ -0,0 +1,106 @@
+# FRR:该模块为评论功能(comments)的单元测试类,
+# 主要测试评论提交、评论列表展示、评论嵌套结构解析及邮件通知等核心功能,
+# 确保评论功能在各种场景下正常工作。
+
+from django.test import Client, RequestFactory, TransactionTestCase
+from django.urls import reverse # 用于生成URL反向解析
+
+from accounts.models import BlogUser # 导入用户模型,用于创建测试用户
+from blog.models import Category, Article # 导入分类和文章模型,用于创建测试文章
+from comments.models import Comment # 导入评论模型,用于测试评论数据
+from comments.templatetags.comments_tags import * # 导入评论相关模板标签,测试评论渲染
+from djangoblog.utils import get_max_articleid_commentid # 导入工具函数,测试ID获取功能
+
+
+# FRR:评论功能测试类,继承TransactionTestCase以支持事务性测试(避免测试数据污染)
+class CommentsTest(TransactionTestCase):
+ # LGM:测试前的初始化方法,会在每个测试方法执行前运行
+ def setUp(self):
+ self.client = Client() # 创建测试客户端,模拟用户请求
+ self.factory = RequestFactory() # 创建请求工厂,用于构造复杂请求
+
+ # FRR:配置博客评论设置(需要审核才能显示)
+ from blog.models import BlogSettings
+ value = BlogSettings()
+ value.comment_need_review = True # 评论需要审核
+ value.save() # 保存设置到数据库
+
+ # FRR:创建超级用户(用于测试登录状态下的评论功能)
+ self.user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1") # 用户名和密码用于测试登录
+
+ # FRR:辅助方法,批量更新文章下所有评论为"启用"状态(模拟管理员审核通过)
+ def update_article_comment_status(self, article):
+ comments = article.comment_set.all() # 获取文章下所有评论
+ for comment in comments:
+ comment.is_enable = True # 设为启用
+ comment.save() # 保存修改
+
+ # FRR:核心测试方法,验证评论提交、嵌套回复、模板渲染等功能
+ def test_validate_comment(self):
+ # 1. 登录测试用户
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ # 2. 创建测试分类和文章(评论必须关联到具体文章)
+ category = Category()
+ category.name = "categoryccc" # 分类名称
+ category.save() # 保存分类
+
+ article = Article()
+ article.title = "nicetitleccc" # 文章标题
+ article.body = "nicecontentccc" # 文章内容
+ article.author = self.user # 文章作者(关联测试用户)
+ article.category = category # 关联分类
+ article.type = 'a' # 文章类型(假设'a'表示普通文章)
+ article.status = 'p' # 发布状态(假设'p'表示已发布)
+ article.save() # 保存文章
+
+ # 3. 测试提交一级评论(无父评论)
+ # 生成评论提交URL(通过文章ID反向解析)
+ comment_url = reverse(
+ 'comments:postcomment', kwargs={
+ 'article_id': article.id})
+
+ # 模拟POST请求提交评论
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff' # 评论内容
+ })
+
+ # 验证提交成功(302表示重定向,通常是提交后跳回文章页)
+ self.assertEqual(response.status_code, 302)
+
+ # 由于评论需要审核(默认未启用),此时评论列表应为空
+ article = Article.objects.get(pk=article.pk) # 重新获取文章(刷新数据)
+ self.assertEqual(len(article.comment_list()), 0) # 评论列表长度为0
+
+ # 手动审核评论(设为启用)
+ self.update_article_comment_status(article)
+ # 审核后评论列表应包含1条评论
+ self.assertEqual(len(article.comment_list()), 1)
+
+ # 4. 再次提交一条评论,测试多条评论场景
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff',
+ })
+ self.assertEqual(response.status_code, 302) # 验证提交成功
+
+ # 审核后评论列表应包含2条评论
+ article = Article.objects.get(pk=article.pk)
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 2)
+
+ # 5. 测试提交嵌套回复(针对第一条评论的回复)
+ parent_comment_id = article.comment_list()[0].id # 获取第一条评论ID作为父评论
+
+ # 提交带Markdown格式的回复内容(测试富文本支持)
+ response = self.client.post(comment_url,
+ {
+ 'body': '''
+ # Title1
+
+ ```python
+ import os
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/urls.py b/doc/DjangoBlog/comments/urls.py
new file mode 100644
index 0000000..eaf1e9f
--- /dev/null
+++ b/doc/DjangoBlog/comments/urls.py
@@ -0,0 +1,20 @@
+# FRR:该模块定义评论应用(comments)的URL路由配置,
+# 映射评论相关的视图函数/类,实现前端请求与后端处理逻辑的关联。
+
+from django.urls import path # 导入Django的path函数,用于定义URL路径
+
+from . import views # 导入当前应用的views模块,关联评论处理视图
+
+app_name = "comments" # 定义应用命名空间,避免不同应用间URL名称冲突
+
+# FRR:URL路由列表,每个path对应一个评论相关的请求路径
+urlpatterns = [
+ # FRR:评论提交路由,用于处理用户提交评论的请求
+ # 路径包含文章ID(article_id),通过捕获整数类型的文章ID参数
+ # 关联视图类CommentPostView的as_view()方法(将类视图转为可调用视图函数)
+ # 命名为'postcomment',便于在模板中通过{% url 'comments:postcomment' article_id %}反向解析
+ path(
+ 'article//postcomment',
+ views.CommentPostView.as_view(),
+ name='postcomment'),
+]
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/utils.py b/doc/DjangoBlog/comments/utils.py
new file mode 100644
index 0000000..38cc13b
--- /dev/null
+++ b/doc/DjangoBlog/comments/utils.py
@@ -0,0 +1,58 @@
+# FRR:该模块提供评论相关的邮件通知功能,
+# 当用户提交评论或收到回复时,自动发送自动发送邮件通知,提升用户交互体验。
+
+import logging # 导入日志模块,用于记录记录发送邮件过程中的错误信息
+
+from django.utils.translation import gettext_lazy as _ # 导入国际化翻译函数
+
+from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数
+from djangoblog.utils import send_email # 导入发送邮件的工具函数
+
+# FRR:创建日志记录器,用于记录当前模块的日志信息
+logger = logging.getLogger(__name__)
+
+
+def send_comment_email(comment):
+ """
+ FRR:发送评论相关邮件通知
+ 1. 向评论作者发送评论提交成功的感谢邮件
+ 2. 若当前评论是回复(有父评论),向父评论作者发送回复通知邮件
+ :param comment: 评论对象(Comment实例)
+ """
+ # FRR:获取当前网站域名(用于构建文章链接)
+ site = get_current_site().domain
+ # FRR:邮件主题(支持国际化)
+ subject = _('Thanks for your comment')
+ # FRR:构建文章详情页的完整URL(包含协议和域名)
+ article_url = f"https://{site}{comment.article.get_absolute_url()}"
+
+ # FRR:1. 向当前评论作者发送感谢邮件
+ # 构建HTML格式的邮件内容(支持国际化和变量替换)
+ html_content = _("""Thank you very much for your comments on this site
+ You can visit %(article_title)s
+ to review your comments,
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
+ tomail = comment.author.email # 收件人邮箱(当前评论作者的邮箱)
+ send_email([tomail], subject, html_content) # 调用工具函数发送邮件
+
+ # FRR:2. 若当前评论是回复(有父评论),通知父评论作者
+ try:
+ if comment.parent_comment: # 判断当前评论是否有父评论(即是否为回复)
+ # 构建回复通知的HTML邮件内容
+ html_content = _("""Your comment on %(article_title)s has
+ received a reply. %(comment_body)s
+
+ go check it out!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s
+ """) % {'article_url': article_url, 'article_title': comment.article.title,
+ 'comment_body': comment.parent_comment.body}
+ tomail = comment.parent_comment.author.email # 收件人邮箱(父评论作者的邮箱)
+ send_email([tomail], subject, html_content) # 发送回复通知邮件
+ except Exception as e:
+ # FRR:捕获发送过程中的异常并记录日志(不中断主流程)
+ logger.error(e)
\ No newline at end of file
diff --git a/doc/DjangoBlog/comments/views.py b/doc/DjangoBlog/comments/views.py
new file mode 100644
index 0000000..ba7149c
--- /dev/null
+++ b/doc/DjangoBlog/comments/views.py
@@ -0,0 +1,82 @@
+# FRR:该模块定义评论提交的视图类,负责处理用户提交评论的表单验证、
+# 数据存储及页面跳转等逻辑,是评论功能与用户交互的核心处理层。
+
+from django.core.exceptions import ValidationError # 导入验证异常类,用于处理评论关闭等错误
+from django.http import HttpResponseRedirect # 导入重定向响应类,用于提交后跳转
+from django.shortcuts import get_object_or_404 # 导入对象获取工具,不存在时返回404
+from django.utils.decorators import method_decorator # 导入方法装饰器工具
+from django.views.decorators.csrf import csrf_protect # 导入CSRF保护装饰器
+from django.views.generic.edit import FormView # 导入表单处理基类
+
+from accounts.models import BlogUser # 导入用户模型,关联评论作者
+from blog.models import Article # 导入文章模型,关联评论所属文章
+from .forms import CommentForm # 导入评论表单类,用于验证提交数据
+from .models import Comment # 导入评论模型,用于存储评论数据
+
+
+# FRR:评论提交视图类,继承FormView处理表单提交逻辑
+class CommentPostView(FormView):
+ form_class = CommentForm # 指定使用的表单类为CommentForm
+ template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
+
+ # FRR:为视图方法添加CSRF保护装饰器,防止跨站请求伪造攻击
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ # 调用父类的dispatch方法,确保视图正常处理请求
+ return super(CommentPostView, self).dispatch(*args, **kwargs)
+
+ # FRR:处理GET请求(直接访问评论提交URL时)
+ def get(self, request, *args, **kwargs):
+ article_id = self.kwargs['article_id'] # 从URL中获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 获取对应的文章,不存在则返回404
+ url = article.get_absolute_url() # 获取文章详情页的URL
+ # 重定向到文章详情页的评论区(#comments为评论区锚点)
+ return HttpResponseRedirect(url + "#comments")
+
+ # FRR:表单验证失败时的处理逻辑
+ def form_invalid(self, form):
+ article_id = self.kwargs['article_id'] # 获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 获取文章对象
+ # 渲染文章详情页,传递错误的表单对象和文章对象(便于前端显示错误信息)
+ return self.render_to_response({
+ 'form': form,
+ 'article': article
+ })
+
+ # FRR:表单验证成功后的处理逻辑(核心方法)
+ def form_valid(self, form):
+ """提交的数据验证合法后的逻辑"""
+ user = self.request.user # 获取当前登录用户
+ author = BlogUser.objects.get(pk=user.pk) # 获取用户对应的BlogUser实例
+ article_id = self.kwargs['article_id'] # 从URL获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 获取文章对象
+
+ # FRR:检查文章是否允许评论(状态为关闭则抛出验证异常)
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.")
+
+ # FRR:创建评论对象但不保存到数据库(save(False))
+ comment = form.save(False)
+ comment.article = article # 关联评论到当前文章
+
+ # FRR:根据网站设置决定评论是否需要审核(默认需要审核,is_enable=False)
+ from djangoblog.utils import get_blog_setting
+ settings = get_blog_setting() # 获取博客全局设置
+ if not settings.comment_need_review: # 若无需审核
+ comment.is_enable = True # 直接设为启用状态
+
+ comment.author = author # 关联评论作者为当前登录用户
+
+ # FRR:处理回复功能(若存在父评论ID,则关联父评论)
+ if form.cleaned_data['parent_comment_id']:
+ parent_comment = Comment.objects.get(
+ pk=form.cleaned_data['parent_comment_id']) # 获取父评论
+ comment.parent_comment = parent_comment # 关联到父评论
+
+ # FRR:保存评论到数据库(save(True)触发模型的save方法和信号)
+ comment.save(True)
+
+ # FRR:重定向到文章详情页的当前评论位置(锚点定位到具体评论)
+ return HttpResponseRedirect(
+ "%s#div-comment-%d" %
+ (article.get_absolute_url(), comment.pk))
\ No newline at end of file
diff --git a/doc/DjangoBlog/deploy/docker-compose/docker-compose.es.yml b/doc/DjangoBlog/deploy/docker-compose/docker-compose.es.yml
new file mode 100644
index 0000000..83e35ff
--- /dev/null
+++ b/doc/DjangoBlog/deploy/docker-compose/docker-compose.es.yml
@@ -0,0 +1,48 @@
+version: '3'
+
+services:
+ es:
+ image: liangliangyy/elasticsearch-analysis-ik:8.6.1
+ container_name: es
+ restart: always
+ environment:
+ - discovery.type=single-node
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+ ports:
+ - 9200:9200
+ volumes:
+ - ./bin/datas/es/:/usr/share/elasticsearch/data/
+
+ kibana:
+ image: kibana:8.6.1
+ restart: always
+ container_name: kibana
+ ports:
+ - 5601:5601
+ environment:
+ - ELASTICSEARCH_HOSTS=http://es:9200
+
+ djangoblog:
+ build: .
+ restart: always
+ command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ - ./uploads:/code/djangoblog/uploads
+ environment:
+ - DJANGO_MYSQL_DATABASE=djangoblog
+ - DJANGO_MYSQL_USER=root
+ - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ - DJANGO_MYSQL_HOST=db
+ - DJANGO_MYSQL_PORT=3306
+ - DJANGO_MEMCACHED_LOCATION=memcached:11211
+ - DJANGO_ELASTICSEARCH_HOST=es:9200
+ links:
+ - db
+ - memcached
+ depends_on:
+ - db
+ container_name: djangoblog
+
diff --git a/doc/DjangoBlog/deploy/docker-compose/docker-compose.yml b/doc/DjangoBlog/deploy/docker-compose/docker-compose.yml
new file mode 100644
index 0000000..9609af3
--- /dev/null
+++ b/doc/DjangoBlog/deploy/docker-compose/docker-compose.yml
@@ -0,0 +1,60 @@
+version: '3'
+
+services:
+ db:
+ image: mysql:latest
+ restart: always
+ environment:
+ - MYSQL_DATABASE=djangoblog
+ - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ ports:
+ - 3306:3306
+ volumes:
+ - ./bin/datas/mysql/:/var/lib/mysql
+ depends_on:
+ - redis
+ container_name: db
+
+ djangoblog:
+ build:
+ context: ../../
+ restart: always
+ command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ - ./logs:/code/djangoblog/logs
+ - ./uploads:/code/djangoblog/uploads
+ environment:
+ - DJANGO_MYSQL_DATABASE=djangoblog
+ - DJANGO_MYSQL_USER=root
+ - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ - DJANGO_MYSQL_HOST=db
+ - DJANGO_MYSQL_PORT=3306
+ - DJANGO_REDIS_URL=redis:6379
+ links:
+ - db
+ - redis
+ depends_on:
+ - db
+ container_name: djangoblog
+ nginx:
+ restart: always
+ image: nginx:latest
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./bin/nginx.conf:/etc/nginx/nginx.conf
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ links:
+ - djangoblog:djangoblog
+ container_name: nginx
+
+ redis:
+ restart: always
+ image: redis:latest
+ container_name: redis
+ ports:
+ - "6379:6379"
diff --git a/doc/DjangoBlog/deploy/entrypoint.sh b/doc/DjangoBlog/deploy/entrypoint.sh
new file mode 100644
index 0000000..2fb6491
--- /dev/null
+++ b/doc/DjangoBlog/deploy/entrypoint.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+NAME="djangoblog"
+DJANGODIR=/code/djangoblog
+USER=root
+GROUP=root
+NUM_WORKERS=1
+DJANGO_WSGI_MODULE=djangoblog.wsgi
+
+
+echo "Starting $NAME as `whoami`"
+
+cd $DJANGODIR
+
+export PYTHONPATH=$DJANGODIR:$PYTHONPATH
+
+python manage.py makemigrations && \
+ python manage.py migrate && \
+ python manage.py collectstatic --noinput && \
+ python manage.py compress --force && \
+ python manage.py build_index && \
+ python manage.py compilemessages || exit 1
+
+exec gunicorn ${DJANGO_WSGI_MODULE}:application \
+--name $NAME \
+--workers $NUM_WORKERS \
+--user=$USER --group=$GROUP \
+--bind 0.0.0.0:8000 \
+--log-level=debug \
+--log-file=- \
+--worker-class gevent \
+--threads 4
diff --git a/doc/DjangoBlog/deploy/k8s/configmap.yaml b/doc/DjangoBlog/deploy/k8s/configmap.yaml
new file mode 100644
index 0000000..835d4ad
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/configmap.yaml
@@ -0,0 +1,119 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: web-nginx-config
+ namespace: djangoblog
+data:
+ nginx.conf: |
+ user nginx;
+ worker_processes auto;
+ error_log /var/log/nginx/error.log notice;
+ pid /var/run/nginx.pid;
+
+ events {
+ worker_connections 1024;
+ multi_accept on;
+ use epoll;
+ }
+
+ http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ keepalive_timeout 65;
+ gzip on;
+ gzip_disable "msie6";
+
+ gzip_vary on;
+ gzip_proxied any;
+ gzip_comp_level 8;
+ gzip_buffers 16 8k;
+ gzip_http_version 1.1;
+ gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+
+ # Include server configurations
+ include /etc/nginx/conf.d/*.conf;
+ }
+ djangoblog.conf: |
+ server {
+ server_name lylinux.net;
+ root /code/djangoblog/collectedstatic/;
+ listen 80;
+ keepalive_timeout 70;
+ location /static/ {
+ expires max;
+ alias /code/djangoblog/collectedstatic/;
+ }
+
+ location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
+ root /resource/djangopub;
+ expires 1d;
+ access_log off;
+ error_log off;
+ }
+
+ location / {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_redirect off;
+ if (!-f $request_filename) {
+ proxy_pass http://djangoblog:8000;
+ break;
+ }
+ }
+ }
+ server {
+ server_name www.lylinux.net;
+ listen 80;
+ return 301 https://lylinux.net$request_uri;
+ }
+ resource.lylinux.net.conf: |
+ server {
+ index index.html index.htm;
+ server_name resource.lylinux.net;
+ root /resource/;
+
+ location /djangoblog/ {
+ alias /code/djangoblog/collectedstatic/;
+ }
+
+ access_log off;
+ error_log off;
+ include lylinux/resource.conf;
+ }
+ lylinux.resource.conf: |
+ expires max;
+ access_log off;
+ log_not_found off;
+ add_header Pragma public;
+ add_header Cache-Control "public";
+ add_header "Access-Control-Allow-Origin" "*";
+
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: djangoblog-env
+ namespace: djangoblog
+data:
+ DJANGO_MYSQL_DATABASE: djangoblog
+ DJANGO_MYSQL_USER: db_user
+ DJANGO_MYSQL_PASSWORD: db_password
+ DJANGO_MYSQL_HOST: db_host
+ DJANGO_MYSQL_PORT: db_port
+ DJANGO_REDIS_URL: "redis:6379"
+ DJANGO_DEBUG: "False"
+ MYSQL_ROOT_PASSWORD: db_password
+ MYSQL_DATABASE: djangoblog
+ MYSQL_PASSWORD: db_password
+ DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
diff --git a/doc/DjangoBlog/deploy/k8s/deployment.yaml b/doc/DjangoBlog/deploy/k8s/deployment.yaml
new file mode 100644
index 0000000..b50c411
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/deployment.yaml
@@ -0,0 +1,274 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: djangoblog
+ namespace: djangoblog
+ labels:
+ app: djangoblog
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: djangoblog
+ template:
+ metadata:
+ labels:
+ app: djangoblog
+ spec:
+ containers:
+ - name: djangoblog
+ image: liangliangyy/djangoblog:latest
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 8000
+ envFrom:
+ - configMapRef:
+ name: djangoblog-env
+ readinessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ livenessProbe:
+ httpGet:
+ path: /health/
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: djangoblog
+ mountPath: /code/djangoblog/collectedstatic
+ - name: resource
+ mountPath: /resource
+ volumes:
+ - name: djangoblog
+ persistentVolumeClaim:
+ claimName: djangoblog-pvc
+ - name: resource
+ persistentVolumeClaim:
+ claimName: resource-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+ namespace: djangoblog
+ labels:
+ app: redis
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ containers:
+ - name: redis
+ image: redis:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 6379
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: 200m
+ memory: 2Gi
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: db
+ namespace: djangoblog
+ labels:
+ app: db
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: db
+ template:
+ metadata:
+ labels:
+ app: db
+ spec:
+ containers:
+ - name: db
+ image: mysql:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 3306
+ envFrom:
+ - configMapRef:
+ name: djangoblog-env
+ readinessProbe:
+ exec:
+ command:
+ - mysqladmin
+ - ping
+ - "-h"
+ - "127.0.0.1"
+ - "-u"
+ - "root"
+ - "-p$MYSQL_ROOT_PASSWORD"
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ livenessProbe:
+ exec:
+ command:
+ - mysqladmin
+ - ping
+ - "-h"
+ - "127.0.0.1"
+ - "-u"
+ - "root"
+ - "-p$MYSQL_ROOT_PASSWORD"
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: db-data
+ mountPath: /var/lib/mysql
+ volumes:
+ - name: db-data
+ persistentVolumeClaim:
+ claimName: db-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx
+ namespace: djangoblog
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: nginx-config
+ mountPath: /etc/nginx/nginx.conf
+ subPath: nginx.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/conf.d/default.conf
+ subPath: djangoblog.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
+ subPath: resource.lylinux.net.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/lylinux/resource.conf
+ subPath: lylinux.resource.conf
+ - name: djangoblog-pvc
+ mountPath: /code/djangoblog/collectedstatic
+ - name: resource-pvc
+ mountPath: /resource
+ volumes:
+ - name: nginx-config
+ configMap:
+ name: web-nginx-config
+ - name: djangoblog-pvc
+ persistentVolumeClaim:
+ claimName: djangoblog-pvc
+ - name: resource-pvc
+ persistentVolumeClaim:
+ claimName: resource-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: elasticsearch
+ namespace: djangoblog
+ labels:
+ app: elasticsearch
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: elasticsearch
+ template:
+ metadata:
+ labels:
+ app: elasticsearch
+ spec:
+ containers:
+ - name: elasticsearch
+ image: liangliangyy/elasticsearch-analysis-ik:8.6.1
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: discovery.type
+ value: single-node
+ - name: ES_JAVA_OPTS
+ value: "-Xms256m -Xmx256m"
+ - name: xpack.security.enabled
+ value: "false"
+ - name: xpack.monitoring.templates.enabled
+ value: "false"
+ ports:
+ - containerPort: 9200
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 9200
+ initialDelaySeconds: 15
+ periodSeconds: 30
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 9200
+ initialDelaySeconds: 15
+ periodSeconds: 30
+ volumeMounts:
+ - name: elasticsearch-data
+ mountPath: /usr/share/elasticsearch/data/
+ volumes:
+ - name: elasticsearch-data
+ persistentVolumeClaim:
+ claimName: elasticsearch-pvc
diff --git a/doc/DjangoBlog/deploy/k8s/gateway.yaml b/doc/DjangoBlog/deploy/k8s/gateway.yaml
new file mode 100644
index 0000000..a8de073
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/gateway.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: nginx
+ namespace: djangoblog
+spec:
+ ingressClassName: nginx
+ rules:
+ - http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: nginx
+ port:
+ number: 80
\ No newline at end of file
diff --git a/doc/DjangoBlog/deploy/k8s/pv.yaml b/doc/DjangoBlog/deploy/k8s/pv.yaml
new file mode 100644
index 0000000..874b72f
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/pv.yaml
@@ -0,0 +1,94 @@
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-db
+spec:
+ capacity:
+ storage: 10Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-db
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-djangoblog
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-djangoblog
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+
+
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-resource
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/resource/
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-elasticsearch
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-elasticsearch
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
\ No newline at end of file
diff --git a/doc/DjangoBlog/deploy/k8s/pvc.yaml b/doc/DjangoBlog/deploy/k8s/pvc.yaml
new file mode 100644
index 0000000..ef238c5
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/pvc.yaml
@@ -0,0 +1,60 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: db-pvc
+ namespace: djangoblog
+spec:
+ storageClassName: local-storage
+ volumeName: local-pv-db
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: djangoblog-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-djangoblog
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: resource-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-resource
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: elasticsearch-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-elasticsearch
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/deploy/k8s/service.yaml b/doc/DjangoBlog/deploy/k8s/service.yaml
new file mode 100644
index 0000000..4ef2931
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/service.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: djangoblog
+ namespace: djangoblog
+ labels:
+ app: djangoblog
+spec:
+ selector:
+ app: djangoblog
+ ports:
+ - protocol: TCP
+ port: 8000
+ targetPort: 8000
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+ namespace: djangoblog
+ labels:
+ app: nginx
+spec:
+ selector:
+ app: nginx
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 80
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+ namespace: djangoblog
+ labels:
+ app: redis
+spec:
+ selector:
+ app: redis
+ ports:
+ - protocol: TCP
+ port: 6379
+ targetPort: 6379
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: db
+ namespace: djangoblog
+ labels:
+ app: db
+spec:
+ selector:
+ app: db
+ ports:
+ - protocol: TCP
+ port: 3306
+ targetPort: 3306
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: elasticsearch
+ namespace: djangoblog
+ labels:
+ app: elasticsearch
+spec:
+ selector:
+ app: elasticsearch
+ ports:
+ - protocol: TCP
+ port: 9200
+ targetPort: 9200
+ type: ClusterIP
+
diff --git a/doc/DjangoBlog/deploy/k8s/storageclass.yaml b/doc/DjangoBlog/deploy/k8s/storageclass.yaml
new file mode 100644
index 0000000..5d5a14c
--- /dev/null
+++ b/doc/DjangoBlog/deploy/k8s/storageclass.yaml
@@ -0,0 +1,10 @@
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+ name: local-storage
+ annotations:
+ storageclass.kubernetes.io/is-default-class: "true"
+provisioner: kubernetes.io/no-provisioner
+volumeBindingMode: Immediate
+
+
diff --git a/doc/DjangoBlog/deploy/nginx.conf b/doc/DjangoBlog/deploy/nginx.conf
new file mode 100644
index 0000000..32161d8
--- /dev/null
+++ b/doc/DjangoBlog/deploy/nginx.conf
@@ -0,0 +1,50 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log notice;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ server {
+ root /code/djangoblog/collectedstatic/;
+ listen 80;
+ keepalive_timeout 70;
+ location /static/ {
+ expires max;
+ alias /code/djangoblog/collectedstatic/;
+ }
+ location / {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_redirect off;
+ if (!-f $request_filename) {
+ proxy_pass http://djangoblog:8000;
+ break;
+ }
+ }
+ }
+}
diff --git a/doc/DjangoBlog/djangoblog/.idea/.gitignore b/doc/DjangoBlog/djangoblog/.idea/.gitignore
new file mode 100644
index 0000000..10b731c
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/.gitignore
@@ -0,0 +1,5 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/doc/DjangoBlog/djangoblog/.idea/djangoblog.iml b/doc/DjangoBlog/djangoblog/.idea/djangoblog.iml
new file mode 100644
index 0000000..460d402
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/djangoblog.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/.idea/inspectionProfiles/profiles_settings.xml b/doc/DjangoBlog/djangoblog/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/.idea/misc.xml b/doc/DjangoBlog/djangoblog/.idea/misc.xml
new file mode 100644
index 0000000..23231ce
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/.idea/modules.xml b/doc/DjangoBlog/djangoblog/.idea/modules.xml
new file mode 100644
index 0000000..055e82a
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/.idea/vcs.xml b/doc/DjangoBlog/djangoblog/.idea/vcs.xml
new file mode 100644
index 0000000..c2365ab
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/__init__.py b/doc/DjangoBlog/djangoblog/__init__.py
new file mode 100644
index 0000000..288702f
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/__init__.py
@@ -0,0 +1,7 @@
+#wr 配置当前应用的默认配置类
+# 当Django加载该应用时,若未在INSTALLED_APPS中显式指定配置类,会自动使用此处指定的配置类
+# 'djangoblog.apps.DjangoblogAppConfig' 表示配置类的完整路径:
+# - djangoblog:应用名称
+# - apps:存放配置类的模块
+# - DjangoblogAppConfig:具体的配置类(通常包含应用初始化、信号注册等逻辑)
+default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/admin_site.py b/doc/DjangoBlog/djangoblog/admin_site.py
new file mode 100644
index 0000000..bf1af55
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/admin_site.py
@@ -0,0 +1,89 @@
+from django.contrib.admin import AdminSite
+from django.contrib.admin.models import LogEntry
+from django.contrib.sites.admin import SiteAdmin
+from django.contrib.sites.models import Site
+
+from accounts.admin import *
+from blog.admin import *
+from blog.models import *
+from comments.admin import *
+from comments.models import *
+from djangoblog.logentryadmin import LogEntryAdmin
+from oauth.admin import *
+from oauth.models import *
+from owntracks.admin import *
+from owntracks.models import *
+from servermanager.admin import *
+from servermanager.models import *
+from django.contrib.admin import AdminSite #wr 导入Django内置的AdminSite基类,用于构建管理后台
+
+
+class DjangoBlogAdminSite(AdminSite):
+ """
+ 自定义管理站点类,继承自Django的AdminSite
+ 用于个性化配置博客系统的管理后台(如标题、权限控制等)
+ """
+ #wr 管理后台页面顶部的标题(显示在登录页和管理首页的顶部横幅)
+ site_header = 'djangoblog administration'
+ #wr 管理后台的页面标题(显示在浏览器标签页上)
+ site_title = 'djangoblog site admin'
+
+ def __init__(self, name='admin'):
+ """
+ 初始化方法,调用父类的初始化逻辑
+ :param name: 管理站点的标识名称,默认使用'admin'(与Django默认管理站点名称一致,便于路由匹配)
+ """
+ super().__init__(name)
+
+ def has_permission(self, request):
+ """
+ 控制访问管理后台的权限校验
+ 仅允许超级用户(is_superuser为True)访问管理后台
+ :param request: 当前HTTP请求对象,包含用户信息等
+ :return: 布尔值,True表示有权限访问,False表示无权限
+ """
+ return request.user.is_superuser
+
+#wr 创建自定义管理站点的实例,名称为'admin'(与Django默认管理站点标识一致,便于在urls.py中配置路由)
+admin_site = DjangoBlogAdminSite(name='admin')
+
+#wr 以下为注册模型到自定义管理站点:
+#wr 将数据模型与对应的Admin配置类绑定,使模型在管理后台可见并可操作
+
+#wr 注册文章模型(Article)及其实例配置类(ArticlelAdmin),用于管理博客文章
+admin_site.register(Article, ArticlelAdmin)
+#wr 注册分类模型(Category)及其实例配置类(CategoryAdmin),用于管理文章分类
+admin_site.register(Category, CategoryAdmin)
+#wr 注册标签模型(Tag)及其实例配置类(TagAdmin),用于管理文章标签
+admin_site.register(Tag, TagAdmin)
+#wr 注册链接模型(Links)及其实例配置类(LinksAdmin),用于管理友情链接
+admin_site.register(Links, LinksAdmin)
+#wr 注册侧边栏模型(SideBar)及其实例配置类(SideBarAdmin),用于管理网站侧边栏内容
+admin_site.register(SideBar, SideBarAdmin)
+#wr 注册博客设置模型(BlogSettings)及其实例配置类(BlogSettingsAdmin),用于管理博客全局设置
+admin_site.register(BlogSettings, BlogSettingsAdmin)
+
+#wr 注册命令日志模型(commands)及其实例配置类(CommandsAdmin),用于管理系统命令执行记录
+admin_site.register(commands, CommandsAdmin)
+#wr 注册邮件发送日志模型(EmailSendLog)及其实例配置类(EmailSendLogAdmin),用于管理邮件发送记录
+admin_site.register(EmailSendLog, EmailSendLogAdmin)
+
+#wr 注册自定义用户模型(BlogUser)及其实例配置类(BlogUserAdmin),用于管理网站用户
+admin_site.register(BlogUser, BlogUserAdmin)
+
+#wr 注册评论模型(Comment)及其实例配置类(CommentAdmin),用于管理文章评论
+admin_site.register(Comment, CommentAdmin)
+
+#wr 注册第三方登录用户模型(OAuthUser)及其实例配置类(OAuthUserAdmin),用于管理第三方登录用户
+admin_site.register(OAuthUser, OAuthUserAdmin)
+#wr 注册第三方登录配置模型(OAuthConfig)及其实例配置类(OAuthConfigAdmin),用于管理第三方登录平台配置
+admin_site.register(OAuthConfig, OAuthConfigAdmin)
+
+#wr 注册位置追踪日志模型(OwnTrackLog)及其实例配置类(OwnTrackLogsAdmin),用于管理位置追踪记录
+admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
+
+#wr 注册站点模型(Site)及其实例配置类(SiteAdmin),用于管理多站点配置(Django自带的sites框架)
+admin_site.register(Site, SiteAdmin)
+
+#wr 注册操作日志模型(LogEntry)及其实例配置类(LogEntryAdmin),用于管理用户在管理后台的操作记录
+admin_site.register(LogEntry, LogEntryAdmin)
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/apps.py b/doc/DjangoBlog/djangoblog/apps.py
new file mode 100644
index 0000000..76be973
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/apps.py
@@ -0,0 +1,29 @@
+#wr 从Django的apps模块导入AppConfig类,该类是所有应用配置的基类
+from django.apps import AppConfig
+
+
+class DjangoblogAppConfig(AppConfig):
+ """
+ wr 自定义应用配置类,用于配置'djangoblog'应用的行为
+ 继承自Django的AppConfig,可自定义应用初始化、信号注册、插件加载等逻辑
+ """
+ #wr 指定模型默认的自增字段类型(Django 3.2+新增配置)
+ #wr BigAutoField是大整数类型的自增字段,可支持更大范围的ID值,避免整数溢出
+ default_auto_field = 'django.db.models.BigAutoField'
+
+ #wr 应用的名称,必须与应用的目录名一致,Django通过此名称识别应用
+ name = 'djangoblog'
+
+ def ready(self):
+ """
+ wr 应用就绪时执行的方法(Django加载完应用后自动调用)
+ 通常用于执行初始化操作,如注册信号、加载插件、启动定时任务等
+ """
+ #wr 调用父类的ready方法,确保基类的初始化逻辑正常执行
+ super().ready()
+
+ #wr 导入并加载插件(应用就绪后加载插件,确保插件依赖的资源已初始化)
+ #wr 从当前应用的plugin_manage.loader模块导入load_plugins函数
+ from .plugin_manage.loader import load_plugins
+ #wr 执行插件加载函数,完成插件的注册和初始化
+ load_plugins()
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/blog_signals.py b/doc/DjangoBlog/djangoblog/blog_signals.py
new file mode 100644
index 0000000..1863d04
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/blog_signals.py
@@ -0,0 +1,177 @@
+import _thread # 导入线程模块,用于异步执行任务(如发送邮件避免阻塞主进程)
+import logging # 导入日志模块,用于记录系统运行信息和错误
+
+import django.dispatch # 导入Django信号工具,用于定义和处理自定义信号
+from django.conf import settings # 导入Django项目配置
+from django.contrib.admin.models import LogEntry # 导入管理员操作日志模型
+from django.contrib.auth.signals import user_logged_in, user_logged_out # 导入用户登录/登出内置信号
+from django.core.mail import EmailMultiAlternatives # 导入Django邮件工具(支持HTML内容)
+from django.db.models.signals import post_save # 导入模型保存后触发的内置信号
+from django.dispatch import receiver # 导入信号接收器装饰器
+
+from comments.models import Comment # 导入评论模型
+from comments.utils import send_comment_email # 导入发送评论通知邮件的工具函数
+from djangoblog.spider_notify import SpiderNotify # 导入搜索引擎通知工具(如百度收录推送)
+from djangoblog.utils import ( # 导入项目自定义工具函数
+ cache, # 缓存操作工具
+ expire_view_cache, # 清除视图缓存
+ delete_sidebar_cache, # 清除侧边栏缓存
+ delete_view_cache # 清除指定视图缓存
+)
+from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具
+from oauth.models import OAuthUser # 导入第三方登录用户模型
+
+#wr 初始化日志记录器(指定记录器名称为当前模块)
+logger = logging.getLogger(__name__)
+
+#wr 定义自定义信号:第三方登录用户登录信号(携带用户ID参数)
+oauth_user_login_signal = django.dispatch.Signal(['id'])
+#wr 定义自定义信号:发送邮件信号(携带收件人、标题、内容参数)
+send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content'])
+
+
+@receiver(send_email_signal)
+def send_email_signal_handler(sender, **kwargs):
+ """
+ wr发送邮件信号的接收器:处理邮件发送逻辑并记录发送日志
+ 当send_email_signal信号触发时,自动执行此函数
+ """
+ #wr 从信号参数中获取邮件信息
+ emailto = kwargs['emailto'] #wr 收件人列表
+ title = kwargs['title'] #wr 邮件标题
+ content = kwargs['content'] #wr 邮件内容(HTML格式)
+
+ #wr 创建邮件对象:指定标题、内容、发件人(从配置获取)、收件人
+ msg = EmailMultiAlternatives(
+ title,
+ content,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=emailto
+ )
+ msg.content_subtype = "html" #wr 声明邮件内容为HTML格式
+
+ #wr 初始化邮件发送日志记录
+ from servermanager.models import EmailSendLog #wr 导入邮件发送日志模型
+ log = EmailSendLog()
+ log.title = title #wr 记录邮件标题
+ log.content = content #wr 记录邮件内容
+ log.emailto = ','.join(emailto) #wr 记录收件人(用逗号拼接列表)
+
+ try:
+ #wr 发送邮件:返回成功发送的数量
+ result = msg.send()
+ log.send_result = result > 0 #wr 发送成功标识(大于0表示至少成功发送1封)
+ except Exception as e:
+ #wr 记录发送失败日志
+ logger.error(f"邮件发送失败,收件人: {emailto}, 错误: {e}")
+ log.send_result = False #wr 标记发送失败
+ log.save() #wr 保存日志记录到数据库
+
+
+@receiver(oauth_user_login_signal)
+def oauth_user_login_signal_handler(sender, **kwargs):
+ """
+ wr 第三方登录用户登录信号的接收器:处理用户头像本地化及缓存清理
+ 当oauth_user_login_signal信号触发时,自动执行此函数
+ """
+ #wr 从信号参数中获取第三方用户ID
+ id = kwargs['id']
+ #wr 查询对应的第三方用户对象
+ oauthuser = OAuthUser.objects.get(id=id)
+ #wr 获取当前站点域名(用于判断头像是否为本地地址)
+ site = get_current_site().domain
+
+ #wr 若用户头像存在且不是本地地址(未包含当前站点域名),则本地化头像
+ if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
+ from djangoblog.utils import save_user_avatar #wr 导入头像保存工具
+ #wr 下载并保存头像到本地,返回本地头像URL
+ oauthuser.picture = save_user_avatar(oauthuser.picture)
+ oauthuser.save() #wr 更新用户信息
+
+ #wr 清除侧边栏缓存(用户信息可能影响侧边栏显示,如登录状态)
+ delete_sidebar_cache()
+
+
+@receiver(post_save)
+def model_post_save_callback(
+ sender,
+ instance,
+ created,
+ raw,
+ using,
+ update_fields,** kwargs):
+ """
+ wr 模型保存后信号的接收器:处理模型保存后的后续操作(如通知搜索引擎、清理缓存等)
+ 当任意模型执行save()后,自动触发此函数
+ """
+ clearcache = False # 缓存清理标识
+
+ #wr 忽略管理员操作日志模型(避免递归触发或无效处理)
+ if isinstance(instance, LogEntry):
+ return
+
+ #wr 若模型实例有get_full_url方法(通常为可访问的内容模型,如文章)
+ if 'get_full_url' in dir(instance):
+ #wr 判断是否仅更新了浏览量字段(避免不必要的操作)
+ is_update_views = update_fields == {'views'}
+ #wr 非测试环境且非仅更新浏览量时,通知搜索引擎(如百度)更新收录
+ if not settings.TESTING and not is_update_views:
+ try:
+ notify_url = instance.get_full_url() # 获取模型实例的完整URL
+ SpiderNotify.baidu_notify([notify_url]) # 通知百度收录该URL
+ except Exception as ex:
+ logger.error("通知搜索引擎失败", ex) # 记录通知失败日志
+
+ #wr 非仅更新浏览量时,标记需要清理缓存
+ if not is_update_views:
+ clearcache = True
+
+ #wr 若保存的是评论模型实例
+ if isinstance(instance, Comment):
+ #wr 仅处理已启用的评论
+ if instance.is_enable:
+ #wr 获取评论所属文章的URL
+ path = instance.article.get_absolute_url()
+ #wr 获取当前站点域名(处理端口号,避免缓存键错误)
+ site = get_current_site().domain
+ if site.find(':') > 0:
+ site = site[0:site.find(':')] # 移除端口号,仅保留域名
+
+ #wr 清除文章详情页的视图缓存(评论更新后需刷新页面)
+ expire_view_cache(
+ path,
+ servername=site,
+ serverport=80,
+ key_prefix='blogdetail'
+ )
+ #wr 清除SEO处理器缓存(评论可能影响页面元数据)
+ if cache.get('seo_processor'):
+ cache.delete('seo_processor')
+ #wr 清除该文章的评论缓存
+ comment_cache_key = f'article_comments_{instance.article.id}'
+ cache.delete(comment_cache_key)
+ #wr 清除侧边栏缓存(侧边栏可能显示最新评论)
+ delete_sidebar_cache()
+ #wr 清除评论列表视图的缓存
+ delete_view_cache('article_comments', [str(instance.article.pk)])
+
+ #wr 启动新线程发送评论通知邮件(异步执行,避免阻塞主请求)
+ _thread.start_new_thread(send_comment_email, (instance,))
+
+ #wr 若标记需要清理缓存,则清除全局缓存
+ if clearcache:
+ cache.clear()
+
+
+@receiver(user_logged_in) #wr 绑定用户登录信号
+@receiver(user_logged_out) #wr 绑定用户登出信号
+def user_auth_callback(sender, request, user, **kwargs):
+ """
+ wr 用户登录/登出信号的接收器:处理用户身份变更后的缓存清理
+ 当用户登录或登出时,自动触发此函数
+ """
+ #wr 若用户存在且用户名有效
+ if user and user.username:
+ logger.info(user) # 记录用户登录/登出日志
+ delete_sidebar_cache() # 清除侧边栏缓存(侧边栏可能显示用户状态)
+ #wr cache.clear() # 注释:可选清除全局缓存(视业务需求启用)
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/elasticsearch_backend.py b/doc/DjangoBlog/djangoblog/elasticsearch_backend.py
new file mode 100644
index 0000000..aa6e889
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/elasticsearch_backend.py
@@ -0,0 +1,291 @@
+#wr 导入必要模块
+from django.utils.encoding import force_str #wr 用于字符串编码转换
+from elasticsearch_dsl import Q #wr Elasticsearch查询构建工具
+#wr 导入Haystack核心组件:引擎、后端、查询基类及日志工具
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
+from haystack.forms import ModelSearchForm #wr Haystack默认搜索表单
+from haystack.models import SearchResult #wr Haystack搜索结果封装类
+from haystack.utils import log as logging #wr Haystack日志工具
+
+#wr 导入项目自定义的Elasticsearch文档和文档管理器(用于文章索引管理)
+from blog.documents import ArticleDocument, ArticleDocumentManager
+from blog.models import Article #wr 博客文章模型
+
+
+#wr 初始化日志记录器
+logger = logging.getLogger(__name__)
+
+
+class ElasticSearchBackend(BaseSearchBackend):
+ """
+ wr自定义Elasticsearch搜索后端,继承自Haystack的BaseSearchBackend
+ 负责与Elasticsearch交互,处理索引创建、更新、删除及搜索逻辑
+ """
+
+ def __init__(self, connection_alias, **connection_options):
+ """
+ wr初始化方法:设置Elasticsearch连接参数及文档管理器
+ :param connection_alias: 连接别名(用于多后端区分)
+ :param connection_options: 连接选项(如主机、端口等)
+ """
+ super(ElasticSearchBackend, self).__init__(connection_alias,** connection_options)
+ self.manager = ArticleDocumentManager() #wr 初始化文章文档管理器(处理索引操作)
+ self.include_spelling = True #wr 启用拼写建议功能
+
+ def _get_models(self, iterable):
+ """
+ wr将模型实例转换为Elasticsearch文档对象
+ :param iterable: 模型实例列表(如文章列表)
+ :return: 转换后的Elasticsearch文档列表
+ """
+ #wr 若输入为空,默认获取所有文章;否则使用输入的模型实例
+ models = iterable if iterable and iterable[0] else Article.objects.all()
+ #wr 调用文档管理器将模型转换为文档
+ docs = self.manager.convert_to_doc(models)
+ return docs
+
+ def _create(self, models):
+ """
+ wr创建索引并初始化数据
+ :param models: 需要索引的模型实例列表
+ """
+ self.manager.create_index() #wr 创建Elasticsearch索引
+ docs = self._get_models(models) #wr 转换模型为文档
+ self.manager.rebuild(docs) #wr 重建索引(全量覆盖)
+
+ def _delete(self, models):
+ """
+ wr从索引中删除模型对应的文档
+ :param models: 需要删除的模型实例列表
+ :return: 操作结果(True表示成功)
+ """
+ for m in models:
+ m.delete() #wr 调用文档的删除方法(实际由ArticleDocument实现)
+ return True
+
+ def _rebuild(self, models):
+ """
+ wr重建索引(增量更新)
+ :param models: 需要更新的模型实例列表(为空则更新所有文章)
+ """
+ models = models if models else Article.objects.all() #wr 处理空输入
+ docs = self.manager.convert_to_doc(models) #wr 转换模型为文档
+ self.manager.update_docs(docs) #wr 增量更新索引
+
+ def update(self, index, iterable, commit=True):
+ """
+ wr(Haystack接口)更新索引(用于Haystack的信号触发更新)
+ :param index: 索引名称(当前实现未使用)
+ :param iterable: 需更新的模型实例列表
+ :param commit: 是否立即提交(当前实现未使用)
+ """
+ models = self._get_models(iterable) #wr 转换模型为文档
+ self.manager.update_docs(models) #wr 执行更新
+
+ def remove(self, obj_or_string):
+ """
+ wr(Haystack接口)从索引中移除对象
+ :param obj_or_string: 模型实例或标识字符串
+ """
+ models = self._get_models([obj_or_string]) #wr 转换为文档
+ self._delete(models) #wr 执行删除
+
+ def clear(self, models=None, commit=True):
+ """
+ wr(Haystack接口)清空索引
+ :param models: 需清空的模型类(当前实现未使用,默认清空所有)
+ :param commit: 是否立即提交(当前实现未使用)
+ """
+ self.remove(None) #wr 调用删除方法清空
+
+ @staticmethod
+ def get_suggestion(query: str) -> str:
+ """
+ wr获取搜索建议词(基于Elasticsearch的term suggest功能)
+ :param query: 原始搜索词
+ :return: 推荐的搜索词(多个词用空格拼接)
+ """
+ #wr 构建搜索:匹配文章内容,并添加拼写建议
+ search = ArticleDocument.search() \
+ .query("match", body=query) \
+ .suggest('suggest_search', query, term={'field': 'body'}) \
+ .execute()
+
+ keywords = []
+ #wr 提取建议结果:若有建议则取第一个,否则保留原词
+ for suggest in search.suggest.suggest_search:
+ if suggest["options"]:
+ keywords.append(suggest["options"][0]["text"])
+ else:
+ keywords.append(suggest["text"])
+
+ return ' '.join(keywords) # 拼接建议词为字符串
+
+ @log_query #wr Haystack装饰器:记录查询日志
+ def search(self, query_string, **kwargs):
+ """
+ wr(核心方法)执行搜索查询
+ :param query_string: 搜索关键词
+ :param kwargs: 额外参数(如分页偏移量、过滤条件等)
+ :return: 搜索结果字典(包含结果列表、总命中数、建议词等)
+ """
+ logger.info('search query_string:' + query_string) #wr 记录搜索关键词
+
+ #wr 获取分页参数(起始偏移量和结束偏移量)
+ start_offset = kwargs.get('start_offset', 0)
+ end_offset = kwargs.get('end_offset', 10) #wr 默认返回10条结果
+
+
+ #wr 处理搜索建议:若启用建议模式,则获取推荐词;否则使用原词
+ if getattr(self, "is_suggest", None):
+ suggestion = self.get_suggestion(query_string)
+ else:
+ suggestion = query_string
+
+ #wr 构建Elasticsearch查询:
+ #wr 1. 布尔查询,匹配标题或内容(至少70%匹配度)
+ #wr 2. 过滤条件:状态为已发布(status='p')、类型为文章(type='a')
+ q = Q('bool',
+ should=[Q('match', body=suggestion), Q('match', title=suggestion)], #wr 匹配标题或内容
+ minimum_should_match="70%") #wr 最小匹配度
+
+ #wr 执行搜索:排除原始文档内容(source=False),应用分页
+ search = ArticleDocument.search() \
+ .query('bool', filter=[q]) \
+ .filter('term', status='p') \
+ .filter('term', type='a') \
+ .source(False)[start_offset: end_offset]
+
+ results = search.execute() #wr 执行查询
+ hits = results['hits'].total #wr 总命中数
+ raw_results = []
+
+ #wr 转换Elasticsearch结果为Haystack的SearchResult对象(适配Haystack接口)
+ for raw_result in results['hits']['hits']:
+ app_label = 'blog' #wr 应用标签(文章属于blog应用)
+ model_name = 'Article' #wr 模型名称
+ additional_fields = {} #wr 额外字段(当前未使用)
+
+ #wr 构建Haystack搜索结果对象
+ result = SearchResult(
+ app_label,
+ model_name,
+ raw_result['_id'], #wr 文档ID(对应文章ID)
+ raw_result['_score'], #wr 匹配分数
+ **additional_fields)
+ raw_results.append(result)
+
+ #wr 准备返回数据:结果列表、总命中数、空 facets(暂未实现)、拼写建议
+ facets = {}
+ spelling_suggestion = None if query_string == suggestion else suggestion #wr 若建议词与原词不同则返回
+
+ return {
+ 'results': raw_results,
+ 'hits': hits,
+ 'facets': facets,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+
+class ElasticSearchQuery(BaseSearchQuery):
+ """
+ wr自定义搜索查询类,继承自Haystack的BaseSearchQuery
+ 负责处理查询参数解析、查询构建等逻辑
+ """
+
+ def _convert_datetime(self, date):
+ """
+ wr转换日期时间为Elasticsearch支持的格式
+ :param date: 日期时间对象
+ :return: 格式化的字符串(如20231001123000)
+ """
+ if hasattr(date, 'hour'): #wr 包含小时信息(datetime)
+ return force_str(date.strftime('%Y%m%d%H%M%S'))
+ else: #wr 仅日期(date),默认时间为00:00:00
+ return force_str(date.strftime('%Y%m%d000000'))
+
+ def clean(self, query_fragment):
+ """
+ wr 清理查询片段(用户输入),处理保留词和特殊字符
+ :param query_fragment: 原始查询片段
+ :return: 清理后的查询字符串
+ """
+ words = query_fragment.split() #wr 拆分关键词
+ cleaned_words = []
+
+ for word in words:
+ #wr 处理保留词(转为小写,避免冲突)
+ if word in self.backend.RESERVED_WORDS:
+ word = word.replace(word, word.lower())
+
+ #wr 处理特殊字符:若包含保留字符则用单引号包裹
+ for char in self.backend.RESERVED_CHARACTERS:
+ if char in word:
+ word = f"'{word}'"
+ break
+
+ cleaned_words.append(word)
+
+ return ' '.join(cleaned_words) #wr 拼接清理后的关键词
+
+ def build_query_fragment(self, field, filter_type, value):
+ """
+ wr构建查询片段(适配Haystack的查询构建逻辑)
+ :param field: 查询字段
+ :param filter_type: 过滤类型
+ :param value: 查询值
+ :return: 查询字符串
+ """
+ return value.query_string #wr 直接返回查询字符串(简化实现)
+
+ def get_count(self):
+ """
+ wr获取搜索结果总数
+ :return: 结果数量
+ """
+ results = self.get_results()
+ return len(results) if results else 0
+
+ def get_spelling_suggestion(self, preferred_query=None):
+ """
+ wr获取拼写建议(适配Haystack接口)
+ :param preferred_query: 优先查询词(未使用)
+ :return: 建议词
+ """
+ return self._spelling_suggestion #wr 返回搜索后端生成的建议词
+
+ def build_params(self, spelling_query=None):
+ """
+ wr 构建查询参数(适配Haystack接口)
+ :param spelling_query: 拼写建议查询词
+ :return: 参数字典
+ """
+ kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
+ return kwargs
+
+
+class ElasticSearchModelSearchForm(ModelSearchForm):
+ """
+ wr 自定义搜索表单,继承自Haystack的ModelSearchForm
+ 扩展搜索功能,支持搜索建议开关
+ """
+
+ def search(self):
+ """
+ wr 执行搜索(重写父类方法,添加建议模式控制)
+ :return: 搜索结果集(SearchQuerySet)
+ """
+ #wr 根据表单参数设置是否启用搜索建议("is_suggest"为"no"时禁用)
+ self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
+ #wr 调用父类方法执行搜索
+ sqs = super().search()
+ return sqs
+
+
+class ElasticSearchEngine(BaseEngine):
+ """
+ wr 自定义Elasticsearch引擎类,继承自Haystack的BaseEngine
+ 关联后端和查询类,作为Haystack的引擎入口
+ """
+ backend = ElasticSearchBackend #wr 指定使用的搜索后端
+ query = ElasticSearchQuery #wr 指定使用的查询类
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/feeds.py b/doc/DjangoBlog/djangoblog/feeds.py
new file mode 100644
index 0000000..aae2485
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/feeds.py
@@ -0,0 +1,97 @@
+#wr 导入必要模块
+from django.contrib.auth import get_user_model #wr 获取项目自定义的用户模型(避免直接引用User类)
+from django.contrib.syndication.views import Feed #wr Django内置的RSS/Atom订阅生成基类
+from django.utils import timezone #wr 处理时间相关操作(用于生成版权信息中的年份)
+from django.utils.feedgenerator import Rss201rev2Feed #wr RSS 2.0标准格式生成器
+from blog.models import Article #wr 博客文章模型(RSS订阅的核心内容来源)
+from djangoblog.utils import CommonMarkdown #wr 自定义Markdown转换工具(将Markdown转为HTML)
+
+
+class DjangoBlogFeed(Feed):
+ """
+ wr自定义RSS订阅生成类,继承自Django的Feed基类
+ 用于生成博客文章的RSS订阅内容,支持标准RSS 2.0格式
+ 访问路径为/feed/,用户可通过订阅工具获取最新文章推送
+ """
+ #wr 指定RSS订阅的格式版本:采用RSS 2.0标准(兼容性最广)
+ feed_type = Rss201rev2Feed
+
+ #wr RSS订阅的描述信息(将显示在订阅工具的描述栏)
+ description = '大巧无工,重剑无锋.'
+ #wr RSS订阅的标题(显示在订阅工具的标题栏)
+ title = "且听风吟 大巧无工,重剑无锋. "
+ #wr RSS订阅的访问URL路径(与urls.py中配置的路由一致)
+ link = "/feed/"
+
+ def author_name(self):
+ """
+ wr订阅内容的作者名称
+ 这里取项目中第一个用户的昵称(默认作者,可根据实际需求修改)
+ :return: 作者昵称字符串
+ """
+ return get_user_model().objects.first().nickname
+
+ def author_link(self):
+ """
+ wr作者的个人页面链接
+ 调用用户模型的get_absolute_url方法,生成作者个人主页URL
+ :return: 作者个人页面的绝对URL
+ """
+ return get_user_model().objects.first().get_absolute_url()
+
+ def items(self):
+ """
+ wrRSS订阅的核心内容列表(即要推送的文章)
+ 过滤条件:类型为文章(type='a')、状态为已发布(status='p')
+ 排序规则:按发布时间倒序(最新文章优先)
+ 数量限制:仅取前5篇(避免订阅内容过多)
+ :return: 筛选后的文章查询集
+ """
+ return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
+
+ def item_title(self, item):
+ """
+ wr单个订阅项(文章)的标题
+ 直接使用文章自身的标题
+ :param item: 单个Article模型实例
+ :return: 文章标题字符串
+ """
+ return item.title
+
+ def item_description(self, item):
+ """
+ wr单个订阅项(文章)的描述内容
+ 将文章的Markdown格式正文转换为HTML(RSS支持HTML格式,确保排版正常)
+ :param item: 单个Article模型实例
+ :return: 转换后的HTML格式文章内容
+ """
+ return CommonMarkdown.get_markdown(item.body)
+
+ def feed_copyright(self):
+ """
+ wr订阅内容的版权声明
+ 动态获取当前年份,生成格式为"Copyright© 年份 且听风吟"的版权信息
+ :return: 版权声明字符串
+ """
+ now = timezone.now()
+ return "Copyright© {year} 且听风吟".format(year=now.year)
+
+ def item_link(self, item):
+ """
+ wr单个订阅项(文章)的访问链接
+ 调用文章模型的get_absolute_url方法,生成文章详情页的绝对URL
+ :param item: 单个Article模型实例
+ :return: 文章详情页的绝对URL
+ """
+ return item.get_absolute_url()
+
+ def item_guid(self, item):
+ """
+ wr单个订阅项(文章)的唯一标识(GUID)
+ 用于订阅工具区分不同文章,避免重复推送
+ 注:原代码未完成实现,建议返回文章的唯一标识(如文章ID+URL组合或绝对URL)
+ 示例实现:return f"{item.get_absolute_url()}?id={item.id}"
+ :param item: 单个Article模型实例
+ :return: 文章的唯一标识字符串
+ """
+ return #wr 原代码未完成,需根据实际需求补充唯一标识逻辑
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/logentryadmin.py b/doc/DjangoBlog/djangoblog/logentryadmin.py
new file mode 100644
index 0000000..8cb90af
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/logentryadmin.py
@@ -0,0 +1,137 @@
+from django.contrib import admin # 导入Django Admin核心模块
+from django.contrib.admin.models import DELETION # 导入表示"删除"操作的常量
+from django.contrib.contenttypes.models import ContentType # 导入内容类型模型(用于关联不同模型)
+from django.urls import reverse, NoReverseMatch # 导入URL反向解析工具及异常
+from django.utils.encoding import force_str # 用于字符串编码转换(兼容Python 2/3)
+from django.utils.html import escape # 用于HTML转义(防止XSS攻击)
+from django.utils.safestring import mark_safe # 标记安全的HTML字符串(允许在模板中渲染)
+from django.utils.translation import gettext_lazy as _ # 国际化翻译工具
+
+
+class LogEntryAdmin(admin.ModelAdmin):
+ """
+ wr自定义LogEntry模型的Admin配置类
+ LogEntry是Django自带的模型,用于记录管理员在后台的操作日志(如新增、修改、删除对象)
+ 此类控制日志在Admin后台的显示、搜索、过滤及操作权限
+ """
+
+ #wr 列表页的过滤条件(右侧过滤器):按内容类型(即操作的模型类型)过滤
+ list_filter = [
+ 'content_type'
+ ]
+
+ #wr 搜索字段:可通过对象名称(object_repr)和操作描述(change_message)搜索日志
+ search_fields = [
+ 'object_repr',
+ 'change_message'
+ ]
+
+ #wr 列表页中可点击的字段(点击跳转到日志详情页)
+ list_display_links = [
+ 'action_time', #wr 操作时间
+ 'get_change_message', #wr 操作描述(自定义方法)
+ ]
+
+ #wr 列表页显示的字段(按顺序排列)
+ list_display = [
+ 'action_time', #wr 操作时间
+ 'user_link', #wr 操作人(带链接的自定义字段)
+ 'content_type', #wr 操作的模型类型(如文章、用户等)
+ 'object_link', #wr 操作的对象(带链接的自定义字段)
+ 'get_change_message', #wr 操作描述(Django原生方法,返回格式化的操作信息)
+ ]
+
+ def has_add_permission(self, request):
+ """
+ wr控制是否允许添加日志条目:返回False,禁止手动添加日志
+ 原因:日志是系统自动记录的,不允许人工干预
+ """
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ """
+ wr控制是否允许修改日志条目:仅允许超级用户或有修改权限的用户以非POST方式访问(即仅查看)
+ 原因:日志记录应保持原始性,禁止修改
+ """
+ return (
+ request.user.is_superuser or #wr 超级用户有权限
+ request.user.has_perm('admin.change_logentry') #wr 有明确权限的用户
+ ) and request.method != 'POST' #wr 禁止POST请求(即禁止提交修改)
+
+ def has_delete_permission(self, request, obj=None):
+ """
+ wr控制是否允许删除日志条目:返回False,禁止删除日志
+ 原因:日志是系统操作记录,需长期保存用于审计
+ """
+ return False
+
+ def object_link(self, obj):
+ """
+ wr自定义字段:显示操作对象的链接(若存在)
+ 功能:如果操作不是删除,且能获取到内容类型,尝试生成对象的Admin修改页链接
+ """
+ object_link = escape(obj.object_repr) #wr 转义对象名称(防止XSS)
+ content_type = obj.content_type #wr 获取操作的模型类型
+
+ #wr 若操作不是删除,且内容类型存在(即有对应的模型)
+ if obj.action_flag != DELETION and content_type is not None:
+ try:
+ #wr 生成对象在Admin中的修改页URL(格式:admin:应用名_模型名_change)
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.object_id] #wr 传入对象ID
+ )
+ #wr 生成带链接的对象名称
+ object_link = '{} '.format(url, object_link)
+ except NoReverseMatch:
+ #wr 若URL反向解析失败(如模型未注册到Admin),则仅显示对象名称
+ pass
+ return mark_safe(object_link) #wr 标记为安全HTML,允许在页面渲染链接
+
+ #wr 配置自定义字段的排序和显示名称
+ object_link.admin_order_field = 'object_repr' #wr 允许按对象名称排序
+ object_link.short_description = _('object') #wr 列表页表头显示名称(支持国际化)
+
+ def user_link(self, obj):
+ """
+ wr自定义字段:显示操作人的链接(跳转到用户的Admin修改页)
+ """
+ #wr 获取操作人(User模型)的内容类型
+ content_type = ContentType.objects.get_for_model(type(obj.user))
+ user_link = escape(force_str(obj.user)) #wr 转义用户名并确保为字符串
+
+ try:
+ #wr 生成用户在Admin中的修改页URL
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.user.pk] #wr 传入用户ID
+ )
+ #wr 生成带链接的用户名
+ user_link = '{} '.format(url, user_link)
+ except NoReverseMatch:
+ #wr 若URL解析失败,仅显示用户名
+ pass
+ return mark_safe(user_link) #wr 标记为安全HTML
+
+ #wr 配置自定义字段的排序和显示名称
+ user_link.admin_order_field = 'user' #wr 允许按用户排序
+ user_link.short_description = _('user') #wr 列表页表头显示名称(支持国际化)
+
+ def get_queryset(self, request):
+ """
+ wr优化查询集:预加载content_type关联数据,减少数据库查询次数
+ 提升列表页加载性能(避免N+1查询问题)
+ """
+ queryset = super(LogEntryAdmin, self).get_queryset(request)
+ return queryset.prefetch_related('content_type') #wr 预加载content_type
+
+ def get_actions(self, request):
+ """
+ wr移除"删除选中项"操作:确保日志无法通过批量操作删除
+ """
+ actions = super(LogEntryAdmin, self).get_actions(request)
+ if 'delete_selected' in actions:
+ del actions['delete_selected'] #wr 删除批量删除操作
+ return actions
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/plugin_manage/base_plugin.py b/doc/DjangoBlog/djangoblog/plugin_manage/base_plugin.py
new file mode 100644
index 0000000..e69f740
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/plugin_manage/base_plugin.py
@@ -0,0 +1,275 @@
+#wr 导入必要模块:日志用于记录插件运行信息,Path用于路径处理
+#wr Django模板相关模块用于模板渲染及处理模板不存在的异常
+import logging
+from pathlib import Path
+
+from django.template import TemplateDoesNotExist # wrDjango模板不存在时抛出的异常
+from django.template.loader import render_to_string # wrDjango的模板渲染函数
+
+#wr创建当前模块的日志记录器,用于记录插件相关日志
+logger = logging.getLogger(__name__)
+
+
+class BasePlugin:
+ """
+ wr插件基类,所有自定义插件需继承此类并实现必要的抽象方法。
+ 提供插件元数据管理、位置渲染、模板处理、静态资源管理等核心功能,
+ 实现了插件系统的基础框架。
+ """
+
+ # ===================== 插件元数据(子类必须定义) =====================
+ PLUGIN_NAME = None # wr插件名称(例如:"文章推荐插件")
+ PLUGIN_DESCRIPTION = None # wr插件描述(说明插件功能)
+ PLUGIN_VERSION = None # wr插件版本(例如:"1.0.0")
+ PLUGIN_AUTHOR = None #wr 插件作者(可选,可留空)
+
+ # ===================== 插件配置(子类可根据需求重写) =====================
+ SUPPORTED_POSITIONS = [] # wr插件支持的显示位置列表(例如:['sidebar', 'article_bottom'])
+ DEFAULT_PRIORITY = 100 #wr 默认优先级:数字越小,插件在同位置越靠前显示
+ POSITION_PRIORITIES = {} #wr 位置特定优先级:为不同位置单独设置优先级(覆盖默认值)
+
+ def __init__(self):
+ """
+ wr初始化插件实例。
+ 检查元数据完整性,获取插件路径和标识符,执行初始化逻辑并注册钩子。
+ """
+ #wr 校验插件元数据:名称、描述、版本为必填项,未定义则抛出异常
+ if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
+ raise ValueError("插件元数据(PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION)必须定义。")
+
+ #wr 获取插件所在目录路径和唯一标识符(slug)
+ self.plugin_dir = self._get_plugin_directory() # 插件目录路径(Path对象)
+ self.plugin_slug = self._get_plugin_slug() # 插件唯一标识(默认使用目录名)
+
+ # wr执行插件初始化逻辑(子类可重写)
+ self.init_plugin()
+ # wr注册插件钩子(子类可重写以注册自定义钩子)
+ self.register_hooks()
+
+ def _get_plugin_directory(self):
+ """
+ wr获取插件所在的目录路径。
+ 通过反射获取当前插件类所在的文件路径,进而得到目录路径。
+ """
+ import inspect
+ # 获取当前类(子类)的定义文件路径
+ plugin_file = inspect.getfile(self.__class__)
+ # 返回文件所在的目录路径
+ return Path(plugin_file).parent
+
+ def _get_plugin_slug(self):
+ """
+ wr获取插件的唯一标识符(slug)。
+ 默认使用插件目录的名称作为slug,确保唯一性。
+ """
+ return self.plugin_dir.name
+
+ def init_plugin(self):
+ """
+ wr插件初始化逻辑(钩子方法)。
+ 子类可重写此方法实现自定义初始化操作(如加载配置、连接数据库等)。
+ 默认仅记录初始化日志。
+ """
+ logger.info(f'{self.PLUGIN_NAME} 已初始化。')
+
+ def register_hooks(self):
+ """
+ wr注册插件钩子(钩子方法)。
+ 子类可重写此方法注册自定义钩子(如监听系统事件、注册URL路由等)。
+ 默认不执行任何操作。
+ """
+ pass
+
+ # ===================== 位置渲染系统(核心功能) =====================
+ def render_position_widget(self, position, context, **kwargs):
+ """
+ wr根据指定位置渲染插件组件,是位置渲染的入口方法。
+
+ Args:
+ position: 位置标识(如'sidebar'表示侧边栏)
+ context: Django模板上下文(包含页面渲染所需数据)
+ **kwargs: 额外参数(如文章ID、用户信息等,按需传递)
+
+ Returns:
+ dict: 包含渲染结果的字典({'html': HTML内容, 'priority': 优先级, 'plugin_name': 插件名})
+ 若位置不支持或无需显示,则返回None
+ """
+ #wr 检查当前位置是否在插件支持的位置列表中,不支持则直接返回None
+ if position not in self.SUPPORTED_POSITIONS:
+ return None
+
+ #wr 检查插件是否应在当前位置显示(调用should_display判断)
+ if not self.should_display(position, context, **kwargs):
+ return None
+
+ #wr 动态拼接当前位置对应的渲染方法名(如position为'sidebar',则方法名为'render_sidebar_widget')
+ method_name = f'render_{position}_widget'
+ #wr 检查当前类是否实现了对应位置的渲染方法
+ if hasattr(self, method_name):
+ #wr 调用对应方法获取HTML内容
+ html = getattr(self, method_name)(context, **kwargs)
+ #wr 若渲染成功(返回非空HTML),则构造结果字典
+ if html:
+ #wr 优先级:优先使用位置特定优先级,否则使用默认优先级
+ priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
+ return {
+ 'html': html,
+ 'priority': priority,
+ 'plugin_name': self.PLUGIN_NAME
+ }
+
+ #wr 若未实现对应渲染方法或渲染失败,返回None
+ return None
+
+ def should_display(self, position, context, **kwargs):
+ """
+ wr判断插件是否应在指定位置显示(钩子方法)。
+ 子类可重写此方法实现条件显示逻辑(如仅在特定页面/用户组显示)。
+ 默认返回True(始终显示)。
+
+ Args:
+ position: 位置标识
+ context: 模板上下文
+ **kwargs: 额外参数
+
+ Returns:
+ bool: True表示显示,False表示不显示
+ """
+ return True
+
+ # ===================== 位置渲染方法(子类需按需重写) =====================
+ def render_sidebar_widget(self, context, **kwargs):
+ """wr渲染侧边栏组件(钩子方法)。子类重写此方法实现侧边栏显示内容。"""
+ return None
+
+ def render_article_bottom_widget(self, context, **kwargs):
+ """wr渲染文章底部组件(钩子方法)。子类重写此方法实现文章底部显示内容。"""
+ return None
+
+ def render_article_top_widget(self, context, **kwargs):
+ """wr渲染文章顶部组件(钩子方法)。子类重写此方法实现文章顶部显示内容。"""
+ return None
+
+ def render_header_widget(self, context, **kwargs):
+ """wr渲染页头组件(钩子方法)。子类重写此方法实现页头显示内容。"""
+ return None
+
+ def render_footer_widget(self, context, **kwargs):
+ """wr渲染页脚组件(钩子方法)。子类重写此方法实现页脚显示内容。"""
+ return None
+
+ def render_comment_before_widget(self, context, **kwargs):
+ """wr渲染评论前组件(钩子方法)。子类重写此方法实现评论区前显示内容。"""
+ return None
+
+ def render_comment_after_widget(self, context, **kwargs):
+ """wr渲染评论后组件(钩子方法)。子类重写此方法实现评论区后显示内容。"""
+ return None
+
+ # ===================== 模板系统(插件模板渲染) =====================
+ def render_template(self, template_name, context=None):
+ """
+ wr 渲染插件自带的模板文件。
+ 模板路径固定为"plugins/[插件slug]/[模板文件名]"。
+
+ Args:
+ template_name: 模板文件名(如"sidebar.html")
+ context: 模板上下文(字典类型,默认为空)
+
+ Returns:
+ str: 渲染后的HTML字符串;若模板不存在,返回空字符串并记录警告日志
+ """
+ if context is None:
+ context = {} # 确保上下文不为None
+
+ #wr 构造模板路径:插件模板需放在"plugins/插件slug/"目录下
+ template_path = f"plugins/{self.plugin_slug}/{template_name}"
+
+ try:
+ #wr 使用Django的render_to_string渲染模板
+ return render_to_string(template_path, context)
+ except TemplateDoesNotExist:
+ #wr 模板不存在时记录警告日志
+ logger.warning(f"插件模板不存在:{template_path}")
+ return ""
+
+ # ===================== 静态资源系统(CSS/JS等资源管理) =====================
+ def get_static_url(self, static_file):
+ """
+ wr获取插件静态文件的URL。
+ 静态文件需放在插件目录的"static/[插件slug]/"下(遵循Django静态文件规范)。
+
+ Args:
+ static_file: 静态文件相对路径(如"css/style.css")
+
+ Returns:
+ str: 静态文件的完整URL(如"/static/demo_plugin/css/style.css")
+ """
+ from django.templatetags.static import static # wr导入Django静态文件工具
+ #wr 构造静态文件路径并生成URL
+ return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
+
+ def get_css_files(self):
+ """
+ wr获取插件所需的CSS文件列表(钩子方法)。
+ 子类重写此方法返回CSS文件路径列表,系统会自动在页面加载这些CSS。
+
+ Returns:
+ list: CSS文件路径列表(如["css/style.css"])
+ """
+ return []
+
+ def get_js_files(self):
+ """
+ wr获取插件所需的JavaScript文件列表(钩子方法)。
+ 子类重写此方法返回JS文件路径列表,系统会自动在页面加载这些JS。
+
+ Returns:
+ list: JS文件路径列表(如["js/script.js"])
+ """
+ return []
+
+ def get_head_html(self, context=None):
+ """
+ wr获取需要插入到HTML头部(标签内)的内容(钩子方法)。
+ 子类重写此方法返回自定义头部内容(如额外的CSS链接、meta标签等)。
+
+ Args:
+ context: 模板上下文
+
+ Returns:
+ str: 需插入到的HTML字符串
+ """
+ return ""
+
+ def get_body_html(self, context=None):
+ """
+ wr获取需要插入到HTML底部(标签前)的内容(钩子方法)。
+ 子类重写此方法返回自定义底部内容(如额外的JS脚本)。
+
+ Args:
+ context: 模板上下文
+
+ Returns:
+ str: 需插入到底部的HTML字符串
+ """
+ return ""
+
+ def get_plugin_info(self):
+ """
+ wr获取插件的详细信息(元数据+配置)。
+ 用于插件管理界面展示插件信息。
+
+ Returns:
+ dict: 包含插件信息的字典
+ """
+ return {
+ 'name': self.PLUGIN_NAME, #wr 插件名称
+ 'description': self.PLUGIN_DESCRIPTION, #wr 插件描述
+ 'version': self.PLUGIN_VERSION, #wr 插件版本
+ 'author': self.PLUGIN_AUTHOR, #wr 插件作者
+ 'slug': self.plugin_slug, #wr 插件唯一标识
+ 'directory': str(self.plugin_dir), #wr 插件目录路径
+ 'supported_positions': self.SUPPORTED_POSITIONS, # wr支持的显示位置
+ 'priorities': self.POSITION_PRIORITIES #wr 各位置的优先级
+ }
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/plugin_manage/hook_constants.py b/doc/DjangoBlog/djangoblog/plugin_manage/hook_constants.py
new file mode 100644
index 0000000..9d88c02
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/plugin_manage/hook_constants.py
@@ -0,0 +1,31 @@
+# wr文章相关操作事件钩子常量
+# wr用于标识文章生命周期中不同操作的事件,插件可通过监听这些事件执行对应逻辑
+ARTICLE_DETAIL_LOAD = 'article_detail_load' # wr文章详情页加载事件(当用户查看文章详情时触发)
+ARTICLE_CREATE = 'article_create' # wr文章创建事件(当文章被创建时触发)
+ARTICLE_UPDATE = 'article_update' # wr文章更新事件(当文章被修改时触发)
+ARTICLE_DELETE = 'article_delete' # wr文章删除事件(当文章被删除时触发)
+
+
+# wr文章内容处理钩子名称
+# wr用于标识对文章内容进行处理的钩子(如内容过滤、替换、添加额外信息等)
+ARTICLE_CONTENT_HOOK_NAME = "the_content"
+
+
+#wr 位置钩子常量字典
+#wr 键:页面中的具体位置标识(如文章顶部、侧边栏等)
+#wr 值:对应位置的组件钩子名称(插件可通过该钩子注册组件,系统会在对应位置渲染这些组件)
+POSITION_HOOKS = {
+ 'article_top': 'article_top_widgets', #wr 文章顶部位置的组件钩子
+ 'article_bottom': 'article_bottom_widgets', #wr文章底部位置的组件钩子
+ 'sidebar': 'sidebar_widgets', # wr侧边栏位置的组件钩子
+ 'header': 'header_widgets', #wr 页头位置的组件钩子
+ 'footer': 'footer_widgets', # wr页脚位置的组件钩子
+ 'comment_before': 'comment_before_widgets', # wr评论区之前位置的组件钩子
+ 'comment_after': 'comment_after_widgets', # wr评论区之后位置的组件钩子
+}
+
+
+#wr 资源注入钩子常量
+# wr用于标识需要在HTML特定区域注入资源(如CSS、JavaScript、meta标签等)的钩子
+HEAD_RESOURCES_HOOK = 'head_resources' # wrHTML头部(标签内)的资源注入钩子
+BODY_RESOURCES_HOOK = 'body_resources' # wrHTML body底部(标签前)的资源注入钩子
diff --git a/doc/DjangoBlog/djangoblog/plugin_manage/hooks.py b/doc/DjangoBlog/djangoblog/plugin_manage/hooks.py
new file mode 100644
index 0000000..591df25
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/plugin_manage/hooks.py
@@ -0,0 +1,85 @@
+import logging
+
+# wr创建当前模块的日志记录器,用于记录钩子系统的运行日志(如注册、执行、错误等)
+logger = logging.getLogger(__name__)
+
+# wr私有字典,用于存储所有注册的钩子:
+# wr键为钩子名称(str),值为该钩子对应的回调函数列表(list[callable])
+_hooks = {}
+
+
+def register(hook_name: str, callback: callable):
+ """
+ wr注册一个回调函数到指定钩子。
+ 一个钩子可以注册多个回调函数,执行时会按注册顺序依次调用。
+
+ Args:
+ hook_name: 钩子名称(需与后续执行/过滤时的钩子名称对应)
+ callback: 回调函数(当钩子触发时需要执行的函数)
+ """
+ #wr 如果钩子名称不在_hooks中,先初始化一个空列表用于存储回调
+ if hook_name not in _hooks:
+ _hooks[hook_name] = []
+ #wr 将回调函数添加到该钩子的回调列表中
+ _hooks[hook_name].append(callback)
+ #wr 记录调试日志,说明已注册钩子及对应的回调函数名
+ logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
+
+
+def run_action(hook_name: str, *args, **kwargs):
+ """
+ wr执行指定的"动作钩子(Action Hook)"。
+ 动作钩子用于触发一系列操作(无返回值),会按注册顺序执行所有绑定的回调函数。
+
+ Args:
+ hook_name: 要执行的钩子名称
+ *args: 传递给回调函数的位置参数
+ **kwargs: 传递给回调函数的关键字参数
+ """
+ # wr检查该钩子是否有已注册的回调函数
+ if hook_name in _hooks:
+ logger.debug(f"Running action hook '{hook_name}'")
+ # wr遍历该钩子的所有回调函数并执行
+ for callback in _hooks[hook_name]:
+ try:
+ #wr 传递参数调用回调函数
+ callback(*args, **kwargs)
+ except Exception as e:
+ # wr捕获回调执行中的异常,记录错误日志(包含异常详情)
+ logger.error(
+ f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
+ exc_info=True #wr 记录完整的异常堆栈信息
+ )
+
+
+def apply_filters(hook_name: str, value, *args, **kwargs):
+ """
+ wr执行指定的"过滤钩子(Filter Hook)"。
+ 过滤钩子用于对一个值进行链式处理(有输入有输出),会将初始值依次传递给所有绑定的回调函数,
+ 每个回调的返回值作为下一个回调的输入,最终返回经过所有处理后的结果。
+
+ Args:
+ hook_name: 要执行的钩子名称
+ value: 初始值(需要被过滤/处理的值)
+ *args: 传递给回调函数的位置参数
+ **kwargs: 传递给回调函数的关键字参数
+
+ Returns:
+ 经过所有回调函数处理后的最终值
+ """
+ #wr 检查该钩子是否有已注册的回调函数
+ if hook_name in _hooks:
+ logger.debug(f"Applying filter hook '{hook_name}'")
+ # wr遍历该钩子的所有回调函数,对值进行链式处理
+ for callback in _hooks[hook_name]:
+ try:
+ # wr调用回调函数处理当前值,将返回值作为新值继续处理
+ value = callback(value, *args, **kwargs)
+ except Exception as e:
+ #wr 捕获回调执行中的异常,记录错误日志(包含异常详情)
+ logger.error(
+ f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
+ exc_info=True #wr 记录完整的异常堆栈信息
+ )
+ #wr 返回最终处理后的值(若没有回调,直接返回初始值)
+ return value
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/plugin_manage/loader.py b/doc/DjangoBlog/djangoblog/plugin_manage/loader.py
new file mode 100644
index 0000000..c7cb021
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/plugin_manage/loader.py
@@ -0,0 +1,116 @@
+import os
+import logging
+from django.conf import settings # wr导入Django配置,用于获取插件相关设置
+
+# wr创建当前模块的日志记录器,用于记录插件加载过程中的日志信息
+logger = logging.getLogger(__name__)
+
+# wr全局插件注册表:存储所有已成功加载的插件实例
+_loaded_plugins = []
+
+
+def load_plugins():
+ """
+ wr动态加载并初始化'plugins'目录中的插件。
+ 此函数应在Django应用注册表就绪时调用(确保Django环境已初始化)。
+ """
+ global _loaded_plugins # wr声明使用全局变量_loaded_plugins
+ _loaded_plugins = [] # wr清空现有插件列表,重新加载
+
+ # wr遍历配置中激活的插件列表(settings.ACTIVE_PLUGINS定义了需要加载的插件名)
+ for plugin_name in settings.ACTIVE_PLUGINS:
+ # wr构建插件目录的绝对路径:PLUGINS_DIR(插件根目录) + 插件名
+ plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
+
+ # wr检查插件目录是否存在,且目录下是否有必要的'plugin.py'文件(插件入口)
+ if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
+ try:
+ # wr导入插件模块:格式为'plugins.插件名.plugin'(对应plugin.py文件)
+ plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
+
+ # wr检查插件模块中是否有'plugin'属性(通常是插件类的实例)
+ if hasattr(plugin_module, 'plugin'):
+ plugin_instance = plugin_module.plugin # wr获取插件实例
+ _loaded_plugins.append(plugin_instance) # wr添加到全局注册表
+ # wr记录成功加载的日志,包含插件名和插件元数据中的名称
+ logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
+ else:
+ # wr插件模块中没有'plugin'实例时记录警告
+ logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
+
+ except ImportError as e:
+ # wr导入插件模块失败时记录错误(如文件缺失、语法错误等)
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
+ except AttributeError as e:
+ # wr获取插件实例时发生属性错误(如缺少必要属性)
+ logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
+ except Exception as e:
+ # wr其他未预料的错误
+ logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
+
+
+def get_loaded_plugins():
+ """
+ wr获取所有已加载的插件实例列表。
+
+ Returns:
+ list: 已初始化的插件实例集合
+ """
+ return _loaded_plugins
+
+
+def get_plugin_by_name(plugin_name):
+ """
+ wr根据插件名称(slug)查找插件实例。
+ 注:实际通过插件的plugin_slug属性匹配(与函数名对应,可能存在命名统一设计)。
+
+ Args:
+ plugin_name: 插件的slug名称(通常为插件目录名)
+
+ Returns:
+ 匹配的插件实例,若未找到则返回None
+ """
+ for plugin in _loaded_plugins:
+ if plugin.plugin_slug == plugin_name:
+ return plugin
+ return None
+
+
+def get_plugin_by_slug(plugin_slug):
+ """
+ wr根据插件slug查找插件实例(与get_plugin_by_name功能一致,可能为兼容保留)。
+
+ Args:
+ plugin_slug: 插件的唯一标识符(slug)
+
+ Returns:
+ 匹配的插件实例,若未找到则返回None
+ """
+ for plugin in _loaded_plugins:
+ if plugin.plugin_slug == plugin_slug:
+ return plugin
+ return None
+
+
+def get_plugins_info():
+ """
+ wr获取所有已加载插件的信息字典列表。
+ 信息由插件的get_plugin_info()方法提供(包含元数据和配置)。
+
+ Returns:
+ list: 每个元素为一个插件的信息字典
+ """
+ return [plugin.get_plugin_info() for plugin in _loaded_plugins]
+
+
+def get_plugins_by_position(position):
+ """
+ wr筛选出支持指定显示位置的插件实例。
+
+ Args:
+ position: 位置标识(如'sidebar'、'article_bottom'等)
+
+ Returns:
+ list: 所有支持该位置的插件实例
+ """
+ return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/settings.py b/doc/DjangoBlog/djangoblog/settings.py
new file mode 100644
index 0000000..95532a0
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/settings.py
@@ -0,0 +1,435 @@
+"""
+Django settings for djangoblog project.
+项目基本配置文件,包含Django运行所需的核心配置项
+
+Generated by 'django-admin startproject' using Django 1.10.2.
+基于Django 1.10.2版本创建
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+#wr 导入必要模块
+import os
+import sys
+from pathlib import Path # 用于路径处理的现代工具
+
+from django.utils.translation import gettext_lazy as _ # 国际化翻译工具
+
+
+def env_to_bool(env, default):
+ """
+ 将环境变量值转换为布尔值
+ :param env: 环境变量名称
+ :param default: 当环境变量不存在时的默认值
+ :return: 转换后的布尔值(环境变量为'True'时返回True,否则返回default)
+ """
+ str_val = os.environ.get(env)
+ return default if str_val is None else str_val == 'True'
+
+
+#wr 项目路径配置
+# 构建项目内部路径,如:BASE_DIR / 'subdir'
+BASE_DIR = Path(__file__).resolve().parent.parent # 项目根目录(当前文件的父级父级目录)
+
+
+#wr 快速开发设置 - 不适用于生产环境
+# 安全警告:生产环境中必须保护好密钥
+SECRET_KEY = os.environ.get(
+ 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
+# 优先从环境变量获取密钥,不存在则使用默认(仅开发环境用)
+
+# 安全警告:生产环境必须关闭调试模式
+DEBUG = env_to_bool('DJANGO_DEBUG', True) # 调试模式开关,默认开启
+# DEBUG = False # 生产环境关闭调试的示例
+
+#wr 测试模式判断:当执行命令包含'test'时视为测试环境
+TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
+
+#wr 允许访问的主机列表(生产环境需指定具体域名,不可用'*')
+# ALLOWED_HOSTS = [] # 默认空列表(仅允许本地访问)
+ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] # 开发环境允许所有主机访问
+
+#wr Django 4.0新增配置:信任的CSRF来源(跨域请求时需要)
+CSRF_TRUSTED_ORIGINS = ['http://example.com']
+
+
+#wr 应用定义(安装的所有Django应用)
+INSTALLED_APPS = [
+ # 'django.contrib.admin', # 默认管理员界面(全功能版)
+ 'django.contrib.admin.apps.SimpleAdminConfig', # 简化版管理员界面
+ 'django.contrib.auth', # 身份认证系统
+ 'django.contrib.contenttypes', # 内容类型框架(用于权限管理)
+ 'django.contrib.sessions', # 会话管理
+ 'django.contrib.messages', # 消息提示系统
+ 'django.contrib.staticfiles', # 静态文件管理
+ 'django.contrib.sites', # 多站点支持框架
+ 'django.contrib.sitemaps', # 站点地图生成
+ 'mdeditor', # Markdown编辑器(第三方应用)
+ 'haystack', # 全文搜索框架(第三方应用)
+ 'blog', # 自定义博客应用
+ 'accounts', # 自定义用户账户应用
+ 'comments', # 自定义评论应用
+ 'oauth', # 第三方登录(OAuth)应用
+ 'servermanager', # 服务器管理应用
+ 'owntracks', # 位置追踪应用
+ 'compressor', # 静态文件压缩工具(第三方应用)
+ 'djangoblog' # 项目主应用
+]
+
+
+#wr 中间件配置(请求/响应处理的钩子函数)
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware', # 安全相关处理(如HTTPS重定向)
+ 'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理中间件
+ 'django.middleware.locale.LocaleMiddleware', # 国际化语言处理
+ 'django.middleware.gzip.GZipMiddleware', # 响应内容GZip压缩
+ # 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新中间件(按需启用)
+ 'django.middleware.common.CommonMiddleware', # 通用中间件(如URL重写)
+ # 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取中间件(按需启用)
+ 'django.middleware.csrf.CsrfViewMiddleware', # CSRF防护中间件
+ 'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证中间件
+ 'django.contrib.messages.middleware.MessageMiddleware', # 消息处理中间件
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护
+ 'django.middleware.http.ConditionalGetMiddleware', # 处理条件请求(如304响应)
+ 'blog.middleware.OnlineMiddleware' # 自定义在线状态中间件
+]
+
+
+#wr URL根配置
+ROOT_URLCONF = 'djangoblog.urls' # 主URL配置模块路径
+
+
+#wr 模板配置
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用Django模板引擎
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录
+ 'APP_DIRS': True, # 允许从应用内的templates目录加载模板
+ 'OPTIONS': {
+ 'context_processors': [ # 模板上下文处理器(全局变量)
+ 'django.template.context_processors.debug', # 调试相关上下文
+ 'django.template.context_processors.request', # 请求对象(request)
+ 'django.contrib.auth.context_processors.auth', # 认证相关上下文
+ 'django.contrib.messages.context_processors.messages', # 消息相关上下文
+ 'blog.context_processors.seo_processor' # 自定义SEO相关上下文
+ ],
+ },
+ },
+]
+
+
+#wr WSGI应用配置(部署用)
+WSGI_APPLICATION = 'djangoblog.wsgi.application' # WSGI应用入口路径
+
+
+wr# 数据库配置
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库引擎
+ 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名
+ 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名
+ 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', # 数据库密码
+ 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机地址
+ 'PORT': int(
+ os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口(默认3306)
+ 'OPTIONS': {
+ 'charset': 'utf8mb4'}, # 字符集(支持emoji表情)
+ }}
+
+
+#wr 密码验证配置
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ # 验证密码与用户属性(如用户名、邮箱)的相似度
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ # 验证密码最小长度(默认8位)
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ # 验证密码是否在常见密码列表中
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ # 验证密码是否仅包含数字
+ },
+]
+
+
+#wr 国际化配置
+LANGUAGES = (
+ ('en', _('English')), # 英语
+ ('zh-hans', _('Simplified Chinese')), # 简体中文
+ ('zh-hant', _('Traditional Chinese')), # 繁体中文
+)
+LOCALE_PATHS = (
+ os.path.join(BASE_DIR, 'locale'), # 翻译文件存储目录
+)
+
+LANGUAGE_CODE = 'zh-hans' # 默认语言(简体中文)
+TIME_ZONE = 'Asia/Shanghai' # 时区(上海)
+USE_I18N = True # 启用国际化
+USE_L10N = True # 启用本地化
+USE_TZ = False # 不使用UTC时区(使用本地时区)
+
+
+#wr 静态文件配置(CSS、JavaScript、图片等)
+
+#wr 全文搜索配置(基于Haystack框架)
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用Whoosh搜索引擎(支持中文)
+ 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引文件存储路径
+ },
+}
+#wr 实时更新搜索索引(当数据变化时自动更新)
+HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+
+#wr 认证后端:允许使用用户名或邮箱登录
+AUTHENTICATION_BACKENDS = [
+ 'accounts.user_login_backend.EmailOrUsernameModelBackend']
+
+
+#wr 静态文件收集目录(生产环境用,通过collectstatic命令收集)
+STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
+
+#wr 静态文件URL前缀
+STATIC_URL = '/static/'
+#wr 静态文件主目录
+STATICFILES = os.path.join(BASE_DIR, 'static')
+
+#wr 额外的静态文件目录(如插件静态文件)
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, 'plugins'), # 插件静态文件目录
+]
+
+
+#wr 自定义用户模型(替换Django默认用户模型)
+AUTH_USER_MODEL = 'accounts.BlogUser'
+# 登录页面URL(未登录时访问受保护页面会重定向到此处)
+LOGIN_URL = '/login/'
+
+
+#wr 时间格式配置(模板中使用)
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 完整时间格式
+DATE_TIME_FORMAT = '%Y-%m-%d' # 日期格式
+
+
+#wr Bootstrap样式颜色类型(前端样式用)
+BOOTSTRAP_COLOR_TYPES = [
+ 'default', 'primary', 'success', 'info', 'warning', 'danger'
+]
+
+
+#wr 分页配置
+PAGINATE_BY = 10 # 每页显示10条数据
+
+#wr HTTP缓存超时时间(秒):2592000 = 30天
+CACHE_CONTROL_MAX_AGE = 2592000
+
+#wr 缓存配置
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存
+ 'TIMEOUT': 10800, # 缓存超时时间(秒):10800 = 3小时
+ 'LOCATION': 'unique-snowflake', # 缓存位置标识(唯一即可)
+ }
+}
+#wr 若存在Redis环境变量,则使用Redis作为缓存
+if os.environ.get("DJANGO_REDIS_URL"):
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache', # Redis缓存引擎
+ 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis连接地址
+ }
+ }
+
+
+#wr 站点框架配置(用于多站点管理)
+SITE_ID = 1
+#wr 百度链接提交API地址(用于SEO)
+BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
+ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
+
+
+#wr 邮件配置(用于发送通知、验证码等)
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP邮件后端
+EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用TLS加密
+EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用SSL加密
+EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # 邮件服务器地址
+EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # 邮件服务器端口
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮箱用户名
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮箱密码/授权码
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人
+SERVER_EMAIL = EMAIL_HOST_USER # 服务器发件人(用于错误报告)
+
+#wr 管理员邮箱(用于接收系统错误报告)
+ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
+
+#wr 微信管理员密码(二次MD5加密)
+WXADMIN = os.environ.get(
+ 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
+
+
+#wr 日志配置
+LOG_PATH = os.path.join(BASE_DIR, 'logs') # 日志文件存储目录
+#wr 若日志目录不存在则创建
+if not os.path.exists(LOG_PATH):
+ os.makedirs(LOG_PATH, exist_ok=True)
+
+LOGGING = {
+ 'version': 1, # 日志配置版本
+ 'disable_existing_loggers': False, # 不禁用已存在的日志器
+ 'root': { # 根日志器
+ 'level': 'INFO', # 日志级别(INFO及以上)
+ 'handlers': ['console', 'log_file'], # 使用的处理器
+ },
+ 'formatters': { # 日志格式
+ 'verbose': { # 详细格式
+ 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
+ }
+ },
+ 'filters': { # 日志过滤器
+ 'require_debug_false': { # 仅当DEBUG=False时生效
+ '()': 'django.utils.log.RequireDebugFalse',
+ },
+ 'require_debug_true': { # 仅当DEBUG=True时生效
+ '()': 'django.utils.log.RequireDebugTrue',
+ },
+ },
+ 'handlers': { # 日志处理器
+ 'log_file': { # 文件处理器
+ 'level': 'INFO', # 处理INFO及以上级别
+ 'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的文件处理器
+ 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
+ 'when': 'D', # 每天轮转一次
+ 'formatter': 'verbose', # 使用详细格式
+ 'interval': 1, # 轮转间隔(1天)
+ 'delay': True, # 延迟创建文件
+ 'backupCount': 5, # 保留5个备份
+ 'encoding': 'utf-8' # 编码格式
+ },
+ 'console': { # 控制台处理器
+ 'level': 'DEBUG', # 处理DEBUG及以上级别
+ 'filters': ['require_debug_true'], # 仅调试模式生效
+ 'class': 'logging.StreamHandler', # 流处理器(输出到控制台)
+ 'formatter': 'verbose' # 使用详细格式
+ },
+ 'null': { # 空处理器(不处理日志)
+ 'class': 'logging.NullHandler',
+ },
+ 'mail_admins': { # 邮件通知处理器
+ 'level': 'ERROR', # 仅处理ERROR及以上级别
+ 'filters': ['require_debug_false'], # 仅生产环境生效
+ 'class': 'django.utils.log.AdminEmailHandler' # 发送邮件给管理员
+ }
+ },
+ 'loggers': { # 日志器
+ 'djangoblog': { # 项目主日志器
+ 'handlers': ['log_file', 'console'], # 使用文件和控制台处理器
+ 'level': 'INFO', # 日志级别
+ 'propagate': True, # 是否向上传播日志
+ }
+ }
+}
+
+
+#wr 静态文件压缩配置(使用django-compressor)
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder', # 从文件系统查找静态文件
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 从应用目录查找静态文件
+ 'compressor.finders.CompressorFinder', # 压缩器查找器
+)
+COMPRESS_ENABLED = True # 启用压缩
+#wr 根据环境变量决定是否启用离线压缩(预压缩静态文件)
+COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
+
+#wr 压缩文件输出目录
+COMPRESS_OUTPUT_DIR = 'compressed'
+
+#wr 压缩文件名包含哈希值(用于缓存失效)
+COMPRESS_CSS_HASHING_METHOD = 'mtime' # 基于修改时间生成哈希
+COMPRESS_JS_HASHING_METHOD = 'mtime'
+
+#wr CSS压缩过滤器
+COMPRESS_CSS_FILTERS = [
+ 'compressor.filters.css_default.CssAbsoluteFilter', # 转换为绝对URL
+ 'compressor.filters.cssmin.CSSCompressorFilter', # CSS压缩
+]
+
+#wr JS压缩过滤器
+COMPRESS_JS_FILTERS = [
+ 'compressor.filters.jsmin.SlimItFilter', # JS压缩
+]
+
+#wr 压缩缓存配置
+COMPRESS_CACHE_BACKEND = 'default' # 使用默认缓存
+COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey' # 缓存键生成函数
+
+#wr 预编译配置(处理Sass/SCSS)
+COMPRESS_PRECOMPILERS = (
+ ('text/x-scss', 'django_libsass.SassCompiler'), # 编译SCSS
+ ('text/x-sass', 'django_libsass.SassCompiler'), # 编译Sass
+)
+
+#wr 压缩性能优化
+COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
+COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
+COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时(30天)
+
+#wr 压缩器配置
+COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor' # CSS压缩器
+COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor' # JS压缩器
+
+#wr 静态文件缓存配置(文件名包含哈希,用于缓存破坏)
+STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
+
+#wr 压缩文件URL和根目录(与静态文件一致)
+COMPRESS_URL = STATIC_URL
+COMPRESS_ROOT = STATIC_ROOT
+
+
+#wr 媒体文件配置(用户上传文件)
+MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') # 上传文件存储目录
+MEDIA_URL = '/media/' # 媒体文件URL前缀
+
+#wr X-Frame-Options配置:允许同源页面嵌入iframe(如Markdown编辑器预览)
+X_FRAME_OPTIONS = 'SAMEORIGIN'
+
+
+#wr 默认自增字段类型(Django 3.2+新增)
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 使用BigInt类型自增ID
+
+
+#wr Elasticsearch配置(若存在环境变量则启用)
+if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
+ ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch地址
+ },
+ }
+ #wr 替换Haystack引擎为Elasticsearch
+ HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
+ },
+ }
+
+
+#wr 插件系统配置
+PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录
+ACTIVE_PLUGINS = [ # 激活的插件列表
+ 'article_copyright', # 文章版权信息插件
+ 'reading_time', # 阅读时间估算插件
+ 'external_links', # 外部链接处理插件
+ 'view_count', # 浏览量统计插件
+ 'seo_optimizer', # SEO优化插件
+ 'image_lazy_loading', # 图片懒加载插件
+ 'article_recommendation', # 文章推荐插件
+]
diff --git a/doc/DjangoBlog/djangoblog/sitemap.py b/doc/DjangoBlog/djangoblog/sitemap.py
new file mode 100644
index 0000000..1bac8b4
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/sitemap.py
@@ -0,0 +1,98 @@
+#wr 导入Django站点地图核心类及相关工具
+from django.contrib.sitemaps import Sitemap # 站点地图基类,所有自定义站点地图需继承此类
+from django.urls import reverse # URL反向解析工具,用于生成页面URL
+
+#wr 导入博客相关模型(站点地图需包含这些模型对应的页面)
+from blog.models import Article, Category, Tag
+
+
+class StaticViewSitemap(Sitemap):
+ """
+ wr静态页面站点地图:用于收录网站中固定不变的静态页面(如首页)
+ 站点地图(Sitemap)用于告诉搜索引擎网站的页面结构,帮助爬虫高效索引
+ """
+ priority = 0.5
+ #wr 页面更新频率(可选值:always, hourly, daily, weekly, monthly, yearly, never)
+ changefreq = 'daily' #wr 静态页面假设每日更新
+
+ def items(self):
+ """
+ wr返回需要收录的静态页面视图名称列表
+ 这里仅包含博客首页(视图名称为'blog:index',对应urls.py中的命名空间+名称)
+ """
+ return ['blog:index', ]
+
+ def location(self, item):
+ """
+ wr生成每个静态页面的URL
+ :param item: items()返回的视图名称(如'blog:index')
+ :return: 页面的绝对URL
+ """
+ return reverse(item) # 通过reverse反向解析视图名称为URL
+
+
+class ArticleSiteMap(Sitemap):
+ """wr文章页面站点地图:用于收录所有已发布的博客文章页面"""
+ changefreq = "monthly" #wr 文章页面更新频率设为每月(假设文章发布后较少修改)
+ priority = "0.6" #wr 文章页面优先级设为0.6(高于静态页面,低于核心页面)
+
+ def items(self):
+ """wr返回需要收录的文章对象列表:仅包含状态为"已发布"(status='p')的文章"""
+ return Article.objects.filter(status='p')
+
+ def lastmod(self, obj):
+ """
+ wr返回文章的最后修改时间(供搜索引擎判断页面是否更新)
+ :param obj: items()返回的Article实例
+ :return: 文章最后修改时间
+ """
+ return obj.last_modify_time
+
+
+class CategorySiteMap(Sitemap):
+ """wr分类页面站点地图:用于收录所有文章分类页面"""
+ changefreq = "Weekly" #wr 分类页面更新频率设为每周(分类信息较少变动)
+ priority = "0.6" #wr 分类页面优先级与文章页面相同
+
+ def items(self):
+ """wr返回所有分类对象列表(所有分类页面都需要被收录)"""
+ return Category.objects.all()
+
+ def lastmod(self, obj):
+ """wr返回分类的最后修改时间"""
+ return obj.last_modify_time
+
+
+class TagSiteMap(Sitemap):
+ """wr标签页面站点地图:用于收录所有文章标签页面"""
+ changefreq = "Weekly" #wr 标签页面更新频率设为每周
+ priority = "0.3" #wr 标签页面优先级较低(0.3),因为重要性低于文章和分类
+
+ def items(self):
+ """wr返回所有标签对象列表(所有标签页面都需要被收录)"""
+ return Tag.objects.all()
+
+ def lastmod(self, obj):
+ """wr返回标签的最后修改时间"""
+ return obj.last_modify_time
+
+
+class UserSiteMap(Sitemap):
+ """wr用户页面站点地图:用于收录所有发表过文章的作者页面"""
+ changefreq = "Weekly" #v 用户页面更新频率设为每周
+ priority = "0.3" #wr 用户页面优先级较低
+
+ def items(self):
+ """
+ wr返回所有发表过文章的作者列表(去重处理)
+ 逻辑:通过map提取所有已发布文章的作者,再用set去重,最后转为列表
+ """
+ return list(set(map(lambda x: x.author, Article.objects.all())))
+
+ def lastmod(self, obj):
+ """
+ wr返回用户相关的最后更新时间(这里用用户注册时间代替,也可改为用户最后发表文章时间)
+ :param obj: items()返回的用户实例
+ :return: 用户注册时间
+ """
+ return obj.date_joined
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/spider_notify.py b/doc/DjangoBlog/djangoblog/spider_notify.py
new file mode 100644
index 0000000..b6df1ad
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/spider_notify.py
@@ -0,0 +1,41 @@
+import logging # 导入日志模块,用于记录通知过程中的信息和错误
+import requests # 导入requests库,用于发送HTTP请求(与搜索引擎API交互)
+from django.conf import settings # 导入Django配置,用于获取百度推送API地址等配置项
+
+#wr 初始化日志记录器(指定记录器名称为当前模块)
+logger = logging.getLogger(__name__)
+
+
+class SpiderNotify():
+ """
+ wr搜索引擎通知工具类:用于向搜索引擎(如百度)推送网站新内容的URL
+ 帮助搜索引擎快速发现并收录新页面,提升SEO效率
+ """
+
+ @staticmethod
+ def baidu_notify(urls):
+ """
+ wr向百度搜索引擎推送URL(使用百度链接提交API)
+ 百度会根据推送的URL优先抓取和收录页面
+ :param urls: 需要推送的URL列表(如文章详情页链接)
+ """
+ try:
+ #wr 将URL列表转换为百度API要求的格式:每行一个URL(用换行符拼接)
+ data = '\n'.join(urls)
+ #wr 发送POST请求到百度推送API(API地址从Django配置中获取)
+ result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
+ #wr 记录API返回结果(便于监控推送是否成功)
+ logger.info(result.text)
+ except Exception as e:
+ #wr 记录推送过程中的异常(如网络错误、API地址错误等)
+ logger.error(e)
+
+ @staticmethod
+ def notify(url):
+ """
+ wr通用通知方法:封装百度推送逻辑,便于后续扩展其他搜索引擎
+ 目前仅实现百度推送,可根据需求添加谷歌、必应等其他引擎的推送逻辑
+ :param url: 需要推送的单个URL(或URL列表,根据实际调用场景调整)
+ """
+ #wr 调用百度推送方法
+ SpiderNotify.baidu_notify(url)
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/tests.py b/doc/DjangoBlog/djangoblog/tests.py
new file mode 100644
index 0000000..d7e8430
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/tests.py
@@ -0,0 +1,42 @@
+def setUp(self):
+ """
+ wr测试前置初始化方法
+ 会在每个测试方法(以test_开头的方法)执行前自动调用
+ 通常用于创建测试数据、初始化测试环境等
+ 本案例暂无需初始化操作,故为空实现
+ """
+ pass
+
+
+def test_utils(self):
+ """
+ wr核心测试方法:验证工具函数的基本功能
+ 测试内容包括哈希计算、Markdown转换、字典转URL参数等工具函数
+ """
+ # wr 测试SHA256哈希函数:对字符串"test"进行哈希计算
+ md5 = get_sha256('test')
+ # wr 断言:哈希结果不为None(验证函数能正常生成哈希值,无异常)
+ self.assertIsNotNone(md5)
+
+ # wr 测试Markdown转换工具:处理包含多级语法的Markdown文本
+ # wr 输入内容包括:一级标题、Python代码块、两个不同链接
+ c = CommonMarkdown.get_markdown('''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''')
+ self.assertIsNotNone(c)
+ d = {
+ 'd': 'key1',
+ 'd2': 'key2'
+ }
+ data = parse_dict_to_url(d)
+ self.assertIsNotNone(data)
diff --git a/doc/DjangoBlog/djangoblog/urls.py b/doc/DjangoBlog/djangoblog/urls.py
new file mode 100644
index 0000000..0e40245
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/urls.py
@@ -0,0 +1,113 @@
+"""djangoblog URL Configuration
+Django博客项目的URL配置文件
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+`urlpatterns`列表用于将URL路由到对应的视图。更多信息请参考:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+函数视图示例:
+ 1. Add an import: from my_app import views
+ 1. 导入视图:from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+ 2. 添加URL到urlpatterns:url(r'^$', views.home, name='home')
+Class-based views
+类视图示例:
+ 1. Add an import: from other_app.views import Home
+ 1. 导入类视图:from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+ 2. 添加URL到urlpatterns:url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+包含其他URL配置示例:
+ 1. Import the include() function: from django.conf.urls import url, include
+ 1. 导入include()函数:from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+ 2. 添加URL到urlpatterns:url(r'^blog/', include('blog.urls'))
+"""
+# wr导入Django配置
+from django.conf import settings
+#wr 导入国际化URL工具(用于生成带语言前缀的URL)
+from django.conf.urls.i18n import i18n_patterns
+#wr 导入静态文件URL配置工具
+from django.conf.urls.static import static
+# wr导入站点地图视图
+from django.contrib.sitemaps.views import sitemap
+# wr导入URL路径配置工具
+from django.urls import path, include
+from django.urls import re_path # wr兼容正则表达式的URL配置
+#wr 导入Haystack搜索视图工厂
+from haystack.views import search_view_factory
+#wr 导入JSON响应工具
+from django.http import JsonResponse
+import time #wr 时间模块,用于健康检查的时间戳
+
+
+from blog.views import EsSearchView
+#wr 导入自定义管理员站点
+from djangoblog.admin_site import admin_site
+#wr 导入ElasticSearch搜索表单
+from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
+#wr 导入RSS订阅源
+from djangoblog.feeds import DjangoBlogFeed
+#wr 导入站点地图配置类
+from djangoblog.sitemap import (
+ ArticleSiteMap, CategorySiteMap,
+ StaticViewSitemap, TagSiteMap, UserSiteMap
+)
+
+#wr 站点地图配置:定义不同类型页面的站点地图
+sitemaps = {
+ 'blog': ArticleSiteMap, #wr文章页面站点地图
+ 'Category': CategorySiteMap, #wr 分类页面站点地图
+ 'Tag': TagSiteMap, #wr 标签页面站点地图
+ 'User': UserSiteMap, #wr 用户页面站点地图
+ 'static': StaticViewSitemap # wr静态页面(如首页)站点地图
+}
+
+# wr错误页面处理配置:指定对应错误码的处理视图
+handler404 = 'blog.views.page_not_found_view' # wr404页面不存在
+handler500 = 'blog.views.server_error_view' # wr500服务器内部错误
+handle403 = 'blog.views.permission_denied_view' # wr403权限拒绝
+
+
+def health_check(request):
+ """
+ wr健康检查接口
+ 用于监控服务是否正常运行(如容器化部署中的存活探针)
+ """
+ return JsonResponse({
+ 'status': 'healthy', #wr 健康状态标识
+ 'timestamp': time.time() # wr当前时间戳(用于验证响应时效性)
+ })
+
+# wr基础URL配置:不依赖语言前缀的URL
+urlpatterns = [
+ path('i18n/', include('django.conf.urls.i18n')), # wr国际化配置入口(语言切换等)
+ path('health/', health_check, name='health_check'), # 健康检查接口
+]
+
+# wr国际化URL配置:带语言前缀的URL(如/en/blog/、/zh/blog/)
+urlpatterns += i18n_patterns(
+ re_path(r'^admin/', admin_site.urls), # wr自定义管理员后台URL
+ re_path(r'', include('blog.urls', namespace='blog')), # wr博客主应用URL(命名空间blog)
+ re_path(r'mdeditor/', include('mdeditor.urls')), #wr Markdown编辑器URL
+ re_path(r'', include('comments.urls', namespace='comment')), # wr评论应用URL(命名空间comment)
+ re_path(r'', include('accounts.urls', namespace='account')), #wr 用户账户应用URL(命名空间account)
+ re_path(r'', include('oauth.urls', namespace='oauth')), #wr 第三方登录应用URL(命名空间oauth)
+ # wr站点地图XML文件URL(搜索引擎会抓取此文件)
+ re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
+ name='django.contrib.sitemaps.views.sitemap'),
+ re_path(r'^feed/$', DjangoBlogFeed()), # wrRSS订阅源URL(/feed/)
+ re_path(r'^rss/$', DjangoBlogFeed()), #wr RSS订阅源URL(/rss/,与feed等价)
+ # wr搜索功能URL:使用自定义的ElasticSearch搜索视图和表单
+ re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
+ name='search'),
+ re_path(r'', include('servermanager.urls', namespace='servermanager')), #wr 服务器管理应用URL
+ re_path(r'', include('owntracks.urls', namespace='owntracks')), # wr位置追踪应用URL
+ prefix_default_language=False #wr 不使用默认语言前缀(如默认语言为中文时,不强制添加/zh/)
+) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) #wr 静态文件URL配置
+
+#wr 开发环境下的媒体文件URL配置(生产环境通常由Web服务器处理)
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/doc/DjangoBlog/djangoblog/utils.py b/doc/DjangoBlog/djangoblog/utils.py
new file mode 100644
index 0000000..96316b6
--- /dev/null
+++ b/doc/DjangoBlog/djangoblog/utils.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# 指定脚本解释器为Python,编码格式为UTF-8
+
+
+import logging #wr 日志模块,用于记录系统运行信息
+import os #wr 操作系统交互模块,用于文件路径、目录操作
+import random #wr 随机数模块,用于生成随机数据
+import string #wr 字符串模块,包含字符串常量(如数字、字母等)
+import uuid #wr 用于生成唯一标识符
+from hashlib import sha256 #wr 哈希算法模块,用于计算SHA-256哈希值
+
+import bleach #wr HTML清理库,用于过滤不安全的HTML标签和属性(防止XSS攻击)
+import markdown #wr Markdown转换库,用于将Markdown文本转为HTML
+import requests #wr HTTP请求库,用于发送网络请求(如下载图片)
+from django.conf import settings #wr Django配置模块,用于获取项目设置
+from django.contrib.sites.models import Site #wr Django站点模型,用于管理网站域名等信息
+from django.core.cache import cache #wr Django缓存模块,用于缓存数据(提升性能)
+from django.templatetags.static import static #wr Django静态文件工具,用于生成静态文件URL
+
+#wr 初始化日志记录器(指定记录器名称为当前模块)
+logger = logging.getLogger(__name__)
+
+
+def get_max_articleid_commentid():
+ """
+ wr获取文章和评论的最大ID
+ 用于系统初始化或数据校验时获取当前最大的文章ID和评论ID
+ :return: 元组 (最大文章ID, 最大评论ID)
+ """
+ from blog.models import Article # 导入文章模型
+ from comments.models import Comment # 导入评论模型
+ #wr 返回最新文章的ID和最新评论的ID(假设模型有pk字段作为主键)
+ return (Article.objects.latest().pk, Comment.objects.latest().pk)
+
+
+def get_sha256(str):
+ """
+ wr计算字符串的SHA-256哈希值
+ 用于密码加密、数据校验等场景(哈希值不可逆,确保数据安全)
+ :param str: 待哈希的字符串
+ :return: 哈希后的十六进制字符串
+ """
+ #wr 创建SHA-256哈希对象,对字符串进行UTF-8编码后计算哈希
+ m = sha256(str.encode('utf-8'))
+ return m.hexdigest() #wr 返回十六进制格式的哈希结果
+
+
+def cache_decorator(expiration=3 * 60):
+ """
+ wr缓存装饰器:用于缓存函数返回结果,减少重复计算或数据库查询
+ :param expiration: 缓存过期时间(秒),默认3分钟
+ :return: 装饰器函数
+ """
+
+ def wrapper(func):
+ def news(*args, **kwargs):
+ try:
+ #wr 尝试从视图对象中获取缓存键(适用于Django视图函数)
+ view = args[0]
+ key = view.get_cache_key()
+ except:
+ #wr 若无法从视图获取,则基于函数、参数生成唯一缓存键
+ key = None
+ if not key:
+ #wr 将函数和参数转换为字符串,确保唯一性
+ unique_str = repr((func, args, kwargs))
+ #wr 计算字符串的SHA-256哈希作为缓存键(避免键过长)
+ m = sha256(unique_str.encode('utf-8'))
+ key = m.hexdigest()
+
+ # wr尝试从缓存中获取数据
+ value = cache.get(key)
+ if value is not None:
+ #wr 缓存命中:返回缓存值(处理默认空值标记)
+ if str(value) == '__default_cache_value__':
+ return None
+ else:
+ return value
+ else:
+ #wr 缓存未命中:执行原函数获取结果
+ logger.debug(f'cache_decorator set cache:{func.__name__} key:{key}')
+ value = func(*args, **kwargs)
+ # wr根据结果设置缓存(空结果用特殊标记,避免缓存穿透)
+ if value is None:
+ cache.set(key, '__default_cache_value__', expiration)
+ else:
+ cache.set(key, value, expiration)
+ return value
+
+ return news
+
+ return wrapper
+
+
+def expire_view_cache(path, servername, serverport, key_prefix=None):
+ '''
+ wr刷新指定视图的缓存
+ 用于在数据更新后主动清除对应视图的缓存,确保用户获取最新数据
+ :param path: 视图对应的URL路径(如'/article/1/')
+ :param servername: 服务器域名(如'example.com')
+ :param serverport: 服务器端口(如'80')
+ :param key_prefix: 缓存键前缀(可选)
+ :return: 布尔值,True表示缓存清除成功,False表示未找到缓存
+ '''
+ from django.http import HttpRequest # wr导入HTTP请求类
+ from django.utils.cache import get_cache_key #wr 导入获取缓存键的工具
+
+ # wr构建模拟请求对象(用于生成缓存键)
+ request = HttpRequest()
+ request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
+ request.path = path
+
+ # wr获取该请求对应的缓存键
+ key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
+ if key:
+ logger.info(f'expire_view_cache:get key:{path}')
+ # wr若缓存存在,则删除
+ if cache.get(key):
+ cache.delete(key)
+ return True
+ return False
+
+
+@cache_decorator() # wr应用缓存装饰器,默认缓存3分钟
+def get_current_site():
+ """
+wr 用于生成绝对URL、网站标题等场景,缓存减轻数据库压力
+ :return: Site模型实例
+ """
+ site = Site.objects.get_current() # 获取当前站点(Django默认功能)
+ return site
+
+
+class CommonMarkdown:
+ """
+ wrMarkdown处理工具类:提供Markdown文本到HTML的转换功能,支持代码高亮和目录生成
+ """
+
+ @staticmethod
+ def _convert_markdown(value):
+ """
+ wr 内部方法:执行Markdown转换
+ :param value: Markdown格式的文本
+ :return: 元组 (转换后的HTML内容, 目录HTML)
+ """
+ # wr初始化Markdown转换器,启用必要扩展:
+ # - extra: 支持表格、脚注等扩展语法
+ # - codehilite: 代码高亮
+ # - toc: 生成目录
+ # - tables: 表格支持(extra已包含,这里冗余确保兼容性)
+ md = markdown.Markdown(
+ extensions=[
+ 'extra',
+ 'codehilite',
+ 'toc',
+ 'tables',
+ ]
+ )
+ body = md.convert(value) # 转换文本为HTML
+ toc = md.toc # 获取生成的目录HTML
+ return body, toc
+
+ @staticmethod
+ def get_markdown_with_toc(value):
+ """
+ wr转换Markdown文本为HTML,并返回内容和目录
+ :param value: Markdown文本
+ :return: 元组 (HTML内容, 目录HTML)
+ """
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body, toc
+
+ @staticmethod
+ def get_markdown(value):
+ """
+ wr转换Markdown文本为HTML(仅返回内容,忽略目录)
+ :param value: Markdown文本
+ :return: HTML内容字符串
+ """
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body
+
+
+def send_email(emailto, title, content):
+ """
+ wr 发送邮件(通过Django信号机制,解耦邮件发送逻辑)
+ 用于用户注册验证、评论通知等场景
+ :param emailto: 收件人邮箱
+ :param title: 邮件标题
+ :param content: 邮件内容
+ """
+ from djangoblog.blog_signals import send_email_signal # 导入邮件发送信号
+ #wr 发送信号(实际发送逻辑由信号接收者实现,如SMTP发送)
+ send_email_signal.send(
+ send_email.__class__,
+ emailto=emailto,
+ title=title,
+ content=content)
+
+
+def generate_code() -> str:
+ """
+ wr生成6位数字验证码
+ 用于用户登录、注册等场景的身份验证
+ :return: 6位数字字符串
+ """
+ #wr 从数字字符集中随机选择6个字符并拼接
+ return ''.join(random.sample(string.digits, 6))
+
+
+def parse_dict_to_url(dict):
+ """
+ wr将字典转换为URL查询参数字符串(如{'a':1, 'b':2} → 'a=1&b=2')
+ 用于构建带参数的URL
+ :param dict: 键值对字典
+ :return: URL查询参数字符串
+ """
+ from urllib.parse import quote #wr 导入URL编码工具
+ #wr 对键和值进行URL编码(处理特殊字符),然后拼接为key=value&key=value格式
+ url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
+ for k, v in dict.items()])
+ return url
+
+
+def get_blog_setting():
+ """
+ wr获取博客系统设置(如网站名称、描述等),带缓存机制
+ 用于网站全局配置的统一管理
+ :return: BlogSettings模型实例
+ """
+ #wr 尝试从缓存获取
+ value = cache.get('get_blog_setting')
+ if value:
+ return value
+ else:
+ from blog.models import BlogSettings #wr 导入博客设置模型
+ #wr 若数据库中无设置记录,创建默认设置
+ if not BlogSettings.objects.count():
+ setting = BlogSettings()
+ setting.site_name = 'djangoblog' #wr 网站名称
+ setting.site_description = '基于Django的博客系统' #wr 网站描述
+ setting.site_seo_description = '基于Django的博客系统' #wr SEO描述
+ setting.site_keywords = 'Django,Python' #wr 网站关键词
+ setting.article_sub_length = 300 #wr 文章摘要长度
+ setting.sidebar_article_count = 10 #wr 侧边栏文章数量
+ setting.sidebar_comment_count = 5 #wr 侧边栏评论数量
+ setting.show_google_adsense = False #wr 是否显示谷歌广告
+ setting.open_site_comment = True #wr 是否开启评论
+ setting.analytics_code = '' #wr 统计代码(如Google Analytics)
+ setting.beian_code = '' #wr 备案号
+ setting.show_gongan_code = False #wr 是否显示公安备案号
+ setting.comment_need_review = False #wr 评论是否需要审核
+ setting.save() #wr 保存默认设置
+ #wr 从数据库获取设置
+ value = BlogSettings.objects.first()
+ logger.info('set cache get_blog_setting')
+ cache.set('get_blog_setting', value) #wr 缓存设置
+ return value
+
+
+def save_user_avatar(url):
+ '''
+ wr下载并保存用户头像到本地静态文件目录
+ 用于用户上传头像或第三方登录时获取头像
+ :param url: 头像图片的URL
+ :return: 本地头像的静态文件URL(异常时返回默认头像)
+ '''
+ logger.info(url) #wr 记录头像URL
+
+ try:
+ #wr 定义头像保存目录(项目静态文件目录下的avatar文件夹)
+ basedir = os.path.join(settings.STATICFILES, 'avatar')
+ #wr 发送GET请求下载图片(超时2秒)
+ rsp = requests.get(url, timeout=2)
+ if rsp.status_code == 200: # 下载成功
+ #wr 若目录不存在则创建
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+
+ #wr 支持的图片扩展名
+ image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
+ #wr 判断URL是否指向图片文件
+ isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
+ #wr 提取扩展名(非图片文件默认用.jpg)
+ ext = os.path.splitext(url)[1] if isimage else '.jpg'
+ #wr 生成唯一文件名(UUID避免重复)
+ save_filename = str(uuid.uuid4().hex) + ext
+ logger.info(f'保存用户头像:{basedir}{save_filename}')
+ #wr 写入文件
+ with open(os.path.join(basedir, save_filename), 'wb+') as file:
+ file.write(rsp.content)
+ #wr 返回静态文件URL(如/static/avatar/xxx.jpg)
+ return static('avatar/' + save_filename)
+ except Exception as e:
+ #wr 异常处理(如网络错误、文件写入失败等)
+ logger.error(e)
+ #wr 返回默认头像URL
+ return static('blog/img/avatar.png')
+
+
+def delete_sidebar_cache():
+ """
+ wr删除侧边栏相关缓存
+ 用于侧边栏数据(如热门文章、最新评论)更新后刷新缓存
+ """
+ from blog.models import LinkShowType # 导入链接展示类型模型
+ #wr 生成所有侧边栏缓存键(基于LinkShowType的取值)
+ keys = ["sidebar" + x for x in LinkShowType.values]
+ #wr 逐个删除缓存
+ for k in keys:
+ logger.info(f'delete sidebar key:{k}')
+ cache.delete(k)
+
+
+def delete_view_cache(prefix, keys):
+ """
+ wr删除Django模板片段缓存
+ 用于模板中通过{% cache %}标签缓存的内容更新后刷新
+ :param prefix: 缓存前缀(对应{% cache %}的第一个参数)
+ :param keys: 缓存键的动态部分(对应{% cache %}的后续参数)
+ """
+ from django.core.cache.utils import make_template_fragment_key #wr 生成模板缓存键的工具
+ #wr 生成模板片段缓存键
+ key = make_template_fragment_key(prefix, keys)
+ cache.delete(key) #wr 删除缓存
+
+
+def get_resource_url():
+ """
+ wr获取静态资源的基础URL
+ 用于统一管理静态文件路径(如CSS、JS、图片等)
+ :return: 静态资源基础URL字符串
+ """
+ if settings.STATIC_URL:
+ #wr 若项目设置中定义了STATIC_URL,直接使用
+ return settings.STATIC_URL
+ else:
+ #wr 否则使用当前站点域名拼接静态目录路径
+ site = get_current_site()
+ return 'http://' + site.domain + '/static/'
+
+
+#wr HTML清理配置:限制允许的HTML标签(防止XSS攻击)
+#wr 只允许必要的标签,避免'
+
+ seo_html = f"""
+ {seo_data.get("title", "")}
+
+
+ {seo_data.get("meta_tags", "")}
+ {json_ld_script}
+ """
+
+ # 将SEO内容追加到现有的metas内容上
+ return metas + seo_html
+
+plugin = SeoOptimizerPlugin()
diff --git a/doc/DjangoBlog/plugins/view_count/__init__.py b/doc/DjangoBlog/plugins/view_count/__init__.py
new file mode 100644
index 0000000..8804fdf
--- /dev/null
+++ b/doc/DjangoBlog/plugins/view_count/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
\ No newline at end of file
diff --git a/doc/DjangoBlog/plugins/view_count/plugin.py b/doc/DjangoBlog/plugins/view_count/plugin.py
new file mode 100644
index 0000000..15e9d94
--- /dev/null
+++ b/doc/DjangoBlog/plugins/view_count/plugin.py
@@ -0,0 +1,18 @@
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+
+
+class ViewCountPlugin(BasePlugin):
+ PLUGIN_NAME = '文章浏览次数统计'
+ PLUGIN_DESCRIPTION = '统计文章的浏览次数'
+ PLUGIN_VERSION = '0.1.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def register_hooks(self):
+ hooks.register('after_article_body_get', self.record_view)
+
+ def record_view(self, article, *args, **kwargs):
+ article.viewed()
+
+
+plugin = ViewCountPlugin()
\ No newline at end of file
diff --git a/doc/DjangoBlog/requirements.txt b/doc/DjangoBlog/requirements.txt
new file mode 100644
index 0000000..e5878ab
Binary files /dev/null and b/doc/DjangoBlog/requirements.txt differ
diff --git a/doc/DjangoBlog/servermanager/MemcacheStorage.py b/doc/DjangoBlog/servermanager/MemcacheStorage.py
new file mode 100644
index 0000000..38a7990
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/MemcacheStorage.py
@@ -0,0 +1,32 @@
+from werobot.session import SessionStorage
+from werobot.utils import json_loads, json_dumps
+
+from djangoblog.utils import cache
+
+
+class MemcacheStorage(SessionStorage):
+ def __init__(self, prefix='ws_'):
+ self.prefix = prefix
+ self.cache = cache
+
+ @property
+ def is_available(self):
+ value = "1"
+ self.set('checkavaliable', value=value)
+ return value == self.get('checkavaliable')
+
+ def key_name(self, s):
+ return '{prefix}{s}'.format(prefix=self.prefix, s=s)
+
+ def get(self, id):
+ id = self.key_name(id)
+ session_json = self.cache.get(id) or '{}'
+ return json_loads(session_json)
+
+ def set(self, id, value):
+ id = self.key_name(id)
+ self.cache.set(id, json_dumps(value))
+
+ def delete(self, id):
+ id = self.key_name(id)
+ self.cache.delete(id)
diff --git a/doc/DjangoBlog/servermanager/__init__.py b/doc/DjangoBlog/servermanager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/servermanager/admin.py b/doc/DjangoBlog/servermanager/admin.py
new file mode 100644
index 0000000..f26f4f6
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/admin.py
@@ -0,0 +1,19 @@
+from django.contrib import admin
+# Register your models here.
+
+
+class CommandsAdmin(admin.ModelAdmin):
+ list_display = ('title', 'command', 'describe')
+
+
+class EmailSendLogAdmin(admin.ModelAdmin):
+ list_display = ('title', 'emailto', 'send_result', 'creation_time')
+ readonly_fields = (
+ 'title',
+ 'emailto',
+ 'send_result',
+ 'creation_time',
+ 'content')
+
+ def has_add_permission(self, request):
+ return False
diff --git a/doc/DjangoBlog/servermanager/api/__init__.py b/doc/DjangoBlog/servermanager/api/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/api/__init__.py
@@ -0,0 +1 @@
+
diff --git a/doc/DjangoBlog/servermanager/api/blogapi.py b/doc/DjangoBlog/servermanager/api/blogapi.py
new file mode 100644
index 0000000..8a4d6ac
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/api/blogapi.py
@@ -0,0 +1,27 @@
+from haystack.query import SearchQuerySet
+
+from blog.models import Article, Category
+
+
+class BlogApi:
+ def __init__(self):
+ self.searchqueryset = SearchQuerySet()
+ self.searchqueryset.auto_query('')
+ self.__max_takecount__ = 8
+
+ def search_articles(self, query):
+ sqs = self.searchqueryset.auto_query(query)
+ sqs = sqs.load_all()
+ return sqs[:self.__max_takecount__]
+
+ def get_category_lists(self):
+ return Category.objects.all()
+
+ def get_category_articles(self, categoryname):
+ articles = Article.objects.filter(category__name=categoryname)
+ if articles:
+ return articles[:self.__max_takecount__]
+ return None
+
+ def get_recent_articles(self):
+ return Article.objects.all()[:self.__max_takecount__]
diff --git a/doc/DjangoBlog/servermanager/api/commonapi.py b/doc/DjangoBlog/servermanager/api/commonapi.py
new file mode 100644
index 0000000..83ad9ff
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/api/commonapi.py
@@ -0,0 +1,64 @@
+import logging
+import os
+
+import openai
+
+from servermanager.models import commands
+
+logger = logging.getLogger(__name__)
+
+openai.api_key = os.environ.get('OPENAI_API_KEY')
+if os.environ.get('HTTP_PROXY'):
+ openai.proxy = os.environ.get('HTTP_PROXY')
+
+
+class ChatGPT:
+
+ @staticmethod
+ def chat(prompt):
+ try:
+ completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
+ messages=[{"role": "user", "content": prompt}])
+ return completion.choices[0].message.content
+ except Exception as e:
+ logger.error(e)
+ return "服务器出错了"
+
+
+class CommandHandler:
+ def __init__(self):
+ self.commands = commands.objects.all()
+
+ def run(self, title):
+ """
+ 运行命令
+ :param title: 命令
+ :return: 返回命令执行结果
+ """
+ cmd = list(
+ filter(
+ lambda x: x.title.upper() == title.upper(),
+ self.commands))
+ if cmd:
+ return self.__run_command__(cmd[0].command)
+ else:
+ return "未找到相关命令,请输入hepme获得帮助。"
+
+ def __run_command__(self, cmd):
+ try:
+ res = os.popen(cmd).read()
+ return res
+ except BaseException:
+ return '命令执行出错!'
+
+ def get_help(self):
+ rsp = ''
+ for cmd in self.commands:
+ rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
+ return rsp
+
+
+if __name__ == '__main__':
+ chatbot = ChatGPT()
+ prompt = "写一篇1000字关于AI的论文"
+ print(chatbot.chat(prompt))
diff --git a/doc/DjangoBlog/servermanager/apps.py b/doc/DjangoBlog/servermanager/apps.py
new file mode 100644
index 0000000..03cc38d
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ServermanagerConfig(AppConfig):
+ name = 'servermanager'
diff --git a/doc/DjangoBlog/servermanager/migrations/0001_initial.py b/doc/DjangoBlog/servermanager/migrations/0001_initial.py
new file mode 100644
index 0000000..bbdbf77
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='commands',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=300, verbose_name='命令标题')),
+ ('command', models.CharField(max_length=2000, verbose_name='命令')),
+ ('describe', models.CharField(max_length=300, verbose_name='命令描述')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+ ],
+ options={
+ 'verbose_name': '命令',
+ 'verbose_name_plural': '命令',
+ },
+ ),
+ migrations.CreateModel(
+ name='EmailSendLog',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('emailto', models.CharField(max_length=300, verbose_name='收件人')),
+ ('title', models.CharField(max_length=2000, verbose_name='邮件标题')),
+ ('content', models.TextField(verbose_name='邮件内容')),
+ ('send_result', models.BooleanField(default=False, verbose_name='结果')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ],
+ options={
+ 'verbose_name': '邮件发送log',
+ 'verbose_name_plural': '邮件发送log',
+ 'ordering': ['-created_time'],
+ },
+ ),
+ ]
diff --git a/doc/DjangoBlog/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/doc/DjangoBlog/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
new file mode 100644
index 0000000..4858857
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servermanager', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='emailsendlog',
+ options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
+ ),
+ migrations.RenameField(
+ model_name='commands',
+ old_name='created_time',
+ new_name='creation_time',
+ ),
+ migrations.RenameField(
+ model_name='commands',
+ old_name='last_mod_time',
+ new_name='last_modify_time',
+ ),
+ migrations.RenameField(
+ model_name='emailsendlog',
+ old_name='created_time',
+ new_name='creation_time',
+ ),
+ ]
diff --git a/doc/DjangoBlog/servermanager/migrations/__init__.py b/doc/DjangoBlog/servermanager/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doc/DjangoBlog/servermanager/models.py b/doc/DjangoBlog/servermanager/models.py
new file mode 100644
index 0000000..4326c65
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/models.py
@@ -0,0 +1,33 @@
+from django.db import models
+
+
+# Create your models here.
+class commands(models.Model):
+ title = models.CharField('命令标题', max_length=300)
+ command = models.CharField('命令', max_length=2000)
+ describe = models.CharField('命令描述', max_length=300)
+ creation_time = models.DateTimeField('创建时间', auto_now_add=True)
+ last_modify_time = models.DateTimeField('修改时间', auto_now=True)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ verbose_name = '命令'
+ verbose_name_plural = verbose_name
+
+
+class EmailSendLog(models.Model):
+ emailto = models.CharField('收件人', max_length=300)
+ title = models.CharField('邮件标题', max_length=2000)
+ content = models.TextField('邮件内容')
+ send_result = models.BooleanField('结果', default=False)
+ creation_time = models.DateTimeField('创建时间', auto_now_add=True)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ verbose_name = '邮件发送log'
+ verbose_name_plural = verbose_name
+ ordering = ['-creation_time']
diff --git a/doc/DjangoBlog/servermanager/robot.py b/doc/DjangoBlog/servermanager/robot.py
new file mode 100644
index 0000000..7b45736
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/robot.py
@@ -0,0 +1,187 @@
+import logging
+import os
+import re
+
+import jsonpickle
+from django.conf import settings
+from werobot import WeRoBot
+from werobot.replies import ArticlesReply, Article
+from werobot.session.filestorage import FileStorage
+
+from djangoblog.utils import get_sha256
+from servermanager.api.blogapi import BlogApi
+from servermanager.api.commonapi import ChatGPT, CommandHandler
+from .MemcacheStorage import MemcacheStorage
+
+robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN')
+ or 'lylinux', enable_session=True)
+memstorage = MemcacheStorage()
+if memstorage.is_available:
+ robot.config['SESSION_STORAGE'] = memstorage
+else:
+ if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')):
+ os.remove(os.path.join(settings.BASE_DIR, 'werobot_session'))
+ robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
+
+blogapi = BlogApi()
+cmd_handler = CommandHandler()
+logger = logging.getLogger(__name__)
+
+
+def convert_to_article_reply(articles, message):
+ reply = ArticlesReply(message=message)
+ from blog.templatetags.blog_tags import truncatechars_content
+ for post in articles:
+ imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body)
+ imgurl = ''
+ if imgs:
+ imgurl = imgs[0]
+ article = Article(
+ title=post.title,
+ description=truncatechars_content(post.body),
+ img=imgurl,
+ url=post.get_full_url()
+ )
+ reply.add_article(article)
+ return reply
+
+
+@robot.filter(re.compile(r"^\?.*"))
+def search(message, session):
+ s = message.content
+ searchstr = str(s).replace('?', '')
+ result = blogapi.search_articles(searchstr)
+ if result:
+ articles = list(map(lambda x: x.object, result))
+ reply = convert_to_article_reply(articles, message)
+ return reply
+ else:
+ return '没有找到相关文章。'
+
+
+@robot.filter(re.compile(r'^category\s*$', re.I))
+def category(message, session):
+ categorys = blogapi.get_category_lists()
+ content = ','.join(map(lambda x: x.name, categorys))
+ return '所有文章分类目录:' + content
+
+
+@robot.filter(re.compile(r'^recent\s*$', re.I))
+def recents(message, session):
+ articles = blogapi.get_recent_articles()
+ if articles:
+ reply = convert_to_article_reply(articles, message)
+ return reply
+ else:
+ return "暂时还没有文章"
+
+
+@robot.filter(re.compile('^help$', re.I))
+def help(message, session):
+ return '''欢迎关注!
+ 默认会与图灵机器人聊天~~
+ 你可以通过下面这些命令来获得信息
+ ?关键字搜索文章.
+ 如?python.
+ category获得文章分类目录及文章数.
+ category-***获得该分类目录文章
+ 如category-python
+ recent获得最新文章
+ help获得帮助.
+ weather:获得天气
+ 如weather:西安
+ idcard:获得身份证信息
+ 如idcard:61048119xxxxxxxxxx
+ music:音乐搜索
+ 如music:阴天快乐
+ PS:以上标点符号都不支持中文标点~~
+ '''
+
+
+@robot.filter(re.compile(r'^weather\:.*$', re.I))
+def weather(message, session):
+ return "建设中..."
+
+
+@robot.filter(re.compile(r'^idcard\:.*$', re.I))
+def idcard(message, session):
+ return "建设中..."
+
+
+@robot.handler
+def echo(message, session):
+ handler = MessageHandler(message, session)
+ return handler.handler()
+
+
+class MessageHandler:
+ def __init__(self, message, session):
+ userid = message.source
+ self.message = message
+ self.session = session
+ self.userid = userid
+ try:
+ info = session[userid]
+ self.userinfo = jsonpickle.decode(info)
+ except Exception as e:
+ userinfo = WxUserInfo()
+ self.userinfo = userinfo
+
+ @property
+ def is_admin(self):
+ return self.userinfo.isAdmin
+
+ @property
+ def is_password_set(self):
+ return self.userinfo.isPasswordSet
+
+ def save_session(self):
+ info = jsonpickle.encode(self.userinfo)
+ self.session[self.userid] = info
+
+ def handler(self):
+ info = self.message.content
+
+ if self.userinfo.isAdmin and info.upper() == 'EXIT':
+ self.userinfo = WxUserInfo()
+ self.save_session()
+ return "退出成功"
+ if info.upper() == 'ADMIN':
+ self.userinfo.isAdmin = True
+ self.save_session()
+ return "输入管理员密码"
+ if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
+ passwd = settings.WXADMIN
+ if settings.TESTING:
+ passwd = '123'
+ if passwd.upper() == get_sha256(get_sha256(info)).upper():
+ self.userinfo.isPasswordSet = True
+ self.save_session()
+ return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
+ else:
+ if self.userinfo.Count >= 3:
+ self.userinfo = WxUserInfo()
+ self.save_session()
+ return "超过验证次数"
+ self.userinfo.Count += 1
+ self.save_session()
+ return "验证失败,请重新输入管理员密码:"
+ if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
+ if self.userinfo.Command != '' and info.upper() == 'Y':
+ return cmd_handler.run(self.userinfo.Command)
+ else:
+ if info.upper() == 'HELPME':
+ return cmd_handler.get_help()
+ self.userinfo.Command = info
+ self.save_session()
+ return "确认执行: " + info + " 命令?"
+
+ return ChatGPT.chat(info)
+
+
+class WxUserInfo():
+ def __init__(self):
+ self.isAdmin = False
+ self.isPasswordSet = False
+ self.Count = 0
+ self.Command = ''
diff --git a/doc/DjangoBlog/servermanager/tests.py b/doc/DjangoBlog/servermanager/tests.py
new file mode 100644
index 0000000..22a6689
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/tests.py
@@ -0,0 +1,79 @@
+from django.test import Client, RequestFactory, TestCase
+from django.utils import timezone
+from werobot.messages.messages import TextMessage
+
+from accounts.models import BlogUser
+from blog.models import Category, Article
+from servermanager.api.commonapi import ChatGPT
+from .models import commands
+from .robot import MessageHandler, CommandHandler
+from .robot import search, category, recents
+
+
+# Create your tests here.
+class ServerManagerTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_chat_gpt(self):
+ content = ChatGPT.chat("你好")
+ self.assertIsNotNone(content)
+
+ def test_validate_comment(self):
+ user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ c = Category()
+ c.name = "categoryccc"
+ c.save()
+
+ article = Article()
+ article.title = "nicetitleccc"
+ article.body = "nicecontentccc"
+ article.author = user
+ article.category = c
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+ s = TextMessage([])
+ s.content = "nice"
+ rsp = search(s, None)
+ rsp = category(None, None)
+ self.assertIsNotNone(rsp)
+ rsp = recents(None, None)
+ self.assertTrue(rsp != '暂时还没有文章')
+
+ cmd = commands()
+ cmd.title = "test"
+ cmd.command = "ls"
+ cmd.describe = "test"
+ cmd.save()
+
+ cmdhandler = CommandHandler()
+ rsp = cmdhandler.run('test')
+ self.assertIsNotNone(rsp)
+ s.source = 'u'
+ s.content = 'test'
+ msghandler = MessageHandler(s, {})
+
+ # msghandler.userinfo.isPasswordSet = True
+ # msghandler.userinfo.isAdmin = True
+ msghandler.handler()
+ s.content = 'y'
+ msghandler.handler()
+ s.content = 'idcard:12321233'
+ msghandler.handler()
+ s.content = 'weather:上海'
+ msghandler.handler()
+ s.content = 'admin'
+ msghandler.handler()
+ s.content = '123'
+ msghandler.handler()
+
+ s.content = 'exit'
+ msghandler.handler()
diff --git a/doc/DjangoBlog/servermanager/urls.py b/doc/DjangoBlog/servermanager/urls.py
new file mode 100644
index 0000000..8d134d2
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path
+from werobot.contrib.django import make_view
+
+from .robot import robot
+
+app_name = "servermanager"
+urlpatterns = [
+ path(r'robot', make_view(robot)),
+
+]
diff --git a/doc/DjangoBlog/servermanager/views.py b/doc/DjangoBlog/servermanager/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/doc/DjangoBlog/servermanager/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/doc/DjangoBlog/templates/account/forget_password.html b/doc/DjangoBlog/templates/account/forget_password.html
new file mode 100644
index 0000000..3384531
--- /dev/null
+++ b/doc/DjangoBlog/templates/account/forget_password.html
@@ -0,0 +1,30 @@
+{% extends 'share_layout/base_account.html' %}
+{% load i18n %}
+{% load static %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/account/login.html b/doc/DjangoBlog/templates/account/login.html
new file mode 100644
index 0000000..cff8d33
--- /dev/null
+++ b/doc/DjangoBlog/templates/account/login.html
@@ -0,0 +1,46 @@
+{% extends 'share_layout/base_account.html' %}
+{% load static %}
+{% load i18n %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/account/registration_form.html b/doc/DjangoBlog/templates/account/registration_form.html
new file mode 100644
index 0000000..65e7549
--- /dev/null
+++ b/doc/DjangoBlog/templates/account/registration_form.html
@@ -0,0 +1,29 @@
+{% extends 'share_layout/base_account.html' %}
+{% load static %}
+{% block content %}
+
+
+
Create Your Account
+
+
+
+
+
+
+
+ Sign In
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/account/result.html b/doc/DjangoBlog/templates/account/result.html
new file mode 100644
index 0000000..23c9094
--- /dev/null
+++ b/doc/DjangoBlog/templates/account/result.html
@@ -0,0 +1,27 @@
+{% extends 'share_layout/base.html' %}
+{% load i18n %}
+{% block header %}
+ {{ title }}
+{% endblock %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/blog/article_archives.html b/doc/DjangoBlog/templates/blog/article_archives.html
new file mode 100644
index 0000000..959319e
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/article_archives.html
@@ -0,0 +1,60 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% load i18n %}
+{% block header %}
+
+ {% trans 'article archive' %} | {{ SITE_DESCRIPTION }}
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ {% regroup article_list by pub_time.year as year_post_group %}
+
+ {% for year in year_post_group %}
+ {{ year.grouper }} {% trans 'year' %}
+ {% regroup year.list by pub_time.month as month_post_group %}
+
+ {% for month in month_post_group %}
+ {{ month.grouper }} {% trans 'month' %}
+
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/doc/DjangoBlog/templates/blog/article_detail.html b/doc/DjangoBlog/templates/blog/article_detail.html
new file mode 100644
index 0000000..a74a0db
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/article_detail.html
@@ -0,0 +1,52 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+
+{% block header %}
+{% endblock %}
+{% block content %}
+
+
+ {% load_article_detail article False user %}
+
+ {% if article.type == 'a' %}
+
+ 文章导航
+ {% if next_article %}
+
+ ← {{ next_article.title }}
+ {% endif %}
+ {% if prev_article %}
+ {{ prev_article.title }} →
+ {% endif %}
+
+ {% endif %}
+
+
+ {% if article.comment_status == "o" and OPEN_SITE_COMMENT %}
+
+
+ {% include 'comments/tags/comment_list.html' %}
+ {% if user.is_authenticated %}
+ {% include 'comments/tags/post_comment.html' %}
+ {% else %}
+
+ {% endif %}
+ {% endif %}
+
+
+{% endblock %}
+
+{% block sidebar %}
+ {% load_sidebar user "p" %}
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/blog/article_index.html b/doc/DjangoBlog/templates/blog/article_index.html
new file mode 100644
index 0000000..0ee6150
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/article_index.html
@@ -0,0 +1,42 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+ {% if tag_name %}
+ {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }}
+ {% comment %} {% endcomment %}
+ {% else %}
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+ {% if page_type and tag_name %}
+
+ {% endif %}
+
+ {% for article in article_list %}
+ {% load_article_detail article True user %}
+ {% endfor %}
+ {% if is_paginated %}
+ {% load_pagination_info page_obj page_type tag_name %}
+
+ {% endif %}
+
+
+
+{% endblock %}
+{% block sidebar %}
+ {% load_sidebar user linktype %}
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/blog/error_page.html b/doc/DjangoBlog/templates/blog/error_page.html
new file mode 100644
index 0000000..d41cfb6
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/error_page.html
@@ -0,0 +1,45 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+ {% if tag_name %}
+ {% if statuscode == '404' %}
+ 404 NotFound
+ {% elif statuscode == '403' %}
+ Permission Denied
+ {% elif statuscode == '500' %}
+ 500 Error
+ {% else %}
+
+ {% endif %}
+ {% comment %} {% endcomment %}
+ {% else %}
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/doc/DjangoBlog/templates/blog/links_list.html b/doc/DjangoBlog/templates/blog/links_list.html
new file mode 100644
index 0000000..ccecbea
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/links_list.html
@@ -0,0 +1,44 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+
+ 友情链接 | {{ SITE_DESCRIPTION }}
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/doc/DjangoBlog/templates/blog/tags/article_info.html b/doc/DjangoBlog/templates/blog/tags/article_info.html
new file mode 100644
index 0000000..65b45fa
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/article_info.html
@@ -0,0 +1,79 @@
+{% load blog_tags %}
+{% load cache %}
+{% load i18n %}
+
+
+
+
+ {% if isindex %}
+ {% render_article_content article True %}
+
Read more
+ {% else %}
+
+ {% if article.show_toc %}
+ {% get_markdown_toc article.body as toc %}
+
{% trans 'toc' %}:
+ {{ toc|safe }}
+
+
+ {% endif %}
+
+
+ {% render_article_content article False %}
+
+
+ {% endif %}
+
+
+
+ {% load_article_metas article user %}
+
+
+
+
+{% if not isindex %}
+ {% render_plugin_widgets 'article_bottom' article=article %}
+{% endif %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/blog/tags/article_meta_info.html b/doc/DjangoBlog/templates/blog/tags/article_meta_info.html
new file mode 100644
index 0000000..ec8a0f9
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/article_meta_info.html
@@ -0,0 +1,57 @@
+{% load i18n %}
+{% load blog_tags %}
+
+
+
+
+
diff --git a/doc/DjangoBlog/templates/blog/tags/article_pagination.html b/doc/DjangoBlog/templates/blog/tags/article_pagination.html
new file mode 100644
index 0000000..95514ff
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/article_pagination.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
+
+ {% trans 'article navigation' %}
+
+ {% if page_obj.has_next and next_url%}
+
+ {% endif %}
+ {% if page_obj.has_previous and previous_url %}
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/blog/tags/article_tag_list.html b/doc/DjangoBlog/templates/blog/tags/article_tag_list.html
new file mode 100644
index 0000000..c8ba474
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/article_tag_list.html
@@ -0,0 +1,19 @@
+{% load i18n %}
+{% if article_tags_list %}
+
+{% endif %}
diff --git a/doc/DjangoBlog/templates/blog/tags/breadcrumb.html b/doc/DjangoBlog/templates/blog/tags/breadcrumb.html
new file mode 100644
index 0000000..67087d5
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/breadcrumb.html
@@ -0,0 +1,19 @@
+
+
+ {% for name,url in names %}
+
+
+ {{ name }}
+
+
+
+ {% endfor %}
+
+ {{ title }}
+
+
+
+
+
diff --git a/doc/DjangoBlog/templates/blog/tags/sidebar.html b/doc/DjangoBlog/templates/blog/tags/sidebar.html
new file mode 100644
index 0000000..ecb6d20
--- /dev/null
+++ b/doc/DjangoBlog/templates/blog/tags/sidebar.html
@@ -0,0 +1,136 @@
+{% load blog_tags %}
+{% load i18n %}
+
diff --git a/doc/DjangoBlog/templates/comments/tags/comment_item.html b/doc/DjangoBlog/templates/comments/tags/comment_item.html
new file mode 100644
index 0000000..0693649
--- /dev/null
+++ b/doc/DjangoBlog/templates/comments/tags/comment_item.html
@@ -0,0 +1,37 @@
+{% load blog_tags %}
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/comments/tags/comment_item_tree.html b/doc/DjangoBlog/templates/comments/tags/comment_item_tree.html
new file mode 100644
index 0000000..a407d76
--- /dev/null
+++ b/doc/DjangoBlog/templates/comments/tags/comment_item_tree.html
@@ -0,0 +1,57 @@
+{% load blog_tags %}
+
+{% query article_comments parent_comment=comment_item as cc_comments %}
+{% for cc in cc_comments %}
+ {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %}
+ {% if depth >= 1 %}
+ {% include template_name %}
+ {% else %}
+ {% with depth=depth|add:1 %}
+ {% include template_name %}
+ {% endwith %}
+ {% endif %}
+ {% endwith %}
+{% endfor %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/comments/tags/comment_list.html b/doc/DjangoBlog/templates/comments/tags/comment_list.html
new file mode 100644
index 0000000..4092161
--- /dev/null
+++ b/doc/DjangoBlog/templates/comments/tags/comment_list.html
@@ -0,0 +1,45 @@
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/comments/tags/post_comment.html b/doc/DjangoBlog/templates/comments/tags/post_comment.html
new file mode 100644
index 0000000..3ae5a27
--- /dev/null
+++ b/doc/DjangoBlog/templates/comments/tags/post_comment.html
@@ -0,0 +1,33 @@
+
+
+
diff --git a/doc/DjangoBlog/templates/oauth/bindsuccess.html b/doc/DjangoBlog/templates/oauth/bindsuccess.html
new file mode 100644
index 0000000..4bee77c
--- /dev/null
+++ b/doc/DjangoBlog/templates/oauth/bindsuccess.html
@@ -0,0 +1,22 @@
+{% extends 'share_layout/base.html' %}
+{% block header %}
+ {{ title }}
+{% endblock %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/oauth/oauth_applications.html b/doc/DjangoBlog/templates/oauth/oauth_applications.html
new file mode 100644
index 0000000..a841ad2
--- /dev/null
+++ b/doc/DjangoBlog/templates/oauth/oauth_applications.html
@@ -0,0 +1,13 @@
+{% load i18n %}
+
diff --git a/doc/DjangoBlog/templates/oauth/require_email.html b/doc/DjangoBlog/templates/oauth/require_email.html
new file mode 100644
index 0000000..3adef12
--- /dev/null
+++ b/doc/DjangoBlog/templates/oauth/require_email.html
@@ -0,0 +1,46 @@
+{% extends 'share_layout/base_account.html' %}
+
+{% load static %}
+{% block content %}
+
+
+
绑定您的邮箱账号
+
+
+ {% if picture %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+ 登录
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/owntracks/show_log_dates.html b/doc/DjangoBlog/templates/owntracks/show_log_dates.html
new file mode 100644
index 0000000..7dbba21
--- /dev/null
+++ b/doc/DjangoBlog/templates/owntracks/show_log_dates.html
@@ -0,0 +1,17 @@
+
+
+
+
+ 记录日期
+
+
+
+
+ {% for date in results %}
+
+ {{ date }}
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/owntracks/show_maps.html b/doc/DjangoBlog/templates/owntracks/show_maps.html
new file mode 100644
index 0000000..3aeda36
--- /dev/null
+++ b/doc/DjangoBlog/templates/owntracks/show_maps.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+ 运动轨迹
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/plugins/article_recommendation/__init__.py b/doc/DjangoBlog/templates/plugins/article_recommendation/__init__.py
new file mode 100644
index 0000000..7d86a99
--- /dev/null
+++ b/doc/DjangoBlog/templates/plugins/article_recommendation/__init__.py
@@ -0,0 +1 @@
+# 插件模板目录
diff --git a/doc/DjangoBlog/templates/plugins/article_recommendation/bottom_widget.html b/doc/DjangoBlog/templates/plugins/article_recommendation/bottom_widget.html
new file mode 100644
index 0000000..829b7b4
--- /dev/null
+++ b/doc/DjangoBlog/templates/plugins/article_recommendation/bottom_widget.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+
+
+ 📖 {{ title }}
+
+
+ {% for article in recommendations %}
+ {% if article.title and article.title|length > 0 %}
+
+ {% endif %}
+ {% endfor %}
+
+
diff --git a/doc/DjangoBlog/templates/plugins/article_recommendation/sidebar_widget.html b/doc/DjangoBlog/templates/plugins/article_recommendation/sidebar_widget.html
new file mode 100644
index 0000000..5f1afbf
--- /dev/null
+++ b/doc/DjangoBlog/templates/plugins/article_recommendation/sidebar_widget.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
diff --git a/doc/DjangoBlog/templates/plugins/css_includes.html b/doc/DjangoBlog/templates/plugins/css_includes.html
new file mode 100644
index 0000000..37029ae
--- /dev/null
+++ b/doc/DjangoBlog/templates/plugins/css_includes.html
@@ -0,0 +1,4 @@
+{% comment %}插件CSS文件包含模板 - 用于压缩{% endcomment %}
+{% for css_file in css_files %}
+
+{% endfor %}
diff --git a/doc/DjangoBlog/templates/plugins/js_includes.html b/doc/DjangoBlog/templates/plugins/js_includes.html
new file mode 100644
index 0000000..2a315e3
--- /dev/null
+++ b/doc/DjangoBlog/templates/plugins/js_includes.html
@@ -0,0 +1,4 @@
+{% comment %}插件JS文件包含模板 - 用于压缩{% endcomment %}
+{% for js_file in js_files %}
+
+{% endfor %}
diff --git a/doc/DjangoBlog/templates/search/indexes/blog/article_text.txt b/doc/DjangoBlog/templates/search/indexes/blog/article_text.txt
new file mode 100644
index 0000000..4f9ca76
--- /dev/null
+++ b/doc/DjangoBlog/templates/search/indexes/blog/article_text.txt
@@ -0,0 +1,3 @@
+{{ object.title }}
+{{ object.author.username }}
+{{ object.body }}
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/search/search.html b/doc/DjangoBlog/templates/search/search.html
new file mode 100644
index 0000000..1404c60
--- /dev/null
+++ b/doc/DjangoBlog/templates/search/search.html
@@ -0,0 +1,66 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% block header %}
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+ {% if query %}
+
+ {% endif %}
+ {% if query and page.object_list %}
+ {% for article in page.object_list %}
+ {% load_article_detail article.object True user %}
+ {% endfor %}
+ {% if page.has_previous or page.has_next %}
+
+ 文章导航
+ {% if page.has_previous %}
+
+ {% endif %}
+ {% if page.has_next %}
+
+ {% endif %}
+
+
+ {% endif %}
+ {% else %}
+
+ {% endif %}
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar request.user 'i' %}
+{% endblock %}
+
+
diff --git a/doc/DjangoBlog/templates/share_layout/adsense.html b/doc/DjangoBlog/templates/share_layout/adsense.html
new file mode 100644
index 0000000..8f99c55
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/adsense.html
@@ -0,0 +1,6 @@
+
+
+
+ {{ GOOGLE_ADSENSE_CODES }}
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/share_layout/base.html b/doc/DjangoBlog/templates/share_layout/base.html
new file mode 100644
index 0000000..bb17933
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/base.html
@@ -0,0 +1,119 @@
+{% load static %}
+{% load cache %}
+{% load i18n %}
+{% load compress %}
+
+
+
+
+
+
+
+
+
+
+
+ {% load blog_tags %}
+ {% head_meta %}
+ {% block header %}
+
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% compress css %}
+
+
+ {% comment %}{% endcomment %}
+
+
+
+ {% block compress_css %}
+ {% endblock %}
+
+ {% plugin_compressed_css %}
+ {% endcompress %}
+
+ {% if GLOBAL_HEADER %}
+ {{ GLOBAL_HEADER|safe }}
+ {% endif %}
+
+
+ {% plugin_head_resources %}
+
+
+
+
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+ {% block sidebar %}
+ {% endblock %}
+
+
+
+ {% include 'share_layout/footer.html' %}
+
+
+
+{% compress js %}
+
+
+
+
+ {% block compress_js %}
+ {% endblock %}
+
+ {% plugin_compressed_js %}
+{% endcompress %}
+
+
+
+
+{% block footer %}
+{% endblock %}
+
+
+{% plugin_body_resources %}
+
+
diff --git a/doc/DjangoBlog/templates/share_layout/base_account.html b/doc/DjangoBlog/templates/share_layout/base_account.html
new file mode 100644
index 0000000..c00d842
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/base_account.html
@@ -0,0 +1,47 @@
+
+
+
+ {% load static %}
+
+
+
+
+
+
+
+
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+
+ {% load compress %}
+ {% compress css %}
+
+
+
+
+
+
+
+
+
+ {% endcompress %}
+ {% compress js %}
+
+
+ {% endcompress %}
+
+
+
+
+
+{% block content %}
+{% endblock %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/share_layout/footer.html b/doc/DjangoBlog/templates/share_layout/footer.html
new file mode 100644
index 0000000..cd86a29
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/footer.html
@@ -0,0 +1,56 @@
+
+
+
diff --git a/doc/DjangoBlog/templates/share_layout/nav.html b/doc/DjangoBlog/templates/share_layout/nav.html
new file mode 100644
index 0000000..24d4da6
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/nav.html
@@ -0,0 +1,30 @@
+{% load i18n %}
+
+
+
+
\ No newline at end of file
diff --git a/doc/DjangoBlog/templates/share_layout/nav_node.html b/doc/DjangoBlog/templates/share_layout/nav_node.html
new file mode 100644
index 0000000..c266880
--- /dev/null
+++ b/doc/DjangoBlog/templates/share_layout/nav_node.html
@@ -0,0 +1,19 @@
+
+
+
diff --git a/doc/frrweek4work2 b/doc/frrweek4work2
new file mode 100644
index 0000000..a4f17ab
--- /dev/null
+++ b/doc/frrweek4work2
@@ -0,0 +1,43 @@
+PixUI 编译为 WebAssembly 适配微信小程序的界面与流转分析
+一、界面结构分层(从技术依赖到最终渲染)
+将界面从 “底层技术” 到 “用户可见界面” 拆分为 4 个层级,各层级核心内容与作用如下:
+1. 技术依赖层
+核心内容:PixUI(C# 编写)、Skia(C++ 图形引擎)
+核心作用:二者共同编译为 WebAssembly,作为界面渲染的 “逻辑与引擎基础”,为后续界面绘制提供底层技术支撑,决定了界面渲染的逻辑规则与图形处理能力。
+2. 资源文件层
+核心内容:包含_framework目录,内部有dotnet.js(41KB,微信小程序文件类型,3 月 14 日修改)、dotnet.native.js(152KB,微信小程序文件类型,当日 10:51 修改)、dotnet.runtime.js(195KB,微信小程序文件类型,3 月 14 日修改)、blazor.boot.json(17KB,JSON 文档,当日 10:51 修改)、dotnet.js.map(57KB,文档,3 月 14 日修改)、dotnet.native.wasm(2.9MB,文档,当日 10:51 修改),还有pkgs(当日 10:52 创建的文件夹)、supportFiles(当日 10:51 创建的文件夹)等。
+核心作用:存储 WebAssembly 运行所需的核心文件,是小程序加载 WASM 的 “资源载体”,这些文件共同保障 WebAssembly 能在微信小程序环境中正常启动与运行。
+3. 小程序代码层
+核心内容:微信小程序的 WXML(以index.wxml为例,包含标签,设置class="scrollarea"、scroll-y、type="list"等属性;标签,设置id="canvas"、type="webgl"、宽高为 390px 和 753px,样式为绝对定位、z-index:1,绑定bindtouchstart和bindtouchend事件; 标签,设置id="input"、类型为 text,样式为绝对定位、z-index:3、无边框、宽高为 0 等)、TS(index.ts)等代码。
+核心作用:充当 “桥梁层”,通过 Canvas 承载 WASM 渲染结果,同时绑定交互事件,实现用户操作与 WASM 逻辑的连接,是小程序与 WebAssembly 交互的关键环节。
+4. 用户可见层
+核心内容:最终呈现给用户的界面,包含图表(柱状图与折线图组合,带有 “Series #1 11”“Series #2 17” 等数据提示)、“Hello World!” 按钮等元素,界面在微信开发者工具模拟器(iPhone 12/13 (Pro) 型号,85% 比例,16 亮度)中展示。
+核心作用:是用户直接交互的 “视觉结果”,用户通过该层进行操作,如触摸图表查看数据详情、点击按钮触发相关功能,直观感受小程序的功能与体验。
+二、界面流转逻辑(从代码到渲染的全链路)
+以 “用户操作触发→代码响应→界面更新” 为线索,界面流转路径如下:
+1. 启动与加载阶段
+触发条件:微信小程序启动
+流转 1:加载_framework目录下的 WASM 资源,即上述资源文件层中的dotnet.native.wasm等核心文件,这些文件是 WebAssembly 运行的基础素材,加载过程为后续渲染做资源准备。
+流转 2:小程序代码(index.wxml/index.ts)初始化 Canvas 组件,根据index.wxml中对 Canvas 的属性设置,为其分配相应的尺寸、定位等,准备好界面渲染的载体,使 Canvas 具备承载 WASM 渲染结果的能力。
+流转 3:WebAssembly 中的 PixUI(C#)与 Skia(C++)开始执行,按照自身的逻辑规则,在已初始化好的 Canvas 上绘制界面,最终呈现出微信开发者工具模拟器中看到的图表、“Hello World!” 按钮等元素。
+2. 交互阶段(以 “触摸图表” 为例)
+触发条件:用户触摸模拟器中 Canvas 上的图表区域
+流转 1:小程序index.wxml中 Canvas 绑定的bindtouchstart/bindtouchend事件被触发,这是用户操作转化为代码响应的起始点,事件绑定机制确保用户触摸行为能被小程序捕捉。
+流转 2:触发的事件通过小程序代码(index.ts)传递到 WebAssembly 中的 PixUI(C#)逻辑层,index.ts在此过程中起到数据传递与逻辑衔接的作用,搭建起小程序与 WebAssembly 之间的通信桥梁。
+流转 3:C# 逻辑层接收事件后,处理触摸逻辑,比如判断触摸位置确定用户关注的图表数据点,然后生成 “Series #1 11”“Series #2 17” 等数据提示,并通知 Skia 重绘 Canvas,Skia 作为图形引擎,负责将处理后的界面效果重新绘制出来。
+流转 4:Canvas 重绘完成后,模拟器中的界面随之更新,图表提示动态变化,完成 “交互→反馈” 的完整流转,使用户能实时看到操作后的界面变化。
+三、核心界面元素与交互(用户可见层分析)
+1. 图表区域
+载体:Canvas 组件(对应小程序代码层index.wxml中的canvas标签)
+内容:由柱状图和折线图组合而成,并且带有 “Series #1 11”“Series #2 17” 等数据提示,图表能直观展示数据关系,数据提示则为用户提供具体数据信息。
+交互:支持用户触摸操作,当用户触摸图表时,触发相关事件,事件传递到 WebAssembly 中 C# 逻辑层进行处理,之后 Skia 重绘界面展示数据详情,使用户能精准获取触摸位置对应的图表数据。
+2. “Hello World!” 按钮
+载体:Canvas 上绘制的自定义按钮,并非微信原生按钮,由 PixUI 负责渲染生成,属于自定义界面元素。
+交互:理论上可绑定点击事件,不过该事件需在 WebAssembly 中通过 PixUI 逻辑实现,点击后可能触发数据更新(如生成新的图表数据)、新界面绘制(如展示新的界面元素)等操作,为用户提供功能触发入口。
+3. 小程序容器元素
+结构:由scroll-view标签包裹 Canvas(对应index.wxml中的scroll-view标签设置),scroll-view具备滚动功能。
+作用:提供页面滚动能力,当界面内容超出当前显示区域时,用户可通过滚动查看完整内容,确保长内容(若后续界面拓展增加更多元素)的良好浏览体验。
+四、设计特点总结
+“跨技术栈融合”:借助 WebAssembly 技术,成功桥接 C#/C++ 与微信小程序两个不同技术栈,实现 “用熟悉的 C#/C++ 语言开发,在微信小程序生态中运行” 的目标,打破技术栈之间的壁垒,让开发者能充分利用已有技术优势进行小程序开发。
+“Canvas 为核心载体”:摒弃以微信原生组件为主的传统界面构建思路,将 Canvas 作为核心载体,界面中的图表、按钮等元素均由 WASM 渲染生成,这种方式保证了界面的 “自定义性”(可根据需求灵活设计界面样式与交互)与 “跨端一致性”(在不同设备上呈现的界面效果更统一)。
+“事件桥接交互”:利用小程序原生事件(如bindtouchstart)与 WebAssembly 逻辑进行联动,用户操作先触发小程序原生事件,再通过代码传递到 WebAssembly 逻辑层处理,最后重绘界面反馈结果,实现 “原生容器 + 自定义交互” 的无缝衔接,既保留小程序的基础交互能力,又拓展了自定义交互的可能性。
\ No newline at end of file
diff --git a/doc/frrweek8work3 b/doc/frrweek8work3
new file mode 100644
index 0000000..2e4307e
--- /dev/null
+++ b/doc/frrweek8work3
@@ -0,0 +1,480 @@
+# 导入Django核心模块
+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 # 从blog应用导入Article模型(评论关联的文章)
+
+
+# 创建评论模型(继承Django的Model基类,所有数据库模型都需继承此类)
+class Comment(models.Model):
+ # 评论正文:TextField支持长文本,max_length=300限制最大长度为300字符
+ # '正文'是字段的verbose_name(在后台管理中显示的名称)
+ body = models.TextField('正文', max_length=300)
+
+ # 评论创建时间:DateTimeField存储日期时间
+ # default=now 表示默认值为当前时间(评论提交时自动记录)
+ # _('creation time') 是国际化翻译标记(可根据语言设置显示不同文字)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+
+ # 评论最后修改时间:用于记录评论是否被编辑过
+ # 初始默认值为创建时间,若后续编辑评论,需手动更新此字段
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ # 评论作者:关联Django用户模型(外键)
+ # settings.AUTH_USER_MODEL 是项目配置的用户模型(通常是Django内置User)
+ # on_delete=models.CASCADE 表示:若用户被删除,其所有评论也会被级联删除
+ # verbose_name=_('author') 用于后台显示和国际化
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE)
+
+ # 关联的文章:外键关联blog应用的Article模型
+ # 表示“这条评论属于哪篇文章”
+ # on_delete=models.CASCADE 表示:若文章被删除,其所有评论也会被级联删除
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE)
+
+ # 父评论:自关联外键,用于实现“评论回复”功能
+ # 'self' 表示关联当前模型(Comment自身)
+ # null=True, blank=True 表示可以为空(即顶级评论,不是回复)
+ # 例如:用户A评论文章(parent_comment为null),用户B回复A的评论(parent_comment指向A的评论)
+ parent_comment = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent comment'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+
+ # 是否启用:布尔值字段,用于控制评论是否显示在前台
+ # default=False 表示新评论默认不显示(需管理员审核后设为True)
+ # blank=False, null=False 强制该字段必须有值(不能空)
+ is_enable = models.BooleanField(_('enable'),
+ default=False, blank=False, null=False)
+
+ # 元数据配置:对模型的补充说明(不影响数据结构,影响Django处理方式)
+ class Meta:
+ ordering = ['-id'] # 排序规则:按id倒序(新评论在前,因为id自增)
+ verbose_name = _('comment') # 模型的单数名称(用于后台显示)
+ verbose_name_plural = verbose_name # 模型的复数名称(保持和单数一致)
+ get_latest_by = 'id' # 指定获取“最新记录”时按id字段排序
+
+ # 定义模型实例的字符串表示(在后台管理和打印对象时显示)
+ # 这里返回评论正文的前N个字符,方便识别不同评论
+ def __str__(self):
+ return self.body
+# 导入Django核心模块
+from django.conf import settings # 引入项目配置(如自定义用户模型)
+from django.db import models # 引入Django数据库模型基类
+from django.utils.timezone import now # 引入当前时间工具(带时区支持)
+from django.utils.translation import gettext_lazy as _ # 引入国际化翻译工具(支持多语言)
+
+from blog.models import Article # 从blog应用导入Article模型(评论需关联具体文章)
+
+
+# 定义评论模型(继承models.Model,所有Django数据库模型必须继承此类)
+class Comment(models.Model):
+ # 评论正文:TextField支持长文本,max_length=300限制最大长度(防止恶意刷屏)
+ # '正文'是字段在后台管理界面的显示名称
+ body = models.TextField('正文', max_length=300)
+
+ # 评论创建时间:DateTimeField存储日期时间
+ # default=now 表示默认值为评论提交时的时间(自动记录)
+ # _('creation time') 用于国际化(如切换语言时显示对应语言的“创建时间”)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+
+ # 评论最后修改时间:记录评论是否被编辑过
+ # 初始值为创建时间,若后续编辑评论,需手动更新此字段(可优化为自动更新)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ # 评论作者:外键关联用户模型(多对一关系)
+ # settings.AUTH_USER_MODEL 指向项目配置的用户模型(默认是Django内置的User)
+ # on_delete=models.CASCADE 表示:若用户账号被删除,其所有评论也会被级联删除
+ # verbose_name=_('author') 是后台显示名称(支持国际化)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE)
+
+ # 关联的文章:外键关联blog应用的Article模型(多对一关系)
+ # 表示“这条评论属于哪篇文章”
+ # on_delete=models.CASCADE 表示:若文章被删除,其所有评论也会被级联删除
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE)
+
+ # 父评论:自关联外键(评论可以回复其他评论)
+ # 'self' 表示关联当前模型(Comment自身)
+ # null=True, blank=True 允许为空(即“顶级评论”,不是回复任何评论)
+ # 例如:用户A评论文章(parent_comment为null),用户B回复A(parent_comment指向A的评论ID)
+ parent_comment = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent comment'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+
+ # 是否启用:控制评论是否在前台显示(审核机制)
+ # default=False 表示新评论默认“未启用”(需管理员审核通过后设为True)
+ # blank=False, null=False 强制该字段必须有值(不能为空)
+ is_enable = models.BooleanField(_('enable'),
+ default=False, blank=False, null=False)
+
+ # 元数据配置:定义模型的额外属性(不影响数据结构,影响Django处理方式)
+ class Meta:
+ ordering = ['-id'] # 排序规则:按id倒序(新评论在前,因为id是自增的)
+ verbose_name = _('comment') # 模型的单数名称(后台显示用,支持国际化)
+ verbose_name_plural = verbose_name # 模型的复数名称(保持与单数一致)
+ get_latest_by = 'id' # 指定“获取最新记录”时按id排序(与ordering一致)
+
+ # 定义模型实例的字符串表示(在后台管理、打印对象时显示)
+ # 返回评论正文,方便快速识别不同评论
+ def __str__(self):
+ return self.body
+# 导入Django核心模块
+from django.core.exceptions import ValidationError # 用于抛出验证错误(如评论关闭时)
+from django.http import HttpResponseRedirect # 用于重定向页面(如评论提交后跳回文章页)
+from django.shortcuts import get_object_or_404 # 用于查询对象,不存在则返回404
+from django.utils.decorators import method_decorator # 用于给类视图方法添加装饰器
+from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器(防止跨站请求伪造)
+from django.views.generic.edit import FormView # 基础表单处理视图(简化表单验证逻辑)
+
+# 导入其他应用模型和当前应用的表单、模型
+from accounts.models import BlogUser # 从accounts应用导入用户模型(评论作者)
+from blog.models import Article # 从blog应用导入文章模型(评论关联的文章)
+from .forms import CommentForm # 导入评论表单(用于验证用户输入)
+from .models import Comment # 导入评论模型(用于创建评论数据)
+
+
+class CommentPostView(FormView):
+ """
+ 评论提交视图:处理用户提交的评论,包含表单验证、评论创建、权限判断等逻辑
+ 继承FormView,无需手动编写表单渲染和基础验证代码,专注业务逻辑
+ """
+ form_class = CommentForm # 指定使用的表单类(CommentForm)
+ template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ """
+ 重写dispatch方法:给视图添加CSRF保护
+ dispatch是所有请求的入口方法,添加@csrf_protect确保POST请求经过CSRF验证
+ """
+ return super(CommentPostView, self).dispatch(*args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ """
+ 处理GET请求:当用户直接访问评论提交URL时,重定向到文章详情页的评论区
+ 避免用户通过GET方式提交评论(评论应通过POST提交)
+ """
+ article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 查询对应的文章
+ url = article.get_absolute_url() # 获取文章详情页的URL
+ return HttpResponseRedirect(url + "#comments") # 重定向到文章页的评论区锚点
+
+ def form_invalid(self, form):
+ """
+ 表单验证失败时的逻辑(如评论内容为空、长度超限等)
+ 重新渲染文章详情页,并传递错误的表单(显示验证错误信息)
+ """
+ article_id = self.kwargs['article_id'] # 获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 查询文章
+
+ # 渲染文章详情页,携带错误的表单和文章对象(模板中可显示错误信息)
+ return self.render_to_response({
+ 'form': form, # 验证失败的表单(含错误信息)
+ 'article': article # 文章对象(用于显示文章内容)
+ })
+
+ def form_valid(self, form):
+ """
+ 表单验证成功后的核心逻辑:创建评论并保存到数据库
+ """
+ # 获取当前登录用户(评论作者)
+ user = self.request.user
+ author = BlogUser.objects.get(pk=user.pk) # 从自定义用户模型中查询用户
+
+ # 获取URL参数中的文章ID,并查询对应的文章
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ # 检查文章是否允许评论:若文章评论关闭或状态为草稿,则抛出验证错误
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.")
+
+ # 保存表单数据但不提交到数据库(commit=False),便于后续补充字段
+ comment = form.save(False)
+ comment.article = article # 关联评论到当前文章
+
+ # 获取博客全局设置(判断评论是否需要审核)
+ from djangoblog.utils import get_blog_setting
+ settings = get_blog_setting()
+ if not settings.comment_need_review: # 若评论无需审核
+ comment.is_enable = True # 直接设为“启用”(前台可见)
+
+ comment.author = author # 关联评论到当前用户
+
+ # 处理回复功能:若表单中包含父评论ID,则设置为回复
+ if form.cleaned_data['parent_comment_id']:
+ parent_comment = Comment.objects.get(
+ pk=form.cleaned_data['parent_comment_id']
+ )
+ comment.parent_comment = parent_comment # 关联到父评论
+
+ # 最终保存评论到数据库(commit=True)
+ comment.save(True)
+
+ # 评论提交成功后,重定向到文章详情页的该评论位置(锚点定位)
+ return HttpResponseRedirect(
+ "%s#div-comment-%d" % (article.get_absolute_url(), comment.pk)
+ )
+# 导入必要的模块和类
+from django.core.exceptions import ValidationError # 用于抛出验证错误(如评论关闭时)
+from django.http import HttpResponseRedirect # 用于重定向页面(评论提交后跳回文章页)
+from django.shortcuts import get_object_or_404 # 查询对象,不存在则返回404错误
+from django.utils.decorators import method_decorator # 为类视图方法添加装饰器
+from django.views.decorators.csrf import csrf_protect # CSRF保护(防止跨站请求伪造)
+from django.views.generic.edit import FormView # 表单处理基类(简化表单验证流程)
+
+# 导入关联模型和表单
+from accounts.models import BlogUser # 自定义用户模型(评论作者作者)
+from blog.models import Article # 博客文章模型(评论关联的文章)
+from .forms import CommentForm # 评论表单(用于验证用户输入)
+from .models import Comment # 评论模型(用于创建和保存评论)
+
+
+class CommentPostView(FormView):
+ """
+ 评论提交视图:处理用户评论的提交、验证、保存逻辑
+ 继承FormView,复用表单渲染、验证等基础功能,专注业务逻辑
+ """
+ form_class = CommentForm # 指定使用的表单类(CommentForm)
+ template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ """
+ 重写dispatch方法:为视图添加CSRF保护
+ dispatch是所有请求的入口,确保POST请求经过CSRF验证,防止跨站攻击
+ """
+ return super().dispatch(*args, **kwargs) # 调用父类方法,保持原有逻辑
+
+ def get(self, request, *args, **kwargs):
+ """
+ 处理GET请求:当用户直接通过URL访问评论提交地址时
+ 重定向到文章详情页的评论区,避免GET方式提交评论(评论需通过POST提交)
+ """
+ article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 查询对应的文章
+ url = article.get_absolute_url() # 获取文章详情页的URL
+ return HttpResponseRedirect(f"{url}#comments") # 重定向到评论区锚点
+
+ def form_invalid(self, form):
+ """
+ 表单验证失败时的处理(如评论内容为空、长度超限等)
+ 重新渲染文章详情页,并传递错误的表单,在页面上显示验证错误
+ """
+ article_id = self.kwargs['article_id'] # 获取文章ID
+ article = get_object_or_404(Article, pk=article_id) # 查询文章
+
+ # 渲染文章详情页,携带错误表单和文章对象(模板中可显示错误信息)
+ return self.render_to_response({
+ 'form': form, # 验证失败的表单(含错误信息)
+ 'article': article # 文章对象(用于显示文章内容)
+ })
+
+ def form_valid(self, form):
+ """
+ 表单验证成功后的核心逻辑:创建评论并保存到数据库
+ """
+ # 获取当前登录用户(评论作者)
+ user = self.request.user
+ author = BlogUser.objects.get(pk=user.pk) # 从自定义用户模型中查询用户
+
+ # 获取URL参数中的文章ID,并查询对应的文章
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ # 检查文章是否允许评论:若文章评论关闭(comment_status='c')或状态为草稿(status='c')
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.") # 抛出验证错误
+
+ # 保存表单数据但不提交到数据库(commit=False),先补充其他字段
+ comment = form.save(commit=False)
+ comment.article = article # 关联评论到当前文章
+
+ # 获取博客全局设置(判断评论是否需要审核)
+ from djangoblog.utils import get_blog_setting # 导入全局设置工具函数
+ settings = get_blog_setting()
+ if not settings.comment_need_review: # 若评论无需审核
+ comment.is_enable = True # 直接设为“启用”(前台可见)
+
+ comment.author = author # 关联评论到当前用户
+
+ # 处理回复功能:若表单中包含父评论ID,则设置为回复
+ parent_comment_id = form.cleaned_data.get('parent_comment_id')
+ if parent_comment_id:
+ parent_comment = Comment.objects.get(pk=parent_comment_id)
+ comment.parent_comment = parent_comment # 关联到父评论
+
+ # 最终保存评论到数据库
+ comment.save()
+
+ # 评论提交成功后,重定向到文章详情页的该评论位置(通过锚点定位)
+ return HttpResponseRedirect(
+ f"{article.get_absolute_url()}#div-comment-{comment.pk}"
+ )
+# 导入Django的URL路径处理模块
+from django.urls import path
+
+# 导入当前应用的视图模块(views.py)
+from . import views
+
+# 定义应用命名空间(app_name),用于在模板中通过命名空间引用URL,避免不同应用的URL名称冲突
+app_name = "comments"
+
+# 定义URL路由列表,每个path对应一个视图
+urlpatterns = [
+ # 评论提交的URL路由
+ path(
+ 'article//postcomment', # URL路径规则
+ views.CommentPostView.as_view(), # 对应的视图类(转换为可调用的视图函数)
+ name='postcomment' # 路由名称(用于模板中反向解析URL)
+ ),
+]
+# 导入Django测试工具和核心模块
+from django.test import Client, RequestFactory, TransactionTestCase # 测试客户端、请求工厂、事务测试基类
+from django.urls import reverse # 用于反向解析URL
+
+# 导入关联模型、模板标签和工具函数
+from accounts.models import BlogUser # 自定义用户模型
+from blog.models import Category, Article # 博客分类、文章模型
+from comments.models import Comment # 评论模型
+from comments.templatetags.comments_tags import * # 评论相关的模板标签(用于测试模板渲染逻辑)
+from djangoblog.utils import get_max_articleid_commentid # 获取最大文章/评论ID的工具函数
+
+
+# 定义评论测试类(继承TransactionTestCase,支持事务回滚,避免测试数据污染)
+class CommentsTest(TransactionTestCase):
+ def setUp(self):
+ """
+ 测试前的初始化工作:创建测试客户端、测试用户、博客设置等
+ 所有测试方法执行前会自动调用
+ """
+ self.client = Client() # 创建测试客户端(模拟用户浏览器请求)
+ self.factory = RequestFactory() # 创建请求工厂(用于构造复杂请求对象)
+
+ # 初始化博客全局设置(评论需要审核)
+ from blog.models import BlogSettings
+ value = BlogSettings()
+ value.comment_need_review = True # 评论需要审核(默认不显示)
+ value.save()
+
+ # 创建超级用户(用于测试登录状态下的评论提交)
+ self.user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ def update_article_comment_status(self, article):
+ """辅助方法:将文章的所有评论设为“启用”(绕过审核,方便测试评论列表显示)"""
+ comments = article.comment_set.all() # 获取文章的所有评论
+ for comment in comments:
+ comment.is_enable = True # 设为启用
+ comment.save() # 保存修改
+
+ def test_validate_comment(self):
+ """
+ 核心测试方法:验证评论提交、显示、回复等功能的正确性
+ 涵盖正常评论、带格式的回复、评论列表数量等场景
+ """
+ # 1. 登录测试用户
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ # 2. 创建测试分类和文章(评论必须关联文章)
+ category = Category()
+ category.name = "categoryccc" # 分类名称
+ category.save()
+
+ article = Article()
+ article.title = "nicetitleccc" # 文章标题
+ article.body = "nicecontentccc" # 文章内容
+ article.author = self.user # 关联作者
+ article.category = category # 关联分类
+ article.type = 'a' # 文章类型(假设'a'表示普通文章)
+ article.status = 'p' # 状态(假设'p'表示已发布)
+ article.save()
+
+ # 3. 测试首次提交评论
+ # 反向解析评论提交URL(使用命名空间和文章ID)
+ comment_url = reverse(
+ 'comments:postcomment', kwargs={'article_id': article.id})
+
+ # 发送POST请求提交评论(内容为'123ffffffffff')
+ response = self.client.post(comment_url, {'body': '123ffffffffff'})
+
+ # 验证:提交成功应重定向(状态码302)
+ self.assertEqual(response.status_code, 302)
+
+ # 验证:因评论需要审核(is_enable默认False),评论列表应为空
+ article = Article.objects.get(pk=article.pk) # 重新查询文章(刷新数据)
+ self.assertEqual(len(article.comment_list()), 0) # 假设comment_list()返回启用的评论
+
+ # 手动启用所有评论(模拟审核通过)
+ self.update_article_comment_status(article)
+ # 验证:启用后评论列表数量应为1
+ self.assertEqual(len(article.comment_list()), 1)
+
+ # 4. 测试再次提交评论(验证多条评论的情况)
+ response = self.client.post(comment_url, {'body': '123ffffffffff'})
+ self.assertEqual(response.status_code, 302) # 重定向成功
+
+ # 启用评论后验证数量为2
+ article = Article.objects.get(pk=article.pk)
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 2)
+
+ # 5. 测试回复功能(带格式的回复内容)
+ # 获取第一条评论的ID作为父评论
+ parent_comment_id = article.comment_list()[0].id
+
+ # 提交带格式的回复(包含Markdown语法:标题、代码块、链接)
+ response = self.client.post(comment_url, {
+ 'body': '''
+ # Title1
+
+ ```python
+ import os
+ # 导入Django表单基础模块
+from django import forms
+from django.forms import ModelForm # 导入模型表单基类(可直接关联数据库模型)
+
+# 导入当前应用的评论模型
+from .models import Comment
+
+
+class CommentForm(ModelForm):
+ """
+ 评论表单类:继承ModelForm,自动关联Comment模型,简化表单字段定义和验证
+ 用于处理用户提交的评论内容及回复关系
+ """
+ # 自定义字段:父评论ID(用于实现回复功能)
+ # IntegerField:存储父评论的ID(整数类型)
+ # widget=forms.HiddenInput:隐藏输入框(不在页面显示,通过前端JS动态设置值)
+ # required=False:允许为空(表示“顶级评论”,不是回复任何评论)
+ parent_comment_id = forms.IntegerField(
+ widget=forms.HiddenInput,
+ required=False
+ )
+
+ # 元数据配置:关联模型及字段映射
+ class Meta:
+ model = Comment # 指定关联的模型(Comment)
+ fields = ['body'] # 需处理的模型字段(仅包含评论正文body)
+ # 说明:其他字段(如author、article、creation_time等)不通过表单提交,
+ # 而是在视图中通过后端逻辑自动填充(避免用户篡改)
\ No newline at end of file
diff --git a/doc/mkweek4work2订正.docx b/doc/mkweek4work2订正.docx
new file mode 100644
index 0000000..f9fca48
Binary files /dev/null and b/doc/mkweek4work2订正.docx differ
diff --git a/doc/mkweek6work.docx b/doc/mkweek6work.docx
new file mode 100644
index 0000000..6d7c831
Binary files /dev/null and b/doc/mkweek6work.docx differ
diff --git a/doc/sjtweek4work1.docx b/doc/sjtweek4work1.docx
new file mode 100644
index 0000000..bf1d13c
Binary files /dev/null and b/doc/sjtweek4work1.docx differ
diff --git a/doc/sjtweek4work3订正.docx b/doc/sjtweek4work3订正.docx
new file mode 100644
index 0000000..50f9289
Binary files /dev/null and b/doc/sjtweek4work3订正.docx differ
diff --git a/doc/sjtweek5work订正.docx b/doc/sjtweek5work订正.docx
new file mode 100644
index 0000000..3bb9cc8
Binary files /dev/null and b/doc/sjtweek5work订正.docx differ
diff --git a/doc/week4work3 b/doc/week4work3
new file mode 100644
index 0000000..b02fe13
--- /dev/null
+++ b/doc/week4work3
@@ -0,0 +1,21 @@
+要分析 Django 模板的继承、包含关系及与模板标签(Tag)的依赖,并通过 UML 包图呈现,需从包结构和关系类型(泛化、依赖)两方面拆解:
+1. 核心概念与关系类型
+模板继承(泛化 Generalization):子模板通过 {% extends %} 继承基础模板的整体结构(如页面骨架、公共区块)。
+模板包含(依赖 Dependency):模板通过 {% include %} 嵌入可复用的组件模板(如导航栏、侧边栏),属于 “使用” 关系。
+模板与标签的依赖(依赖 Dependency):模板使用 {% static %}、{% url %} 等模板标签时,依赖标签库提供的功能。
+2. UML 包图设计
+以 “博客园文章详情页” 的模板体系为例,设计包结构与关系:
+(1)包结构
+顶层包 Templates:管理所有模板相关资源;
+子包 BaseTemplates:存放基础模板(定义页面通用骨架,如 base.html);
+子包 ComponentTemplates:存放组件模板(可复用的局部模块,如 header.html 导航栏、sidebar.html 侧边栏);
+子包 ArticleTemplates:存放业务模板(文章详情等场景化模板,如 article_detail.html);
+子包 TagLibrary:存放模板标签库(提供 static、url 等内置 / 自定义标签)。
+(2)关系映射
+1. 继承(泛化):ArticleTemplates::article_detail.html 继承 BaseTemplates::base.html(子模板复用基础模板的整体结构)。
+2. 包含(依赖):BaseTemplates::base.html 包含 ComponentTemplates::header.html(嵌入导航栏)和 ComponentTemplates::sidebar.html(嵌入侧边栏)。
+3. 模板与标签的依赖:BaseTemplates::base.html(引入静态 CSS/JS)和 ArticleTemplates::article_detail.html(生成页面链接)都依赖 TagLibrary(使用 static、url 等标签)。
+4. 关系解释
+泛化(继承):箭头从子模板 article_detail.html 指向父模板 base.html,表示子模板复用父模板的结构(如 、公共导航、布局框架);
+依赖(包含):虚线箭头从 base.html 指向 header.html/sidebar.html,表示 base.html 通过 {% include %} 嵌入这些组件模板;
+依赖(标签):虚线箭头从模板(base.html/article_detail.html)指向 TagLibrary,表示模板通过 {% static %}/{% url %} 等标签依赖标签库的功能。
\ No newline at end of file
diff --git a/doc/week5work.docx b/doc/week5work.docx
new file mode 100644
index 0000000..0c238ef
Binary files /dev/null and b/doc/week5work.docx differ
diff --git a/doc/编码规范.docx b/doc/编码规范.docx
new file mode 100644
index 0000000..c64bfbd
Binary files /dev/null and b/doc/编码规范.docx differ
diff --git a/src/新建 文本文档.txt b/src/新建 文本文档.txt
new file mode 100644
index 0000000..e69de29
+ {# {% query article_comments parent_comment=None as parent_comments %}#} + {% for comment_item in p_comments %} + + {% with 0 as depth %} + {% include "comments/tags/comment_item_tree.html" %} + {% endwith %} + {% endfor %} + +
++