Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
@ -1,18 +0,0 @@
|
|||||||
<!--
|
|
||||||
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
|
|
||||||
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
|
|
||||||
-->
|
|
||||||
|
|
||||||
**我确定我已经查看了** (标注`[ ]`为`[x]`)
|
|
||||||
|
|
||||||
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
|
|
||||||
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
|
|
||||||
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
**我要申请** (标注`[ ]`为`[x]`)
|
|
||||||
|
|
||||||
- [ ] BUG 反馈
|
|
||||||
- [ ] 添加新的特性或者功能
|
|
||||||
- [ ] 请求技术支持
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,371 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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}}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="FacetManager">
|
|
||||||
<facet type="django" name="Django">
|
|
||||||
<configuration>
|
|
||||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
|
||||||
<option name="settingsModule" value="djangoblog/settings.py" />
|
|
||||||
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
|
||||||
<option name="environment" value="<map/>" />
|
|
||||||
<option name="doNotUseTestRunner" value="false" />
|
|
||||||
<option name="trackFilePattern" value="migrations" />
|
|
||||||
</configuration>
|
|
||||||
</facet>
|
|
||||||
</component>
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
<component name="PyDocumentationSettings">
|
|
||||||
<option name="format" value="PLAIN" />
|
|
||||||
<option name="myDocStringFormat" value="Plain" />
|
|
||||||
</component>
|
|
||||||
<component name="TemplatesService">
|
|
||||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
|
||||||
<option name="TEMPLATE_FOLDERS">
|
|
||||||
<list>
|
|
||||||
<option value="$MODULE_DIR$/templates" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="Python 3.10" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog2.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog2.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
<mapping directory="$PROJECT_DIR$/src/DjangoBlog" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
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 *
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoBlogAdminSite(AdminSite):
|
|
||||||
site_header = 'djangoblog administration'
|
|
||||||
site_title = 'djangoblog site admin'
|
|
||||||
|
|
||||||
def __init__(self, name='admin'):
|
|
||||||
super().__init__(name)
|
|
||||||
|
|
||||||
def has_permission(self, request):
|
|
||||||
return request.user.is_superuser
|
|
||||||
|
|
||||||
# def get_urls(self):
|
|
||||||
# urls = super().get_urls()
|
|
||||||
# from django.urls import path
|
|
||||||
# from blog.views import refresh_memcache
|
|
||||||
#
|
|
||||||
# my_urls = [
|
|
||||||
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
|
|
||||||
# ]
|
|
||||||
# return urls + my_urls
|
|
||||||
|
|
||||||
|
|
||||||
admin_site = DjangoBlogAdminSite(name='admin')
|
|
||||||
|
|
||||||
admin_site.register(Article, ArticlelAdmin)
|
|
||||||
admin_site.register(Category, CategoryAdmin)
|
|
||||||
admin_site.register(Tag, TagAdmin)
|
|
||||||
admin_site.register(Links, LinksAdmin)
|
|
||||||
admin_site.register(SideBar, SideBarAdmin)
|
|
||||||
admin_site.register(BlogSettings, BlogSettingsAdmin)
|
|
||||||
|
|
||||||
admin_site.register(commands, CommandsAdmin)
|
|
||||||
admin_site.register(EmailSendLog, EmailSendLogAdmin)
|
|
||||||
|
|
||||||
admin_site.register(BlogUser, BlogUserAdmin)
|
|
||||||
|
|
||||||
admin_site.register(Comment, CommentAdmin)
|
|
||||||
|
|
||||||
admin_site.register(OAuthUser, OAuthUserAdmin)
|
|
||||||
admin_site.register(OAuthConfig, OAuthConfigAdmin)
|
|
||||||
|
|
||||||
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
|
|
||||||
|
|
||||||
admin_site.register(Site, SiteAdmin)
|
|
||||||
|
|
||||||
admin_site.register(LogEntry, LogEntryAdmin)
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
class DjangoblogAppConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'djangoblog'
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
super().ready()
|
|
||||||
# Import and load plugins here
|
|
||||||
from .plugin_manage.loader import load_plugins
|
|
||||||
load_plugins()
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import _thread
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import django.dispatch
|
|
||||||
from django.conf import settings
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
oauth_user_login_signal = django.dispatch.Signal(['id'])
|
|
||||||
send_email_signal = django.dispatch.Signal(
|
|
||||||
['emailto', 'title', 'content'])
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(send_email_signal)
|
|
||||||
def send_email_signal_handler(sender, **kwargs):
|
|
||||||
emailto = kwargs['emailto']
|
|
||||||
title = kwargs['title']
|
|
||||||
content = kwargs['content']
|
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
||||||
to=emailto)
|
|
||||||
msg.content_subtype = "html"
|
|
||||||
|
|
||||||
from servermanager.models import EmailSendLog
|
|
||||||
log = EmailSendLog()
|
|
||||||
log.title = title
|
|
||||||
log.content = content
|
|
||||||
log.emailto = ','.join(emailto)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = msg.send()
|
|
||||||
log.send_result = result > 0
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"失败邮箱号: {emailto}, {e}")
|
|
||||||
log.send_result = False
|
|
||||||
log.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(oauth_user_login_signal)
|
|
||||||
def oauth_user_login_signal_handler(sender, **kwargs):
|
|
||||||
id = kwargs['id']
|
|
||||||
oauthuser = OAuthUser.objects.get(id=id)
|
|
||||||
site = get_current_site().domain
|
|
||||||
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
|
|
||||||
from djangoblog.utils import save_user_avatar
|
|
||||||
oauthuser.picture = save_user_avatar(oauthuser.picture)
|
|
||||||
oauthuser.save()
|
|
||||||
|
|
||||||
delete_sidebar_cache()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
|
||||||
def model_post_save_callback(
|
|
||||||
sender,
|
|
||||||
instance,
|
|
||||||
created,
|
|
||||||
raw,
|
|
||||||
using,
|
|
||||||
update_fields,
|
|
||||||
**kwargs):
|
|
||||||
clearcache = False
|
|
||||||
if isinstance(instance, LogEntry):
|
|
||||||
return
|
|
||||||
if 'get_full_url' in dir(instance):
|
|
||||||
is_update_views = update_fields == {'views'}
|
|
||||||
if not settings.TESTING and not is_update_views:
|
|
||||||
try:
|
|
||||||
notify_url = instance.get_full_url()
|
|
||||||
SpiderNotify.baidu_notify([notify_url])
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error("notify sipder", ex)
|
|
||||||
if not is_update_views:
|
|
||||||
clearcache = True
|
|
||||||
|
|
||||||
if isinstance(instance, Comment):
|
|
||||||
if instance.is_enable:
|
|
||||||
path = instance.article.get_absolute_url()
|
|
||||||
site = get_current_site().domain
|
|
||||||
if site.find(':') > 0:
|
|
||||||
site = site[0:site.find(':')]
|
|
||||||
|
|
||||||
expire_view_cache(
|
|
||||||
path,
|
|
||||||
servername=site,
|
|
||||||
serverport=80,
|
|
||||||
key_prefix='blogdetail')
|
|
||||||
if cache.get('seo_processor'):
|
|
||||||
cache.delete('seo_processor')
|
|
||||||
comment_cache_key = 'article_comments_{id}'.format(
|
|
||||||
id=instance.article.id)
|
|
||||||
cache.delete(comment_cache_key)
|
|
||||||
delete_sidebar_cache()
|
|
||||||
delete_view_cache('article_comments', [str(instance.article.pk)])
|
|
||||||
|
|
||||||
_thread.start_new_thread(send_comment_email, (instance,))
|
|
||||||
|
|
||||||
if clearcache:
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_in)
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def user_auth_callback(sender, request, user, **kwargs):
|
|
||||||
if user and user.username:
|
|
||||||
logger.info(user)
|
|
||||||
delete_sidebar_cache()
|
|
||||||
# cache.clear()
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
from django.utils.encoding import force_str
|
|
||||||
from elasticsearch_dsl import Q
|
|
||||||
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
|
||||||
from haystack.forms import ModelSearchForm
|
|
||||||
from haystack.models import SearchResult
|
|
||||||
from haystack.utils import log as logging
|
|
||||||
|
|
||||||
from blog.documents import ArticleDocument, ArticleDocumentManager
|
|
||||||
from blog.models import Article
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchBackend(BaseSearchBackend):
|
|
||||||
def __init__(self, connection_alias, **connection_options):
|
|
||||||
super(
|
|
||||||
ElasticSearchBackend,
|
|
||||||
self).__init__(
|
|
||||||
connection_alias,
|
|
||||||
**connection_options)
|
|
||||||
self.manager = ArticleDocumentManager()
|
|
||||||
self.include_spelling = True
|
|
||||||
|
|
||||||
def _get_models(self, iterable):
|
|
||||||
models = iterable if iterable and iterable[0] else Article.objects.all()
|
|
||||||
docs = self.manager.convert_to_doc(models)
|
|
||||||
return docs
|
|
||||||
|
|
||||||
def _create(self, models):
|
|
||||||
self.manager.create_index()
|
|
||||||
docs = self._get_models(models)
|
|
||||||
self.manager.rebuild(docs)
|
|
||||||
|
|
||||||
def _delete(self, models):
|
|
||||||
for m in models:
|
|
||||||
m.delete()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _rebuild(self, models):
|
|
||||||
models = models if models else Article.objects.all()
|
|
||||||
docs = self.manager.convert_to_doc(models)
|
|
||||||
self.manager.update_docs(docs)
|
|
||||||
|
|
||||||
def update(self, index, iterable, commit=True):
|
|
||||||
|
|
||||||
models = self._get_models(iterable)
|
|
||||||
self.manager.update_docs(models)
|
|
||||||
|
|
||||||
def remove(self, obj_or_string):
|
|
||||||
models = self._get_models([obj_or_string])
|
|
||||||
self._delete(models)
|
|
||||||
|
|
||||||
def clear(self, models=None, commit=True):
|
|
||||||
self.remove(None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_suggestion(query: str) -> str:
|
|
||||||
"""获取推荐词, 如果没有找到添加原搜索词"""
|
|
||||||
|
|
||||||
search = ArticleDocument.search() \
|
|
||||||
.query("match", body=query) \
|
|
||||||
.suggest('suggest_search', query, term={'field': 'body'}) \
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
keywords = []
|
|
||||||
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
|
|
||||||
def search(self, query_string, **kwargs):
|
|
||||||
logger.info('search query_string:' + query_string)
|
|
||||||
|
|
||||||
start_offset = kwargs.get('start_offset')
|
|
||||||
end_offset = kwargs.get('end_offset')
|
|
||||||
|
|
||||||
# 推荐词搜索
|
|
||||||
if getattr(self, "is_suggest", None):
|
|
||||||
suggestion = self.get_suggestion(query_string)
|
|
||||||
else:
|
|
||||||
suggestion = query_string
|
|
||||||
|
|
||||||
q = Q('bool',
|
|
||||||
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
|
||||||
minimum_should_match="70%")
|
|
||||||
|
|
||||||
search = ArticleDocument.search() \
|
|
||||||
.query('bool', filter=[q]) \
|
|
||||||
.filter('term', status='p') \
|
|
||||||
.filter('term', type='a') \
|
|
||||||
.source(False)[start_offset: end_offset]
|
|
||||||
|
|
||||||
results = search.execute()
|
|
||||||
hits = results['hits'].total
|
|
||||||
raw_results = []
|
|
||||||
for raw_result in results['hits']['hits']:
|
|
||||||
app_label = 'blog'
|
|
||||||
model_name = 'Article'
|
|
||||||
additional_fields = {}
|
|
||||||
|
|
||||||
result_class = SearchResult
|
|
||||||
|
|
||||||
result = result_class(
|
|
||||||
app_label,
|
|
||||||
model_name,
|
|
||||||
raw_result['_id'],
|
|
||||||
raw_result['_score'],
|
|
||||||
**additional_fields)
|
|
||||||
raw_results.append(result)
|
|
||||||
facets = {}
|
|
||||||
spelling_suggestion = None if query_string == suggestion else suggestion
|
|
||||||
|
|
||||||
return {
|
|
||||||
'results': raw_results,
|
|
||||||
'hits': hits,
|
|
||||||
'facets': facets,
|
|
||||||
'spelling_suggestion': spelling_suggestion,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchQuery(BaseSearchQuery):
|
|
||||||
def _convert_datetime(self, date):
|
|
||||||
if hasattr(date, 'hour'):
|
|
||||||
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
|
||||||
else:
|
|
||||||
return force_str(date.strftime('%Y%m%d000000'))
|
|
||||||
|
|
||||||
def clean(self, query_fragment):
|
|
||||||
"""
|
|
||||||
Provides a mechanism for sanitizing user input before presenting the
|
|
||||||
value to the backend.
|
|
||||||
|
|
||||||
Whoosh 1.X differs here in that you can no longer use a backslash
|
|
||||||
to escape reserved characters. Instead, the whole word should be
|
|
||||||
quoted.
|
|
||||||
"""
|
|
||||||
words = query_fragment.split()
|
|
||||||
cleaned_words = []
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
if word in self.backend.RESERVED_WORDS:
|
|
||||||
word = word.replace(word, word.lower())
|
|
||||||
|
|
||||||
for char in self.backend.RESERVED_CHARACTERS:
|
|
||||||
if char in word:
|
|
||||||
word = "'%s'" % word
|
|
||||||
break
|
|
||||||
|
|
||||||
cleaned_words.append(word)
|
|
||||||
|
|
||||||
return ' '.join(cleaned_words)
|
|
||||||
|
|
||||||
def build_query_fragment(self, field, filter_type, value):
|
|
||||||
return value.query_string
|
|
||||||
|
|
||||||
def get_count(self):
|
|
||||||
results = self.get_results()
|
|
||||||
return len(results) if results else 0
|
|
||||||
|
|
||||||
def get_spelling_suggestion(self, preferred_query=None):
|
|
||||||
return self._spelling_suggestion
|
|
||||||
|
|
||||||
def build_params(self, spelling_query=None):
|
|
||||||
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchModelSearchForm(ModelSearchForm):
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
# 是否建议搜索
|
|
||||||
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
|
||||||
sqs = super().search()
|
|
||||||
return sqs
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchEngine(BaseEngine):
|
|
||||||
backend = ElasticSearchBackend
|
|
||||||
query = ElasticSearchQuery
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.syndication.views import Feed
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.feedgenerator import Rss201rev2Feed
|
|
||||||
|
|
||||||
from blog.models import Article
|
|
||||||
from djangoblog.utils import CommonMarkdown
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoBlogFeed(Feed):
|
|
||||||
feed_type = Rss201rev2Feed
|
|
||||||
|
|
||||||
description = '大巧无工,重剑无锋.'
|
|
||||||
title = "且听风吟 大巧无工,重剑无锋. "
|
|
||||||
link = "/feed/"
|
|
||||||
|
|
||||||
def author_name(self):
|
|
||||||
return get_user_model().objects.first().nickname
|
|
||||||
|
|
||||||
def author_link(self):
|
|
||||||
return get_user_model().objects.first().get_absolute_url()
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
|
|
||||||
|
|
||||||
def item_title(self, item):
|
|
||||||
return item.title
|
|
||||||
|
|
||||||
def item_description(self, item):
|
|
||||||
return CommonMarkdown.get_markdown(item.body)
|
|
||||||
|
|
||||||
def feed_copyright(self):
|
|
||||||
now = timezone.now()
|
|
||||||
return "Copyright© {year} 且听风吟".format(year=now.year)
|
|
||||||
|
|
||||||
def item_link(self, item):
|
|
||||||
return item.get_absolute_url()
|
|
||||||
|
|
||||||
def item_guid(self, item):
|
|
||||||
return
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from django.contrib.admin.models import DELETION
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.urls import reverse, NoReverseMatch
|
|
||||||
from django.utils.encoding import force_str
|
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = [
|
|
||||||
'content_type'
|
|
||||||
]
|
|
||||||
|
|
||||||
search_fields = [
|
|
||||||
'object_repr',
|
|
||||||
'change_message'
|
|
||||||
]
|
|
||||||
|
|
||||||
list_display_links = [
|
|
||||||
'action_time',
|
|
||||||
'get_change_message',
|
|
||||||
]
|
|
||||||
list_display = [
|
|
||||||
'action_time',
|
|
||||||
'user_link',
|
|
||||||
'content_type',
|
|
||||||
'object_link',
|
|
||||||
'get_change_message',
|
|
||||||
]
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
|
||||||
return (
|
|
||||||
request.user.is_superuser or
|
|
||||||
request.user.has_perm('admin.change_logentry')
|
|
||||||
) and request.method != 'POST'
|
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def object_link(self, obj):
|
|
||||||
object_link = escape(obj.object_repr)
|
|
||||||
content_type = obj.content_type
|
|
||||||
|
|
||||||
if obj.action_flag != DELETION and content_type is not None:
|
|
||||||
# try returning an actual link instead of object repr string
|
|
||||||
try:
|
|
||||||
url = reverse(
|
|
||||||
'admin:{}_{}_change'.format(content_type.app_label,
|
|
||||||
content_type.model),
|
|
||||||
args=[obj.object_id]
|
|
||||||
)
|
|
||||||
object_link = '<a href="{}">{}</a>'.format(url, object_link)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
return mark_safe(object_link)
|
|
||||||
|
|
||||||
object_link.admin_order_field = 'object_repr'
|
|
||||||
object_link.short_description = _('object')
|
|
||||||
|
|
||||||
def user_link(self, obj):
|
|
||||||
content_type = ContentType.objects.get_for_model(type(obj.user))
|
|
||||||
user_link = escape(force_str(obj.user))
|
|
||||||
try:
|
|
||||||
# try returning an actual link instead of object repr string
|
|
||||||
url = reverse(
|
|
||||||
'admin:{}_{}_change'.format(content_type.app_label,
|
|
||||||
content_type.model),
|
|
||||||
args=[obj.user.pk]
|
|
||||||
)
|
|
||||||
user_link = '<a href="{}">{}</a>'.format(url, user_link)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
return mark_safe(user_link)
|
|
||||||
|
|
||||||
user_link.admin_order_field = 'user'
|
|
||||||
user_link.short_description = _('user')
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
queryset = super(LogEntryAdmin, self).get_queryset(request)
|
|
||||||
return queryset.prefetch_related('content_type')
|
|
||||||
|
|
||||||
def get_actions(self, request):
|
|
||||||
actions = super(LogEntryAdmin, self).get_actions(request)
|
|
||||||
if 'delete_selected' in actions:
|
|
||||||
del actions['delete_selected']
|
|
||||||
return actions
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.template import TemplateDoesNotExist
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePlugin:
|
|
||||||
# 插件元数据
|
|
||||||
PLUGIN_NAME = None
|
|
||||||
PLUGIN_DESCRIPTION = None
|
|
||||||
PLUGIN_VERSION = None
|
|
||||||
PLUGIN_AUTHOR = None
|
|
||||||
|
|
||||||
# 插件配置
|
|
||||||
SUPPORTED_POSITIONS = [] # 支持的显示位置
|
|
||||||
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
|
|
||||||
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
|
||||||
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
|
||||||
|
|
||||||
# 设置插件路径
|
|
||||||
self.plugin_dir = self._get_plugin_directory()
|
|
||||||
self.plugin_slug = self._get_plugin_slug()
|
|
||||||
|
|
||||||
self.init_plugin()
|
|
||||||
self.register_hooks()
|
|
||||||
|
|
||||||
def _get_plugin_directory(self):
|
|
||||||
"""获取插件目录路径"""
|
|
||||||
import inspect
|
|
||||||
plugin_file = inspect.getfile(self.__class__)
|
|
||||||
return Path(plugin_file).parent
|
|
||||||
|
|
||||||
def _get_plugin_slug(self):
|
|
||||||
"""获取插件标识符(目录名)"""
|
|
||||||
return self.plugin_dir.name
|
|
||||||
|
|
||||||
def init_plugin(self):
|
|
||||||
"""
|
|
||||||
插件初始化逻辑
|
|
||||||
子类可以重写此方法来实现特定的初始化操作
|
|
||||||
"""
|
|
||||||
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
|
||||||
|
|
||||||
def register_hooks(self):
|
|
||||||
"""
|
|
||||||
注册插件钩子
|
|
||||||
子类可以重写此方法来注册特定的钩子
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# === 位置渲染系统 ===
|
|
||||||
def render_position_widget(self, position, context, **kwargs):
|
|
||||||
"""
|
|
||||||
根据位置渲染插件组件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position: 位置标识
|
|
||||||
context: 模板上下文
|
|
||||||
**kwargs: 额外参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: {'html': 'HTML内容', 'priority': 优先级} 或 None
|
|
||||||
"""
|
|
||||||
if position not in self.SUPPORTED_POSITIONS:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查条件显示
|
|
||||||
if not self.should_display(position, context, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 调用具体的位置渲染方法
|
|
||||||
method_name = f'render_{position}_widget'
|
|
||||||
if hasattr(self, method_name):
|
|
||||||
html = getattr(self, method_name)(context, **kwargs)
|
|
||||||
if html:
|
|
||||||
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
|
|
||||||
return {
|
|
||||||
'html': html,
|
|
||||||
'priority': priority,
|
|
||||||
'plugin_name': self.PLUGIN_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def should_display(self, position, context, **kwargs):
|
|
||||||
"""
|
|
||||||
判断插件是否应该在指定位置显示
|
|
||||||
子类可重写此方法实现条件显示逻辑
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position: 位置标识
|
|
||||||
context: 模板上下文
|
|
||||||
**kwargs: 额外参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否显示
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
# === 各位置渲染方法 - 子类重写 ===
|
|
||||||
def render_sidebar_widget(self, context, **kwargs):
|
|
||||||
"""渲染侧边栏组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_article_bottom_widget(self, context, **kwargs):
|
|
||||||
"""渲染文章底部组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_article_top_widget(self, context, **kwargs):
|
|
||||||
"""渲染文章顶部组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_header_widget(self, context, **kwargs):
|
|
||||||
"""渲染页头组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_footer_widget(self, context, **kwargs):
|
|
||||||
"""渲染页脚组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_comment_before_widget(self, context, **kwargs):
|
|
||||||
"""渲染评论前组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_comment_after_widget(self, context, **kwargs):
|
|
||||||
"""渲染评论后组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
# === 模板系统 ===
|
|
||||||
def render_template(self, template_name, context=None):
|
|
||||||
"""
|
|
||||||
渲染插件模板
|
|
||||||
|
|
||||||
Args:
|
|
||||||
template_name: 模板文件名
|
|
||||||
context: 模板上下文
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTML字符串
|
|
||||||
"""
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
template_path = f"plugins/{self.plugin_slug}/{template_name}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
return render_to_string(template_path, context)
|
|
||||||
except TemplateDoesNotExist:
|
|
||||||
logger.warning(f"Plugin template not found: {template_path}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# === 静态资源系统 ===
|
|
||||||
def get_static_url(self, static_file):
|
|
||||||
"""获取插件静态文件URL"""
|
|
||||||
from django.templatetags.static import static
|
|
||||||
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
|
|
||||||
|
|
||||||
def get_css_files(self):
|
|
||||||
"""获取插件CSS文件列表"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_js_files(self):
|
|
||||||
"""获取插件JavaScript文件列表"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_head_html(self, context=None):
|
|
||||||
"""获取需要插入到<head>中的HTML内容"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_body_html(self, context=None):
|
|
||||||
"""获取需要插入到<body>底部的HTML内容"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_plugin_info(self):
|
|
||||||
"""
|
|
||||||
获取插件信息
|
|
||||||
:return: 包含插件元数据的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'name': self.PLUGIN_NAME,
|
|
||||||
'description': self.PLUGIN_DESCRIPTION,
|
|
||||||
'version': self.PLUGIN_VERSION,
|
|
||||||
'author': self.PLUGIN_AUTHOR,
|
|
||||||
'slug': self.plugin_slug,
|
|
||||||
'directory': str(self.plugin_dir),
|
|
||||||
'supported_positions': self.SUPPORTED_POSITIONS,
|
|
||||||
'priorities': self.POSITION_PRIORITIES
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
|
||||||
ARTICLE_CREATE = 'article_create'
|
|
||||||
ARTICLE_UPDATE = 'article_update'
|
|
||||||
ARTICLE_DELETE = 'article_delete'
|
|
||||||
|
|
||||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
|
||||||
|
|
||||||
# 位置钩子常量
|
|
||||||
POSITION_HOOKS = {
|
|
||||||
'article_top': 'article_top_widgets',
|
|
||||||
'article_bottom': 'article_bottom_widgets',
|
|
||||||
'sidebar': 'sidebar_widgets',
|
|
||||||
'header': 'header_widgets',
|
|
||||||
'footer': 'footer_widgets',
|
|
||||||
'comment_before': 'comment_before_widgets',
|
|
||||||
'comment_after': 'comment_after_widgets',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 资源注入钩子
|
|
||||||
HEAD_RESOURCES_HOOK = 'head_resources'
|
|
||||||
BODY_RESOURCES_HOOK = 'body_resources'
|
|
||||||
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_hooks = {}
|
|
||||||
|
|
||||||
|
|
||||||
def register(hook_name: str, callback: callable):
|
|
||||||
"""
|
|
||||||
注册一个钩子回调。
|
|
||||||
"""
|
|
||||||
if hook_name not in _hooks:
|
|
||||||
_hooks[hook_name] = []
|
|
||||||
_hooks[hook_name].append(callback)
|
|
||||||
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
|
|
||||||
|
|
||||||
|
|
||||||
def run_action(hook_name: str, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
执行一个 Action Hook。
|
|
||||||
它会按顺序执行所有注册到该钩子上的回调函数。
|
|
||||||
"""
|
|
||||||
if hook_name in _hooks:
|
|
||||||
logger.debug(f"Running action hook '{hook_name}'")
|
|
||||||
for callback in _hooks[hook_name]:
|
|
||||||
try:
|
|
||||||
callback(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_filters(hook_name: str, value, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
执行一个 Filter Hook。
|
|
||||||
它会把 value 依次传递给所有注册的回调函数进行处理。
|
|
||||||
"""
|
|
||||||
if hook_name in _hooks:
|
|
||||||
logger.debug(f"Applying filter hook '{hook_name}'")
|
|
||||||
for callback in _hooks[hook_name]:
|
|
||||||
try:
|
|
||||||
value = callback(value, *args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
|
||||||
return value
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 全局插件注册表
|
|
||||||
_loaded_plugins = []
|
|
||||||
|
|
||||||
def load_plugins():
|
|
||||||
"""
|
|
||||||
Dynamically loads and initializes plugins from the 'plugins' directory.
|
|
||||||
This function is intended to be called when the Django app registry is ready.
|
|
||||||
"""
|
|
||||||
global _loaded_plugins
|
|
||||||
_loaded_plugins = []
|
|
||||||
|
|
||||||
for plugin_name in settings.ACTIVE_PLUGINS:
|
|
||||||
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
|
||||||
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
|
||||||
try:
|
|
||||||
# 导入插件模块
|
|
||||||
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
|
|
||||||
|
|
||||||
# 获取插件实例
|
|
||||||
if hasattr(plugin_module, 'plugin'):
|
|
||||||
plugin_instance = plugin_module.plugin
|
|
||||||
_loaded_plugins.append(plugin_instance)
|
|
||||||
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
|
|
||||||
|
|
||||||
except ImportError as e:
|
|
||||||
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
|
||||||
except AttributeError as e:
|
|
||||||
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
|
|
||||||
|
|
||||||
def get_loaded_plugins():
|
|
||||||
"""获取所有已加载的插件"""
|
|
||||||
return _loaded_plugins
|
|
||||||
|
|
||||||
def get_plugin_by_name(plugin_name):
|
|
||||||
"""根据名称获取插件"""
|
|
||||||
for plugin in _loaded_plugins:
|
|
||||||
if plugin.plugin_slug == plugin_name:
|
|
||||||
return plugin
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_plugin_by_slug(plugin_slug):
|
|
||||||
"""根据slug获取插件"""
|
|
||||||
for plugin in _loaded_plugins:
|
|
||||||
if plugin.plugin_slug == plugin_slug:
|
|
||||||
return plugin
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_plugins_info():
|
|
||||||
"""获取所有插件的信息"""
|
|
||||||
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
|
|
||||||
|
|
||||||
def get_plugins_by_position(position):
|
|
||||||
"""获取支持指定位置的插件"""
|
|
||||||
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
from django.contrib.sitemaps import Sitemap
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from blog.models import Article, Category, Tag
|
|
||||||
|
|
||||||
|
|
||||||
class StaticViewSitemap(Sitemap):
|
|
||||||
priority = 0.5
|
|
||||||
changefreq = 'daily'
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return ['blog:index', ]
|
|
||||||
|
|
||||||
def location(self, item):
|
|
||||||
return reverse(item)
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleSiteMap(Sitemap):
|
|
||||||
changefreq = "monthly"
|
|
||||||
priority = "0.6"
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Article.objects.filter(status='p')
|
|
||||||
|
|
||||||
def lastmod(self, obj):
|
|
||||||
return obj.last_modify_time
|
|
||||||
|
|
||||||
|
|
||||||
class CategorySiteMap(Sitemap):
|
|
||||||
changefreq = "Weekly"
|
|
||||||
priority = "0.6"
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Category.objects.all()
|
|
||||||
|
|
||||||
def lastmod(self, obj):
|
|
||||||
return obj.last_modify_time
|
|
||||||
|
|
||||||
|
|
||||||
class TagSiteMap(Sitemap):
|
|
||||||
changefreq = "Weekly"
|
|
||||||
priority = "0.3"
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Tag.objects.all()
|
|
||||||
|
|
||||||
def lastmod(self, obj):
|
|
||||||
return obj.last_modify_time
|
|
||||||
|
|
||||||
|
|
||||||
class UserSiteMap(Sitemap):
|
|
||||||
changefreq = "Weekly"
|
|
||||||
priority = "0.3"
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return list(set(map(lambda x: x.author, Article.objects.all())))
|
|
||||||
|
|
||||||
def lastmod(self, obj):
|
|
||||||
return obj.date_joined
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SpiderNotify():
|
|
||||||
@staticmethod
|
|
||||||
def baidu_notify(urls):
|
|
||||||
try:
|
|
||||||
data = '\n'.join(urls)
|
|
||||||
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
|
|
||||||
logger.info(result.text)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def notify(url):
|
|
||||||
SpiderNotify.baidu_notify(url)
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from djangoblog.utils import *
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoBlogTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_utils(self):
|
|
||||||
md5 = get_sha256('test')
|
|
||||||
self.assertIsNotNone(md5)
|
|
||||||
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)
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
"""djangoblog URL Configuration
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.conf.urls import url, include
|
|
||||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.i18n import i18n_patterns
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
from django.contrib.sitemaps.views import sitemap
|
|
||||||
from django.urls import path, include
|
|
||||||
from django.urls import re_path
|
|
||||||
from haystack.views import search_view_factory
|
|
||||||
from django.http import JsonResponse
|
|
||||||
import time
|
|
||||||
|
|
||||||
from blog.views import EsSearchView
|
|
||||||
from djangoblog.admin_site import admin_site
|
|
||||||
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
|
|
||||||
from djangoblog.feeds import DjangoBlogFeed
|
|
||||||
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
|
|
||||||
|
|
||||||
sitemaps = {
|
|
||||||
|
|
||||||
'blog': ArticleSiteMap,
|
|
||||||
'Category': CategorySiteMap,
|
|
||||||
'Tag': TagSiteMap,
|
|
||||||
'User': UserSiteMap,
|
|
||||||
'static': StaticViewSitemap
|
|
||||||
}
|
|
||||||
|
|
||||||
handler404 = 'blog.views.page_not_found_view'
|
|
||||||
handler500 = 'blog.views.server_error_view'
|
|
||||||
handle403 = 'blog.views.permission_denied_view'
|
|
||||||
|
|
||||||
|
|
||||||
def health_check(request):
|
|
||||||
"""
|
|
||||||
健康检查接口
|
|
||||||
简单返回服务健康状态
|
|
||||||
"""
|
|
||||||
return JsonResponse({
|
|
||||||
'status': 'healthy',
|
|
||||||
'timestamp': time.time()
|
|
||||||
})
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('i18n/', include('django.conf.urls.i18n')),
|
|
||||||
path('health/', health_check, name='health_check'),
|
|
||||||
]
|
|
||||||
urlpatterns += i18n_patterns(
|
|
||||||
re_path(r'^admin/', admin_site.urls),
|
|
||||||
re_path(r'', include('src.blog.urls', namespace='blog')),
|
|
||||||
re_path(r'mdeditor/', include('mdeditor.urls')),
|
|
||||||
re_path(r'', include('src.comments.urls', namespace='comment')),
|
|
||||||
re_path(r'', include('src.accounts.urls', namespace='account')),
|
|
||||||
re_path(r'', include('src.oauth.urls', namespace='oauth')),
|
|
||||||
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
|
|
||||||
name='django.contrib.sitemaps.views.sitemap'),
|
|
||||||
re_path(r'^feed/$', DjangoBlogFeed()),
|
|
||||||
re_path(r'^rss/$', DjangoBlogFeed()),
|
|
||||||
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
|
|
||||||
name='search'),
|
|
||||||
re_path(r'', include('src.servermanager.urls', namespace='servermanager')),
|
|
||||||
re_path(r'', include('src.owntracks.urls', namespace='owntracks'))
|
|
||||||
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
|
||||||
if settings.DEBUG:
|
|
||||||
urlpatterns += static(settings.MEDIA_URL,
|
|
||||||
document_root=settings.MEDIA_ROOT)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
WSGI config for djangoblog project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"# 文档目录"
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
bin/data/
|
|
||||||
# virtualenv
|
|
||||||
venv/
|
|
||||||
collectedstatic/
|
|
||||||
djangoblog/whoosh_index/
|
|
||||||
uploads/
|
|
||||||
settings_production.py
|
|
||||||
*.md
|
|
||||||
docs/
|
|
||||||
logs/
|
|
||||||
static/
|
|
||||||
.github/
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
blog/static/* linguist-vendored
|
|
||||||
*.js linguist-vendored
|
|
||||||
*.css linguist-vendored
|
|
||||||
* text=auto
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.conf text eol=lf
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
# 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
|
|
||||||
src/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/
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit b32ef4b40303055f250c66ef81e980e7860b31f5
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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"]
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.contrib.auth.forms import UserChangeForm
|
|
||||||
from django.contrib.auth.forms import UsernameField
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
from .models import BlogUser
|
|
||||||
|
|
||||||
|
|
||||||
class BlogUserCreationForm(forms.ModelForm):
|
|
||||||
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
|
||||||
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = BlogUser
|
|
||||||
fields = ('email',)
|
|
||||||
|
|
||||||
def clean_password2(self):
|
|
||||||
# Check that the two password entries match
|
|
||||||
password1 = self.cleaned_data.get("password1")
|
|
||||||
password2 = self.cleaned_data.get("password2")
|
|
||||||
if password1 and password2 and password1 != password2:
|
|
||||||
raise forms.ValidationError(_("passwords do not match"))
|
|
||||||
return password2
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
# Save the provided password in hashed format
|
|
||||||
user = super().save(commit=False)
|
|
||||||
user.set_password(self.cleaned_data["password1"])
|
|
||||||
if commit:
|
|
||||||
user.source = 'adminsite'
|
|
||||||
user.save()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
class BlogUserChangeForm(UserChangeForm):
|
|
||||||
class Meta:
|
|
||||||
model = BlogUser
|
|
||||||
fields = '__all__'
|
|
||||||
field_classes = {'username': UsernameField}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BlogUserAdmin(UserAdmin):
|
|
||||||
form = BlogUserChangeForm
|
|
||||||
add_form = BlogUserCreationForm
|
|
||||||
list_display = (
|
|
||||||
'id',
|
|
||||||
'nickname',
|
|
||||||
'username',
|
|
||||||
'email',
|
|
||||||
'last_login',
|
|
||||||
'date_joined',
|
|
||||||
'source')
|
|
||||||
list_display_links = ('id', 'username')
|
|
||||||
ordering = ('-id',)
|
|
||||||
search_fields = ('username', 'nickname', 'email')
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AccountsConfig(AppConfig):
|
|
||||||
name = 'accounts'
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib.auth import get_user_model, password_validation
|
|
||||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.forms import widgets
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from . import utils
|
|
||||||
from .models import BlogUser
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(AuthenticationForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(LoginForm, self).__init__(*args, **kwargs)
|
|
||||||
self.fields['username'].widget = widgets.TextInput(
|
|
||||||
attrs={'placeholder': "username", "class": "form-control"})
|
|
||||||
self.fields['password'].widget = widgets.PasswordInput(
|
|
||||||
attrs={'placeholder': "password", "class": "form-control"})
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterForm(UserCreationForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.fields['username'].widget = widgets.TextInput(
|
|
||||||
attrs={'placeholder': "username", "class": "form-control"})
|
|
||||||
self.fields['email'].widget = widgets.EmailInput(
|
|
||||||
attrs={'placeholder': "email", "class": "form-control"})
|
|
||||||
self.fields['password1'].widget = widgets.PasswordInput(
|
|
||||||
attrs={'placeholder': "password", "class": "form-control"})
|
|
||||||
self.fields['password2'].widget = widgets.PasswordInput(
|
|
||||||
attrs={'placeholder': "repeat password", "class": "form-control"})
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
email = self.cleaned_data['email']
|
|
||||||
if get_user_model().objects.filter(email=email).exists():
|
|
||||||
raise ValidationError(_("email already exists"))
|
|
||||||
return email
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = get_user_model()
|
|
||||||
fields = ("username", "email")
|
|
||||||
|
|
||||||
|
|
||||||
class ForgetPasswordForm(forms.Form):
|
|
||||||
new_password1 = forms.CharField(
|
|
||||||
label=_("New password"),
|
|
||||||
widget=forms.PasswordInput(
|
|
||||||
attrs={
|
|
||||||
"class": "form-control",
|
|
||||||
'placeholder': _("New password")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
new_password2 = forms.CharField(
|
|
||||||
label="确认密码",
|
|
||||||
widget=forms.PasswordInput(
|
|
||||||
attrs={
|
|
||||||
"class": "form-control",
|
|
||||||
'placeholder': _("Confirm password")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
email = forms.EmailField(
|
|
||||||
label='邮箱',
|
|
||||||
widget=forms.TextInput(
|
|
||||||
attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'placeholder': _("Email")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
code = forms.CharField(
|
|
||||||
label=_('Code'),
|
|
||||||
widget=forms.TextInput(
|
|
||||||
attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'placeholder': _("Code")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_new_password2(self):
|
|
||||||
password1 = self.data.get("new_password1")
|
|
||||||
password2 = self.data.get("new_password2")
|
|
||||||
if password1 and password2 and password1 != password2:
|
|
||||||
raise ValidationError(_("passwords do not match"))
|
|
||||||
password_validation.validate_password(password2)
|
|
||||||
|
|
||||||
return password2
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
user_email = self.cleaned_data.get("email")
|
|
||||||
if not BlogUser.objects.filter(
|
|
||||||
email=user_email
|
|
||||||
).exists():
|
|
||||||
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
|
|
||||||
raise ValidationError(_("email does not exist"))
|
|
||||||
return user_email
|
|
||||||
|
|
||||||
def clean_code(self):
|
|
||||||
code = self.cleaned_data.get("code")
|
|
||||||
error = utils.verify(
|
|
||||||
email=self.cleaned_data.get("email"),
|
|
||||||
code=code,
|
|
||||||
)
|
|
||||||
if error:
|
|
||||||
raise ValidationError(error)
|
|
||||||
return code
|
|
||||||
|
|
||||||
|
|
||||||
class ForgetPasswordCodeForm(forms.Form):
|
|
||||||
email = forms.EmailField(
|
|
||||||
label=_('Email'),
|
|
||||||
)
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
# 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()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
|
||||||
class BlogUser(AbstractUser):
|
|
||||||
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
|
||||||
source = models.CharField(_('create source'), max_length=100, blank=True)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:author_detail', kwargs={
|
|
||||||
'author_name': self.username})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.email
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-id']
|
|
||||||
verbose_name = _('user')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
from django.test import Client, RequestFactory, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from accounts.models import BlogUser
|
|
||||||
from blog.models import Article, Category
|
|
||||||
from djangoblog.utils import *
|
|
||||||
from . import utils
|
|
||||||
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
||||||
class AccountTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.blog_user = BlogUser.objects.create_user(
|
|
||||||
username="test",
|
|
||||||
email="admin@admin.com",
|
|
||||||
password="12345678"
|
|
||||||
)
|
|
||||||
self.new_test = "xxx123--="
|
|
||||||
|
|
||||||
def test_validate_account(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
user = BlogUser.objects.create_superuser(
|
|
||||||
email="liangliangyy1@gmail.com",
|
|
||||||
username="liangliangyy1",
|
|
||||||
password="qwer!@#$ggg")
|
|
||||||
testuser = BlogUser.objects.get(username='liangliangyy1')
|
|
||||||
|
|
||||||
loginresult = self.client.login(
|
|
||||||
username='liangliangyy1',
|
|
||||||
password='qwer!@#$ggg')
|
|
||||||
self.assertEqual(loginresult, True)
|
|
||||||
response = self.client.get('/admin/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
category = Category()
|
|
||||||
category.name = "categoryaaa"
|
|
||||||
category.creation_time = timezone.now()
|
|
||||||
category.last_modify_time = timezone.now()
|
|
||||||
category.save()
|
|
||||||
|
|
||||||
article = Article()
|
|
||||||
article.title = "nicetitleaaa"
|
|
||||||
article.body = "nicecontentaaa"
|
|
||||||
article.author = user
|
|
||||||
article.category = category
|
|
||||||
article.type = 'a'
|
|
||||||
article.status = 'p'
|
|
||||||
article.save()
|
|
||||||
|
|
||||||
response = self.client.get(article.get_admin_url())
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_validate_register(self):
|
|
||||||
self.assertEquals(
|
|
||||||
0, len(
|
|
||||||
BlogUser.objects.filter(
|
|
||||||
email='user123@user.com')))
|
|
||||||
response = self.client.post(reverse('account:register'), {
|
|
||||||
'username': 'user1233',
|
|
||||||
'email': 'user123@user.com',
|
|
||||||
'password1': 'password123!q@wE#R$T',
|
|
||||||
'password2': 'password123!q@wE#R$T',
|
|
||||||
})
|
|
||||||
self.assertEquals(
|
|
||||||
1, len(
|
|
||||||
BlogUser.objects.filter(
|
|
||||||
email='user123@user.com')))
|
|
||||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
|
||||||
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
|
||||||
path = reverse('accounts:result')
|
|
||||||
url = '{path}?type=validation&id={id}&sign={sign}'.format(
|
|
||||||
path=path, id=user.id, sign=sign)
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
self.client.login(username='user1233', password='password123!q@wE#R$T')
|
|
||||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
|
||||||
user.is_superuser = True
|
|
||||||
user.is_staff = True
|
|
||||||
user.save()
|
|
||||||
delete_sidebar_cache()
|
|
||||||
category = Category()
|
|
||||||
category.name = "categoryaaa"
|
|
||||||
category.creation_time = timezone.now()
|
|
||||||
category.last_modify_time = timezone.now()
|
|
||||||
category.save()
|
|
||||||
|
|
||||||
article = Article()
|
|
||||||
article.category = category
|
|
||||||
article.title = "nicetitle333"
|
|
||||||
article.body = "nicecontentttt"
|
|
||||||
article.author = user
|
|
||||||
|
|
||||||
article.type = 'a'
|
|
||||||
article.status = 'p'
|
|
||||||
article.save()
|
|
||||||
|
|
||||||
response = self.client.get(article.get_admin_url())
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse('account:logout'))
|
|
||||||
self.assertIn(response.status_code, [301, 302, 200])
|
|
||||||
|
|
||||||
response = self.client.get(article.get_admin_url())
|
|
||||||
self.assertIn(response.status_code, [301, 302, 200])
|
|
||||||
|
|
||||||
response = self.client.post(reverse('account:login'), {
|
|
||||||
'username': 'user1233',
|
|
||||||
'password': 'password123'
|
|
||||||
})
|
|
||||||
self.assertIn(response.status_code, [301, 302, 200])
|
|
||||||
|
|
||||||
response = self.client.get(article.get_admin_url())
|
|
||||||
self.assertIn(response.status_code, [301, 302, 200])
|
|
||||||
|
|
||||||
def test_verify_email_code(self):
|
|
||||||
to_email = "admin@admin.com"
|
|
||||||
code = generate_code()
|
|
||||||
utils.set_code(to_email, code)
|
|
||||||
utils.send_verify_email(to_email, code)
|
|
||||||
|
|
||||||
err = utils.verify("admin@admin.com", code)
|
|
||||||
self.assertEqual(err, None)
|
|
||||||
|
|
||||||
err = utils.verify("admin@123.com", code)
|
|
||||||
self.assertEqual(type(err), str)
|
|
||||||
|
|
||||||
def test_forget_password_email_code_success(self):
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password_code"),
|
|
||||||
data=dict(email="admin@admin.com")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertEqual(resp.content.decode("utf-8"), "ok")
|
|
||||||
|
|
||||||
def test_forget_password_email_code_fail(self):
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password_code"),
|
|
||||||
data=dict()
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
|
||||||
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password_code"),
|
|
||||||
data=dict(email="admin@com")
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
|
||||||
|
|
||||||
def test_forget_password_email_success(self):
|
|
||||||
code = generate_code()
|
|
||||||
utils.set_code(self.blog_user.email, code)
|
|
||||||
data = dict(
|
|
||||||
new_password1=self.new_test,
|
|
||||||
new_password2=self.new_test,
|
|
||||||
email=self.blog_user.email,
|
|
||||||
code=code,
|
|
||||||
)
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password"),
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 302)
|
|
||||||
|
|
||||||
# 验证用户密码是否修改成功
|
|
||||||
blog_user = BlogUser.objects.filter(
|
|
||||||
email=self.blog_user.email,
|
|
||||||
).first() # type: BlogUser
|
|
||||||
self.assertNotEqual(blog_user, None)
|
|
||||||
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
|
|
||||||
|
|
||||||
def test_forget_password_email_not_user(self):
|
|
||||||
data = dict(
|
|
||||||
new_password1=self.new_test,
|
|
||||||
new_password2=self.new_test,
|
|
||||||
email="123@123.com",
|
|
||||||
code="123456",
|
|
||||||
)
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password"),
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_forget_password_email_code_error(self):
|
|
||||||
code = generate_code()
|
|
||||||
utils.set_code(self.blog_user.email, code)
|
|
||||||
data = dict(
|
|
||||||
new_password1=self.new_test,
|
|
||||||
new_password2=self.new_test,
|
|
||||||
email=self.blog_user.email,
|
|
||||||
code="111111",
|
|
||||||
)
|
|
||||||
resp = self.client.post(
|
|
||||||
path=reverse("account:forget_password"),
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.urls import re_path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
from .forms import LoginForm
|
|
||||||
|
|
||||||
app_name = "accounts"
|
|
||||||
|
|
||||||
urlpatterns = [re_path(r'^login/$',
|
|
||||||
views.LoginView.as_view(success_url='/'),
|
|
||||||
name='login',
|
|
||||||
kwargs={'authentication_form': LoginForm}),
|
|
||||||
re_path(r'^register/$',
|
|
||||||
views.RegisterView.as_view(success_url="/"),
|
|
||||||
name='register'),
|
|
||||||
re_path(r'^logout/$',
|
|
||||||
views.LogoutView.as_view(),
|
|
||||||
name='logout'),
|
|
||||||
path(r'account/result.html',
|
|
||||||
views.account_result,
|
|
||||||
name='result'),
|
|
||||||
re_path(r'^forget_password/$',
|
|
||||||
views.ForgetPasswordView.as_view(),
|
|
||||||
name='forget_password'),
|
|
||||||
re_path(r'^forget_password_code/$',
|
|
||||||
views.ForgetPasswordEmailCode.as_view(),
|
|
||||||
name='forget_password_code'),
|
|
||||||
]
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
|
||||||
|
|
||||||
|
|
||||||
class EmailOrUsernameModelBackend(ModelBackend):
|
|
||||||
"""
|
|
||||||
允许使用用户名或邮箱登录
|
|
||||||
"""
|
|
||||||
|
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
|
||||||
if '@' in username:
|
|
||||||
kwargs = {'email': username}
|
|
||||||
else:
|
|
||||||
kwargs = {'username': username}
|
|
||||||
try:
|
|
||||||
user = get_user_model().objects.get(**kwargs)
|
|
||||||
if user.check_password(password):
|
|
||||||
return user
|
|
||||||
except get_user_model().DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_user(self, username):
|
|
||||||
try:
|
|
||||||
return get_user_model().objects.get(pk=username)
|
|
||||||
except get_user_model().DoesNotExist:
|
|
||||||
return None
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleForm(forms.ModelForm):
|
|
||||||
# body = forms.CharField(widget=AdminPagedownWidget())
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Article
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
def makr_article_publish(modeladmin, request, queryset):
|
|
||||||
queryset.update(status='p')
|
|
||||||
|
|
||||||
|
|
||||||
def draft_article(modeladmin, request, queryset):
|
|
||||||
queryset.update(status='d')
|
|
||||||
|
|
||||||
|
|
||||||
def close_article_commentstatus(modeladmin, request, queryset):
|
|
||||||
queryset.update(comment_status='c')
|
|
||||||
|
|
||||||
|
|
||||||
def open_article_commentstatus(modeladmin, request, queryset):
|
|
||||||
queryset.update(comment_status='o')
|
|
||||||
|
|
||||||
|
|
||||||
makr_article_publish.short_description = _('Publish selected articles')
|
|
||||||
draft_article.short_description = _('Draft selected articles')
|
|
||||||
close_article_commentstatus.short_description = _('Close article comments')
|
|
||||||
open_article_commentstatus.short_description = _('Open article comments')
|
|
||||||
|
|
||||||
|
|
||||||
class ArticlelAdmin(admin.ModelAdmin):
|
|
||||||
list_per_page = 20
|
|
||||||
search_fields = ('body', 'title')
|
|
||||||
form = ArticleForm
|
|
||||||
list_display = (
|
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'author',
|
|
||||||
'link_to_category',
|
|
||||||
'creation_time',
|
|
||||||
'views',
|
|
||||||
'status',
|
|
||||||
'type',
|
|
||||||
'article_order')
|
|
||||||
list_display_links = ('id', 'title')
|
|
||||||
list_filter = ('status', 'type', 'category')
|
|
||||||
date_hierarchy = 'creation_time'
|
|
||||||
filter_horizontal = ('tags',)
|
|
||||||
exclude = ('creation_time', 'last_modify_time')
|
|
||||||
view_on_site = True
|
|
||||||
actions = [
|
|
||||||
makr_article_publish,
|
|
||||||
draft_article,
|
|
||||||
close_article_commentstatus,
|
|
||||||
open_article_commentstatus]
|
|
||||||
raw_id_fields = ('author', 'category',)
|
|
||||||
|
|
||||||
def link_to_category(self, obj):
|
|
||||||
info = (obj.category._meta.app_label, obj.category._meta.model_name)
|
|
||||||
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
|
|
||||||
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
|
|
||||||
|
|
||||||
link_to_category.short_description = _('category')
|
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
|
||||||
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
|
|
||||||
form.base_fields['author'].queryset = get_user_model(
|
|
||||||
).objects.filter(is_superuser=True)
|
|
||||||
return form
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
|
||||||
super(ArticlelAdmin, self).save_model(request, obj, form, change)
|
|
||||||
|
|
||||||
def get_view_on_site_url(self, obj=None):
|
|
||||||
if obj:
|
|
||||||
url = obj.get_full_url()
|
|
||||||
return url
|
|
||||||
else:
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
site = get_current_site().domain
|
|
||||||
return site
|
|
||||||
|
|
||||||
|
|
||||||
class TagAdmin(admin.ModelAdmin):
|
|
||||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'parent_category', 'index')
|
|
||||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
|
||||||
|
|
||||||
|
|
||||||
class LinksAdmin(admin.ModelAdmin):
|
|
||||||
exclude = ('last_mod_time', 'creation_time')
|
|
||||||
|
|
||||||
|
|
||||||
class SideBarAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'content', 'is_enable', 'sequence')
|
|
||||||
exclude = ('last_mod_time', 'creation_time')
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettingsAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BlogConfig(AppConfig):
|
|
||||||
name = 'blog'
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from djangoblog.utils import cache, get_blog_setting
|
|
||||||
from .models import Category, Article
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def seo_processor(requests):
|
|
||||||
key = 'seo_processor'
|
|
||||||
value = cache.get(key)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
logger.info('set processor cache.')
|
|
||||||
setting = get_blog_setting()
|
|
||||||
value = {
|
|
||||||
'SITE_NAME': setting.site_name,
|
|
||||||
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
|
|
||||||
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
|
|
||||||
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
|
|
||||||
'SITE_DESCRIPTION': setting.site_description,
|
|
||||||
'SITE_KEYWORDS': setting.site_keywords,
|
|
||||||
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
|
|
||||||
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
|
|
||||||
'nav_category_list': Category.objects.all(),
|
|
||||||
'nav_pages': Article.objects.filter(
|
|
||||||
type='p',
|
|
||||||
status='p'),
|
|
||||||
'OPEN_SITE_COMMENT': setting.open_site_comment,
|
|
||||||
'BEIAN_CODE': setting.beian_code,
|
|
||||||
'ANALYTICS_CODE': setting.analytics_code,
|
|
||||||
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
|
|
||||||
"SHOW_GONGAN_CODE": setting.show_gongan_code,
|
|
||||||
"CURRENT_YEAR": timezone.now().year,
|
|
||||||
"GLOBAL_HEADER": setting.global_header,
|
|
||||||
"GLOBAL_FOOTER": setting.global_footer,
|
|
||||||
"COMMENT_NEED_REVIEW": setting.comment_need_review,
|
|
||||||
}
|
|
||||||
cache.set(key, value, 60 * 60 * 10)
|
|
||||||
return value
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
import elasticsearch.client
|
|
||||||
from django.conf import settings
|
|
||||||
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
|
|
||||||
from elasticsearch_dsl.connections import connections
|
|
||||||
|
|
||||||
from blog.models import Article
|
|
||||||
|
|
||||||
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
|
|
||||||
|
|
||||||
if ELASTICSEARCH_ENABLED:
|
|
||||||
connections.create_connection(
|
|
||||||
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
|
|
||||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
|
||||||
from elasticsearch.client import IngestClient
|
|
||||||
|
|
||||||
c = IngestClient(es)
|
|
||||||
try:
|
|
||||||
c.get_pipeline('geoip')
|
|
||||||
except elasticsearch.exceptions.NotFoundError:
|
|
||||||
c.put_pipeline('geoip', body='''{
|
|
||||||
"description" : "Add geoip info",
|
|
||||||
"processors" : [
|
|
||||||
{
|
|
||||||
"geoip" : {
|
|
||||||
"field" : "ip"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}''')
|
|
||||||
|
|
||||||
|
|
||||||
class GeoIp(InnerDoc):
|
|
||||||
continent_name = Keyword()
|
|
||||||
country_iso_code = Keyword()
|
|
||||||
country_name = Keyword()
|
|
||||||
location = GeoPoint()
|
|
||||||
|
|
||||||
|
|
||||||
class UserAgentBrowser(InnerDoc):
|
|
||||||
Family = Keyword()
|
|
||||||
Version = Keyword()
|
|
||||||
|
|
||||||
|
|
||||||
class UserAgentOS(UserAgentBrowser):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserAgentDevice(InnerDoc):
|
|
||||||
Family = Keyword()
|
|
||||||
Brand = Keyword()
|
|
||||||
Model = Keyword()
|
|
||||||
|
|
||||||
|
|
||||||
class UserAgent(InnerDoc):
|
|
||||||
browser = Object(UserAgentBrowser, required=False)
|
|
||||||
os = Object(UserAgentOS, required=False)
|
|
||||||
device = Object(UserAgentDevice, required=False)
|
|
||||||
string = Text()
|
|
||||||
is_bot = Boolean()
|
|
||||||
|
|
||||||
|
|
||||||
class ElapsedTimeDocument(Document):
|
|
||||||
url = Keyword()
|
|
||||||
time_taken = Long()
|
|
||||||
log_datetime = Date()
|
|
||||||
ip = Keyword()
|
|
||||||
geoip = Object(GeoIp, required=False)
|
|
||||||
useragent = Object(UserAgent, required=False)
|
|
||||||
|
|
||||||
class Index:
|
|
||||||
name = 'performance'
|
|
||||||
settings = {
|
|
||||||
"number_of_shards": 1,
|
|
||||||
"number_of_replicas": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
doc_type = 'ElapsedTime'
|
|
||||||
|
|
||||||
|
|
||||||
class ElaspedTimeDocumentManager:
|
|
||||||
@staticmethod
|
|
||||||
def build_index():
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
|
||||||
res = client.indices.exists(index="performance")
|
|
||||||
if not res:
|
|
||||||
ElapsedTimeDocument.init()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_index():
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
|
||||||
es.indices.delete(index='performance', ignore=[400, 404])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create(url, time_taken, log_datetime, useragent, ip):
|
|
||||||
ElaspedTimeDocumentManager.build_index()
|
|
||||||
ua = UserAgent()
|
|
||||||
ua.browser = UserAgentBrowser()
|
|
||||||
ua.browser.Family = useragent.browser.family
|
|
||||||
ua.browser.Version = useragent.browser.version_string
|
|
||||||
|
|
||||||
ua.os = UserAgentOS()
|
|
||||||
ua.os.Family = useragent.os.family
|
|
||||||
ua.os.Version = useragent.os.version_string
|
|
||||||
|
|
||||||
ua.device = UserAgentDevice()
|
|
||||||
ua.device.Family = useragent.device.family
|
|
||||||
ua.device.Brand = useragent.device.brand
|
|
||||||
ua.device.Model = useragent.device.model
|
|
||||||
ua.string = useragent.ua_string
|
|
||||||
ua.is_bot = useragent.is_bot
|
|
||||||
|
|
||||||
doc = ElapsedTimeDocument(
|
|
||||||
meta={
|
|
||||||
'id': int(
|
|
||||||
round(
|
|
||||||
time.time() *
|
|
||||||
1000))
|
|
||||||
},
|
|
||||||
url=url,
|
|
||||||
time_taken=time_taken,
|
|
||||||
log_datetime=log_datetime,
|
|
||||||
useragent=ua, ip=ip)
|
|
||||||
doc.save(pipeline="geoip")
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleDocument(Document):
|
|
||||||
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
|
||||||
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
|
||||||
author = Object(properties={
|
|
||||||
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
|
||||||
'id': Integer()
|
|
||||||
})
|
|
||||||
category = Object(properties={
|
|
||||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
|
||||||
'id': Integer()
|
|
||||||
})
|
|
||||||
tags = Object(properties={
|
|
||||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
|
||||||
'id': Integer()
|
|
||||||
})
|
|
||||||
|
|
||||||
pub_time = Date()
|
|
||||||
status = Text()
|
|
||||||
comment_status = Text()
|
|
||||||
type = Text()
|
|
||||||
views = Integer()
|
|
||||||
article_order = Integer()
|
|
||||||
|
|
||||||
class Index:
|
|
||||||
name = 'blog'
|
|
||||||
settings = {
|
|
||||||
"number_of_shards": 1,
|
|
||||||
"number_of_replicas": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
doc_type = 'Article'
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleDocumentManager():
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.create_index()
|
|
||||||
|
|
||||||
def create_index(self):
|
|
||||||
ArticleDocument.init()
|
|
||||||
|
|
||||||
def delete_index(self):
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
|
||||||
es.indices.delete(index='blog', ignore=[400, 404])
|
|
||||||
|
|
||||||
def convert_to_doc(self, articles):
|
|
||||||
return [
|
|
||||||
ArticleDocument(
|
|
||||||
meta={
|
|
||||||
'id': article.id},
|
|
||||||
body=article.body,
|
|
||||||
title=article.title,
|
|
||||||
author={
|
|
||||||
'nickname': article.author.username,
|
|
||||||
'id': article.author.id},
|
|
||||||
category={
|
|
||||||
'name': article.category.name,
|
|
||||||
'id': article.category.id},
|
|
||||||
tags=[
|
|
||||||
{
|
|
||||||
'name': t.name,
|
|
||||||
'id': t.id} for t in article.tags.all()],
|
|
||||||
pub_time=article.pub_time,
|
|
||||||
status=article.status,
|
|
||||||
comment_status=article.comment_status,
|
|
||||||
type=article.type,
|
|
||||||
views=article.views,
|
|
||||||
article_order=article.article_order) for article in articles]
|
|
||||||
|
|
||||||
def rebuild(self, articles=None):
|
|
||||||
ArticleDocument.init()
|
|
||||||
articles = articles if articles else Article.objects.all()
|
|
||||||
docs = self.convert_to_doc(articles)
|
|
||||||
for doc in docs:
|
|
||||||
doc.save()
|
|
||||||
|
|
||||||
def update_docs(self, docs):
|
|
||||||
for doc in docs:
|
|
||||||
doc.save()
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from haystack.forms import SearchForm
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSearchForm(SearchForm):
|
|
||||||
querydata = forms.CharField(required=True)
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
datas = super(BlogSearchForm, self).search()
|
|
||||||
if not self.is_valid():
|
|
||||||
return self.no_query_found()
|
|
||||||
|
|
||||||
if self.cleaned_data['querydata']:
|
|
||||||
logger.info(self.cleaned_data['querydata'])
|
|
||||||
return datas
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
|
|
||||||
ELASTICSEARCH_ENABLED
|
|
||||||
|
|
||||||
|
|
||||||
# TODO 参数化
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'build search index'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
if ELASTICSEARCH_ENABLED:
|
|
||||||
ElaspedTimeDocumentManager.build_index()
|
|
||||||
manager = ElapsedTimeDocument()
|
|
||||||
manager.init()
|
|
||||||
manager = ArticleDocumentManager()
|
|
||||||
manager.delete_index()
|
|
||||||
manager.rebuild()
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from blog.models import Tag, Category
|
|
||||||
|
|
||||||
|
|
||||||
# TODO 参数化
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'build search words'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
datas = set([t.name for t in Tag.objects.all()] +
|
|
||||||
[t.name for t in Category.objects.all()])
|
|
||||||
print('\n'.join(datas))
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from djangoblog.utils import cache
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'clear the whole cache'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
cache.clear()
|
|
||||||
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from blog.models import Article, Tag, Category
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'create test datas'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
user = get_user_model().objects.get_or_create(
|
|
||||||
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
|
|
||||||
|
|
||||||
pcategory = Category.objects.get_or_create(
|
|
||||||
name='我是父类目', parent_category=None)[0]
|
|
||||||
|
|
||||||
category = Category.objects.get_or_create(
|
|
||||||
name='子类目', parent_category=pcategory)[0]
|
|
||||||
|
|
||||||
category.save()
|
|
||||||
basetag = Tag()
|
|
||||||
basetag.name = "标签"
|
|
||||||
basetag.save()
|
|
||||||
for i in range(1, 20):
|
|
||||||
article = Article.objects.get_or_create(
|
|
||||||
category=category,
|
|
||||||
title='nice title ' + str(i),
|
|
||||||
body='nice content ' + str(i),
|
|
||||||
author=user)[0]
|
|
||||||
tag = Tag()
|
|
||||||
tag.name = "标签" + str(i)
|
|
||||||
tag.save()
|
|
||||||
article.tags.add(tag)
|
|
||||||
article.tags.add(basetag)
|
|
||||||
article.save()
|
|
||||||
|
|
||||||
from djangoblog.utils import cache
|
|
||||||
cache.clear()
|
|
||||||
self.stdout.write(self.style.SUCCESS('created test datas \n'))
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from djangoblog.spider_notify import SpiderNotify
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
from blog.models import Article, Tag, Category
|
|
||||||
|
|
||||||
site = get_current_site().domain
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'notify baidu url'
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
'data_type',
|
|
||||||
type=str,
|
|
||||||
choices=[
|
|
||||||
'all',
|
|
||||||
'article',
|
|
||||||
'tag',
|
|
||||||
'category'],
|
|
||||||
help='article : all article,tag : all tag,category: all category,all: All of these')
|
|
||||||
|
|
||||||
def get_full_url(self, path):
|
|
||||||
url = "https://{site}{path}".format(site=site, path=path)
|
|
||||||
return url
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
type = options['data_type']
|
|
||||||
self.stdout.write('start get %s' % type)
|
|
||||||
|
|
||||||
urls = []
|
|
||||||
if type == 'article' or type == 'all':
|
|
||||||
for article in Article.objects.filter(status='p'):
|
|
||||||
urls.append(article.get_full_url())
|
|
||||||
if type == 'tag' or type == 'all':
|
|
||||||
for tag in Tag.objects.all():
|
|
||||||
url = tag.get_absolute_url()
|
|
||||||
urls.append(self.get_full_url(url))
|
|
||||||
if type == 'category' or type == 'all':
|
|
||||||
for category in Category.objects.all():
|
|
||||||
url = category.get_absolute_url()
|
|
||||||
urls.append(self.get_full_url(url))
|
|
||||||
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.SUCCESS(
|
|
||||||
'start notify %d urls' %
|
|
||||||
len(urls)))
|
|
||||||
SpiderNotify.baidu_notify(urls)
|
|
||||||
self.stdout.write(self.style.SUCCESS('finish notify'))
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import requests
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.templatetags.static import static
|
|
||||||
|
|
||||||
from djangoblog.utils import save_user_avatar
|
|
||||||
from oauth.models import OAuthUser
|
|
||||||
from oauth.oauthmanager import get_manager_by_type
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'sync user avatar'
|
|
||||||
|
|
||||||
def test_picture(self, url):
|
|
||||||
try:
|
|
||||||
if requests.get(url, timeout=2).status_code == 200:
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
static_url = static("../")
|
|
||||||
users = OAuthUser.objects.all()
|
|
||||||
self.stdout.write(f'开始同步{len(users)}个用户头像')
|
|
||||||
for u in users:
|
|
||||||
self.stdout.write(f'开始同步:{u.nickname}')
|
|
||||||
url = u.picture
|
|
||||||
if url:
|
|
||||||
if url.startswith(static_url):
|
|
||||||
if self.test_picture(url):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if u.metadata:
|
|
||||||
manage = get_manager_by_type(u.type)
|
|
||||||
url = manage.get_picture(u.metadata)
|
|
||||||
url = save_user_avatar(url)
|
|
||||||
else:
|
|
||||||
url = static('blog/img/avatar.png')
|
|
||||||
else:
|
|
||||||
url = save_user_avatar(url)
|
|
||||||
else:
|
|
||||||
url = static('blog/img/avatar.png')
|
|
||||||
if url:
|
|
||||||
self.stdout.write(
|
|
||||||
f'结束同步:{u.nickname}.url:{url}')
|
|
||||||
u.picture = url
|
|
||||||
u.save()
|
|
||||||
self.stdout.write('结束同步')
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
from ipware import get_client_ip
|
|
||||||
from user_agents import parse
|
|
||||||
|
|
||||||
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class OnlineMiddleware(object):
|
|
||||||
def __init__(self, get_response=None):
|
|
||||||
self.get_response = get_response
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def __call__(self, request):
|
|
||||||
''' page render time '''
|
|
||||||
start_time = time.time()
|
|
||||||
response = self.get_response(request)
|
|
||||||
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
|
|
||||||
ip, _ = get_client_ip(request)
|
|
||||||
user_agent = parse(http_user_agent)
|
|
||||||
if not response.streaming:
|
|
||||||
try:
|
|
||||||
cast_time = time.time() - start_time
|
|
||||||
if ELASTICSEARCH_ENABLED:
|
|
||||||
time_taken = round((cast_time) * 1000, 2)
|
|
||||||
url = request.path
|
|
||||||
from django.utils import timezone
|
|
||||||
ElaspedTimeDocumentManager.create(
|
|
||||||
url=url,
|
|
||||||
time_taken=time_taken,
|
|
||||||
log_datetime=timezone.now(),
|
|
||||||
useragent=user_agent,
|
|
||||||
ip=ip)
|
|
||||||
response.content = response.content.replace(
|
|
||||||
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error OnlineMiddleware: %s" % e)
|
|
||||||
|
|
||||||
return response
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import mdeditor.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='BlogSettings',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
|
|
||||||
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
|
|
||||||
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
|
|
||||||
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
|
|
||||||
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
|
|
||||||
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
|
|
||||||
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
|
|
||||||
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
|
|
||||||
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
|
|
||||||
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
|
|
||||||
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
|
|
||||||
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
|
|
||||||
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
|
|
||||||
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
|
|
||||||
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '网站配置',
|
|
||||||
'verbose_name_plural': '网站配置',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Links',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
|
|
||||||
('link', models.URLField(verbose_name='链接地址')),
|
|
||||||
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
|
||||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
|
||||||
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '友情链接',
|
|
||||||
'verbose_name_plural': '友情链接',
|
|
||||||
'ordering': ['sequence'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SideBar',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=100, verbose_name='标题')),
|
|
||||||
('content', models.TextField(verbose_name='内容')),
|
|
||||||
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
|
||||||
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '侧边栏',
|
|
||||||
'verbose_name_plural': '侧边栏',
|
|
||||||
'ordering': ['sequence'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Tag',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
|
|
||||||
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '标签',
|
|
||||||
'verbose_name_plural': '标签',
|
|
||||||
'ordering': ['name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Category',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
|
|
||||||
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
|
||||||
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
|
|
||||||
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '分类',
|
|
||||||
'verbose_name_plural': '分类',
|
|
||||||
'ordering': ['-index'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Article',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
|
|
||||||
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
|
|
||||||
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
|
|
||||||
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
|
|
||||||
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
|
|
||||||
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
|
|
||||||
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
|
|
||||||
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
|
|
||||||
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
|
|
||||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
|
||||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
|
|
||||||
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '文章',
|
|
||||||
'verbose_name_plural': '文章',
|
|
||||||
'ordering': ['-article_order', '-pub_time'],
|
|
||||||
'get_latest_by': 'id',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-29 06:08
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('blog', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='global_footer',
|
|
||||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='global_header',
|
|
||||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 4.2.1 on 2023-05-09 07:45
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('blog', '0002_blogsettings_global_footer_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='comment_need_review',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 4.2.1 on 2023-05-09 07:51
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('blog', '0003_blogsettings_comment_need_review'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
old_name='analyticscode',
|
|
||||||
new_name='analytics_code',
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
old_name='beiancode',
|
|
||||||
new_name='beian_code',
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
old_name='sitename',
|
|
||||||
new_name='site_name',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,300 +0,0 @@
|
|||||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import mdeditor.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='article',
|
|
||||||
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='category',
|
|
||||||
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='links',
|
|
||||||
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='sidebar',
|
|
||||||
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='tag',
|
|
||||||
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='article',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='article',
|
|
||||||
name='last_mod_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='category',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='category',
|
|
||||||
name='last_mod_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='links',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='tag',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='tag',
|
|
||||||
name='last_mod_time',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='article',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='article',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='links',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='tag',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='tag',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='article_order',
|
|
||||||
field=models.IntegerField(default=0, verbose_name='order'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='author',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='body',
|
|
||||||
field=mdeditor.fields.MDTextField(verbose_name='body'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='category',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='comment_status',
|
|
||||||
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='pub_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='show_toc',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='show toc'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='status',
|
|
||||||
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='tags',
|
|
||||||
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='title',
|
|
||||||
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='type',
|
|
||||||
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='article',
|
|
||||||
name='views',
|
|
||||||
field=models.PositiveIntegerField(default=0, verbose_name='views'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='article_comment_count',
|
|
||||||
field=models.IntegerField(default=5, verbose_name='article comment count'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='article_sub_length',
|
|
||||||
field=models.IntegerField(default=300, verbose_name='article sub length'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='google_adsense_codes',
|
|
||||||
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='open_site_comment',
|
|
||||||
field=models.BooleanField(default=True, verbose_name='open site comment'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='show_google_adsense',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='show adsense'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='sidebar_article_count',
|
|
||||||
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='sidebar_comment_count',
|
|
||||||
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='site_description',
|
|
||||||
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='site_keywords',
|
|
||||||
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='site_name',
|
|
||||||
field=models.CharField(default='', max_length=200, verbose_name='site name'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='blogsettings',
|
|
||||||
name='site_seo_description',
|
|
||||||
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='category',
|
|
||||||
name='index',
|
|
||||||
field=models.IntegerField(default=0, verbose_name='index'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='category',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='category',
|
|
||||||
name='parent_category',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='is_enable',
|
|
||||||
field=models.BooleanField(default=True, verbose_name='is show'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='last_mod_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='link',
|
|
||||||
field=models.URLField(verbose_name='link'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='sequence',
|
|
||||||
field=models.IntegerField(unique=True, verbose_name='order'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='links',
|
|
||||||
name='show_type',
|
|
||||||
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='content',
|
|
||||||
field=models.TextField(verbose_name='content'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='is_enable',
|
|
||||||
field=models.BooleanField(default=True, verbose_name='is enable'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='last_mod_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=100, verbose_name='title'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='sequence',
|
|
||||||
field=models.IntegerField(unique=True, verbose_name='order'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='tag',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 4.2.7 on 2024-01-26 02:41
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='blogsettings',
|
|
||||||
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from mdeditor.fields import MDTextField
|
|
||||||
from uuslug import slugify
|
|
||||||
|
|
||||||
from djangoblog.utils import cache_decorator, cache
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinkShowType(models.TextChoices):
|
|
||||||
I = ('i', _('index'))
|
|
||||||
L = ('l', _('list'))
|
|
||||||
P = ('p', _('post'))
|
|
||||||
A = ('a', _('all'))
|
|
||||||
S = ('s', _('slide'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
is_update_views = isinstance(
|
|
||||||
self,
|
|
||||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
|
||||||
if is_update_views:
|
|
||||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
|
||||||
else:
|
|
||||||
if 'slug' in self.__dict__:
|
|
||||||
slug = getattr(
|
|
||||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
|
||||||
self, 'name')
|
|
||||||
setattr(self, 'slug', slugify(slug))
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_absolute_url(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
|
||||||
"""文章"""
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('d', _('Draft')),
|
|
||||||
('p', _('Published')),
|
|
||||||
)
|
|
||||||
COMMENT_STATUS = (
|
|
||||||
('o', _('Open')),
|
|
||||||
('c', _('Close')),
|
|
||||||
)
|
|
||||||
TYPE = (
|
|
||||||
('a', _('Article')),
|
|
||||||
('p', _('Page')),
|
|
||||||
)
|
|
||||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
|
||||||
body = MDTextField(_('body'))
|
|
||||||
pub_time = models.DateTimeField(
|
|
||||||
_('publish time'), blank=False, null=False, default=now)
|
|
||||||
status = models.CharField(
|
|
||||||
_('status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=STATUS_CHOICES,
|
|
||||||
default='p')
|
|
||||||
comment_status = models.CharField(
|
|
||||||
_('comment status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=COMMENT_STATUS,
|
|
||||||
default='o')
|
|
||||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
|
||||||
views = models.PositiveIntegerField(_('views'), default=0)
|
|
||||||
author = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name=_('author'),
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
article_order = models.IntegerField(
|
|
||||||
_('order'), blank=False, null=False, default=0)
|
|
||||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
'Category',
|
|
||||||
verbose_name=_('category'),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=False,
|
|
||||||
null=False)
|
|
||||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
|
||||||
|
|
||||||
def body_to_string(self):
|
|
||||||
return self.body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-article_order', '-pub_time']
|
|
||||||
verbose_name = _('article')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:detailbyid', kwargs={
|
|
||||||
'article_id': self.id,
|
|
||||||
'year': self.creation_time.year,
|
|
||||||
'month': self.creation_time.month,
|
|
||||||
'day': self.creation_time.day
|
|
||||||
})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
tree = self.category.get_category_tree()
|
|
||||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def viewed(self):
|
|
||||||
self.views += 1
|
|
||||||
self.save(update_fields=['views'])
|
|
||||||
|
|
||||||
def comment_list(self):
|
|
||||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
if value:
|
|
||||||
logger.info('get article comments:{id}'.format(id=self.id))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
|
||||||
cache.set(cache_key, comments, 60 * 100)
|
|
||||||
logger.info('set article comments:{id}'.format(id=self.id))
|
|
||||||
return comments
|
|
||||||
|
|
||||||
def get_admin_url(self):
|
|
||||||
info = (self._meta.app_label, self._meta.model_name)
|
|
||||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def next_article(self):
|
|
||||||
# 下一篇
|
|
||||||
return Article.objects.filter(
|
|
||||||
id__gt=self.id, status='p').order_by('id').first()
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def prev_article(self):
|
|
||||||
# 前一篇
|
|
||||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
|
||||||
|
|
||||||
def get_first_image_url(self):
|
|
||||||
"""
|
|
||||||
Get the first image url from article.body.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
"""文章分类"""
|
|
||||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
|
||||||
parent_category = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
verbose_name=_('parent category'),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-index']
|
|
||||||
verbose_name = _('category')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:category_detail', kwargs={
|
|
||||||
'category_name': self.slug})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
"""
|
|
||||||
递归获得分类目录的父级
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
categorys.append(category)
|
|
||||||
if category.parent_category:
|
|
||||||
parse(category.parent_category)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_sub_categorys(self):
|
|
||||||
"""
|
|
||||||
获得当前分类目录所有子集
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
all_categorys = Category.objects.all()
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(category)
|
|
||||||
childs = all_categorys.filter(parent_category=category)
|
|
||||||
for child in childs:
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(child)
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(BaseModel):
|
|
||||||
"""文章标签"""
|
|
||||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_article_count(self):
|
|
||||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
verbose_name = _('tag')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
class Links(models.Model):
|
|
||||||
"""友情链接"""
|
|
||||||
|
|
||||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
|
||||||
link = models.URLField(_('link'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(
|
|
||||||
_('is show'), default=True, blank=False, null=False)
|
|
||||||
show_type = models.CharField(
|
|
||||||
_('show type'),
|
|
||||||
max_length=1,
|
|
||||||
choices=LinkShowType.choices,
|
|
||||||
default=LinkShowType.I)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('link')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class SideBar(models.Model):
|
|
||||||
"""侧边栏,可以展示一些html内容"""
|
|
||||||
name = models.CharField(_('title'), max_length=100)
|
|
||||||
content = models.TextField(_('content'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('sidebar')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettings(models.Model):
|
|
||||||
"""blog的配置"""
|
|
||||||
site_name = models.CharField(
|
|
||||||
_('site name'),
|
|
||||||
max_length=200,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_description = models.TextField(
|
|
||||||
_('site description'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_seo_description = models.TextField(
|
|
||||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
|
||||||
site_keywords = models.TextField(
|
|
||||||
_('site keywords'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
|
||||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
|
||||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
|
||||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
|
||||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
|
||||||
google_adsense_codes = models.TextField(
|
|
||||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
|
||||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
|
||||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
|
||||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
|
||||||
beian_code = models.CharField(
|
|
||||||
'备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
analytics_code = models.TextField(
|
|
||||||
"网站统计代码",
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
show_gongan_code = models.BooleanField(
|
|
||||||
'是否显示公安备案号', default=False, null=False)
|
|
||||||
gongan_beiancode = models.TextField(
|
|
||||||
'公安备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
comment_need_review = models.BooleanField(
|
|
||||||
'评论是否需要审核', default=False, null=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Website configuration')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.site_name
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if BlogSettings.objects.exclude(id=self.id).count():
|
|
||||||
raise ValidationError(_('There can only be one configuration'))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
from djangoblog.utils import cache
|
|
||||||
cache.clear()
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
from haystack import indexes
|
|
||||||
|
|
||||||
from blog.models import Article
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
|
|
||||||
text = indexes.CharField(document=True, use_template=True)
|
|
||||||
|
|
||||||
def get_model(self):
|
|
||||||
return Article
|
|
||||||
|
|
||||||
def index_queryset(self, using=None):
|
|
||||||
return self.get_model().objects.filter(status='p')
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.button {
|
|
||||||
border: none;
|
|
||||||
padding: 4px 80px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 4px 2px;
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
let wait = 60;
|
|
||||||
|
|
||||||
function time(o) {
|
|
||||||
if (wait == 0) {
|
|
||||||
o.removeAttribute("disabled");
|
|
||||||
o.value = "获取验证码";
|
|
||||||
wait = 60
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
o.setAttribute("disabled", true);
|
|
||||||
o.value = "重新发送(" + wait + ")";
|
|
||||||
wait--;
|
|
||||||
setTimeout(function () {
|
|
||||||
time(o)
|
|
||||||
},
|
|
||||||
1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("btn").onclick = function () {
|
|
||||||
let id_email = $("#id_email")
|
|
||||||
let token = $("*[name='csrfmiddlewaretoken']").val()
|
|
||||||
let ts = this
|
|
||||||
let myErr = $("#myErr")
|
|
||||||
$.ajax(
|
|
||||||
{
|
|
||||||
url: "/forget_password_code/",
|
|
||||||
type: "POST",
|
|
||||||
data: {
|
|
||||||
"email": id_email.val(),
|
|
||||||
"csrfmiddlewaretoken": token
|
|
||||||
},
|
|
||||||
success: function (result) {
|
|
||||||
if (result != "ok") {
|
|
||||||
myErr.remove()
|
|
||||||
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
myErr.remove()
|
|
||||||
time(ts)
|
|
||||||
},
|
|
||||||
error: function (e) {
|
|
||||||
alert("发送失败,请重试")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,13 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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; }
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
body {
|
|
||||||
padding-top: 40px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin {
|
|
||||||
max-width: 330px;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.form-signin-heading {
|
|
||||||
margin: 0 0 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.form-signin .checkbox {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.form-signin .form-control {
|
|
||||||
position: relative;
|
|
||||||
height: auto;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.form-signin .form-control:focus {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.form-signin input[type="email"] {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.form-signin input[type="password"] {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
width: 304px;
|
|
||||||
padding: 20px 25px 30px;
|
|
||||||
margin: 0 auto 25px;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
|
|
||||||
}
|
|
||||||
.card-signin {
|
|
||||||
width: 354px;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
.card-signin .profile-img {
|
|
||||||
display: block;
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 221 B |
@ -1,51 +0,0 @@
|
|||||||
// 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!')
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
@ -1,273 +0,0 @@
|
|||||||
/*
|
|
||||||
Styles for older IE versions (previous to IE9).
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
|
||||||
body.custom-background-empty {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
body.custom-background-empty .site,
|
|
||||||
body.custom-background-white .site {
|
|
||||||
box-shadow: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.assistive-text,
|
|
||||||
.site .screen-reader-text {
|
|
||||||
clip: rect(1px 1px 1px 1px);
|
|
||||||
}
|
|
||||||
.full-width .site-content {
|
|
||||||
float: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
img.size-full,
|
|
||||||
img.size-large,
|
|
||||||
img.header-image,
|
|
||||||
img.wp-post-image,
|
|
||||||
img[class*="align"],
|
|
||||||
img[class*="wp-image-"],
|
|
||||||
img[class*="attachment-"] {
|
|
||||||
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
|
|
||||||
}
|
|
||||||
.author-avatar {
|
|
||||||
float: left;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-top: 0.571428571rem;
|
|
||||||
}
|
|
||||||
.author-description {
|
|
||||||
float: right;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
.site {
|
|
||||||
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
|
|
||||||
margin: 48px auto;
|
|
||||||
max-width: 960px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 40px;
|
|
||||||
}
|
|
||||||
.site-content {
|
|
||||||
float: left;
|
|
||||||
width: 65.104166667%;
|
|
||||||
}
|
|
||||||
body.template-front-page .site-content,
|
|
||||||
body.attachment .site-content,
|
|
||||||
body.full-width .site-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.widget-area {
|
|
||||||
float: right;
|
|
||||||
width: 26.041666667%;
|
|
||||||
}
|
|
||||||
.site-header h1,
|
|
||||||
.site-header h2 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.site-header h1 {
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 1.846153846;
|
|
||||||
}
|
|
||||||
.main-navigation ul.nav-menu,
|
|
||||||
.main-navigation div.nav-menu > ul {
|
|
||||||
border-bottom: 1px solid #ededed;
|
|
||||||
border-top: 1px solid #ededed;
|
|
||||||
display: inline-block !important;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.main-navigation ul {
|
|
||||||
margin: 0;
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
.main-navigation li a,
|
|
||||||
.main-navigation li {
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.ie7 .main-navigation li a,
|
|
||||||
.ie7 .main-navigation li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.main-navigation li a {
|
|
||||||
border-bottom: 0;
|
|
||||||
color: #6a6a6a;
|
|
||||||
line-height: 3.692307692;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.main-navigation li a:hover {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
.main-navigation li {
|
|
||||||
margin: 0 40px 0 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.main-navigation li ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
height: 1px;
|
|
||||||
width: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
|
||||||
}
|
|
||||||
.ie7 .main-navigation li ul {
|
|
||||||
clip: inherit;
|
|
||||||
display: none;
|
|
||||||
left: 0;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
.main-navigation li ul ul,
|
|
||||||
.ie7 .main-navigation li ul ul {
|
|
||||||
top: 0;
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
.main-navigation ul li:hover > ul,
|
|
||||||
.main-navigation ul li:focus > ul,
|
|
||||||
.main-navigation .focus > ul {
|
|
||||||
border-left: 0;
|
|
||||||
clip: inherit;
|
|
||||||
overflow: inherit;
|
|
||||||
height: inherit;
|
|
||||||
width: inherit;
|
|
||||||
}
|
|
||||||
.ie7 .main-navigation ul li:hover > ul,
|
|
||||||
.ie7 .main-navigation ul li:focus > ul {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.main-navigation li ul li a {
|
|
||||||
background: #efefef;
|
|
||||||
border-bottom: 1px solid #ededed;
|
|
||||||
display: block;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 2.181818182;
|
|
||||||
padding: 8px 10px;
|
|
||||||
width: 180px;
|
|
||||||
}
|
|
||||||
.main-navigation li ul li a:hover {
|
|
||||||
background: #e3e3e3;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
.main-navigation .current-menu-item > a,
|
|
||||||
.main-navigation .current-menu-ancestor > a,
|
|
||||||
.main-navigation .current_page_item > a,
|
|
||||||
.main-navigation .current_page_ancestor > a {
|
|
||||||
color: #636363;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.main-navigation .menu-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.entry-header .entry-title {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
#respond form input[type="text"] {
|
|
||||||
width: 46.333333333%;
|
|
||||||
}
|
|
||||||
#respond form textarea.blog-textarea {
|
|
||||||
width: 79.666666667%;
|
|
||||||
}
|
|
||||||
.template-front-page .site-content,
|
|
||||||
.template-front-page article {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.template-front-page.has-post-thumbnail article {
|
|
||||||
float: left;
|
|
||||||
width: 47.916666667%;
|
|
||||||
}
|
|
||||||
.entry-page-image {
|
|
||||||
float: right;
|
|
||||||
margin-bottom: 0;
|
|
||||||
width: 47.916666667%;
|
|
||||||
}
|
|
||||||
/* IE Front Page Template Widget fix */
|
|
||||||
.template-front-page .widget-area {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.template-front-page .widget {
|
|
||||||
width: 100% !important;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.template-front-page .widget-area .widget,
|
|
||||||
.template-front-page .first.front-widgets,
|
|
||||||
.template-front-page.two-sidebars .widget-area .front-widgets {
|
|
||||||
float: left;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
width: 51.875%;
|
|
||||||
}
|
|
||||||
.template-front-page .second.front-widgets,
|
|
||||||
.template-front-page .widget-area .widget:nth-child(odd) {
|
|
||||||
clear: right;
|
|
||||||
}
|
|
||||||
.template-front-page .first.front-widgets,
|
|
||||||
.template-front-page .second.front-widgets,
|
|
||||||
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
|
|
||||||
float: right;
|
|
||||||
margin: 0 0 24px;
|
|
||||||
width: 39.0625%;
|
|
||||||
}
|
|
||||||
.template-front-page.two-sidebars .widget,
|
|
||||||
.template-front-page.two-sidebars .widget:nth-child(even) {
|
|
||||||
float: none;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
/* add input font for <IE9 Password Box to make the bullets show up */
|
|
||||||
input[type="password"] {
|
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RTL overrides for IE7 and IE8
|
|
||||||
-------------------------------------------------------------- */
|
|
||||||
.rtl .site-header h1,
|
|
||||||
.rtl .site-header h2 {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.rtl .widget-area,
|
|
||||||
.rtl .author-description {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.rtl .author-avatar,
|
|
||||||
.rtl .site-content {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.rtl .main-navigation ul.nav-menu,
|
|
||||||
.rtl .main-navigation div.nav-menu > ul {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.rtl .main-navigation ul li ul li,
|
|
||||||
.rtl .main-navigation ul li ul li ul li {
|
|
||||||
margin-left: 40px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
.rtl .main-navigation li ul ul {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.ie7 .rtl .main-navigation li ul ul {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.ie7 .rtl .main-navigation ul li {
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
.ie7 .rtl .main-navigation li ul {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.ie7 .rtl .main-navigation li {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
.ie7 .rtl .main-navigation li ul ul ul {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
/* 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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,305 +0,0 @@
|
|||||||
|
|
||||||
.icon-sn-google {
|
|
||||||
background-position: 0 -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-google {
|
|
||||||
background-color: #4285f4;
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-google {
|
|
||||||
color: #4285f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-github {
|
|
||||||
background-position: -28px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-github {
|
|
||||||
background-color: #333;
|
|
||||||
background-position: -28px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-github {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-weibo {
|
|
||||||
background-position: -56px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-weibo {
|
|
||||||
background-color: #e90d24;
|
|
||||||
background-position: -56px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-weibo {
|
|
||||||
color: #e90d24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-qq {
|
|
||||||
background-position: -84px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-qq {
|
|
||||||
background-color: #0098e6;
|
|
||||||
background-position: -84px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-qq {
|
|
||||||
color: #0098e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-twitter {
|
|
||||||
background-position: -112px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-twitter {
|
|
||||||
background-color: #50abf1;
|
|
||||||
background-position: -112px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-twitter {
|
|
||||||
color: #50abf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-facebook {
|
|
||||||
background-position: -140px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-facebook {
|
|
||||||
background-color: #4862a3;
|
|
||||||
background-position: -140px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-facebook {
|
|
||||||
color: #4862a3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-renren {
|
|
||||||
background-position: -168px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-renren {
|
|
||||||
background-color: #197bc8;
|
|
||||||
background-position: -168px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-renren {
|
|
||||||
color: #197bc8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-tqq {
|
|
||||||
background-position: -196px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-tqq {
|
|
||||||
background-color: #1f9ed2;
|
|
||||||
background-position: -196px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-tqq {
|
|
||||||
color: #1f9ed2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-douban {
|
|
||||||
background-position: -224px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-douban {
|
|
||||||
background-color: #279738;
|
|
||||||
background-position: -224px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-douban {
|
|
||||||
color: #279738;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-weixin {
|
|
||||||
background-position: -252px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-weixin {
|
|
||||||
background-color: #00b500;
|
|
||||||
background-position: -252px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-weixin {
|
|
||||||
color: #00b500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-dotted {
|
|
||||||
background-position: -280px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-dotted {
|
|
||||||
background-color: #eee;
|
|
||||||
background-position: -280px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-dotted {
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-site {
|
|
||||||
background-position: -308px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-site {
|
|
||||||
background-color: #00b500;
|
|
||||||
background-position: -308px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-site {
|
|
||||||
color: #00b500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-linkedin {
|
|
||||||
background-position: -336px -28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sn-bg-linkedin {
|
|
||||||
background-color: #0077b9;
|
|
||||||
background-position: -336px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-sn-linkedin {
|
|
||||||
color: #0077b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*=icon-sn-] {
|
|
||||||
display: inline-block;
|
|
||||||
background-image: url('../img/icon-sn.svg');
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-size: auto 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*=icon-sn-]:hover {
|
|
||||||
opacity: .8;
|
|
||||||
filter: alpha(opacity=80);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-google {
|
|
||||||
background: #4285f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
|
|
||||||
background: #2a75f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-github {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
|
|
||||||
background: #262626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-weibo {
|
|
||||||
background: #e90d24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
|
|
||||||
background: #d10c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-qq {
|
|
||||||
background: #0098e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
|
|
||||||
background: #0087cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-twitter {
|
|
||||||
background: #50abf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
|
|
||||||
background: #38a0ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-facebook {
|
|
||||||
background: #4862a3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
|
|
||||||
background: #405791;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-renren {
|
|
||||||
background: #197bc8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
|
|
||||||
background: #166db1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-tqq {
|
|
||||||
background: #1f9ed2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
|
|
||||||
background: #1c8dbc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-douban {
|
|
||||||
background: #279738;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
|
|
||||||
background: #228330;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-weixin {
|
|
||||||
background: #00b500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
|
|
||||||
background: #009c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-dotted {
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
|
|
||||||
background: #e1e1e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-site {
|
|
||||||
background: #00b500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
|
|
||||||
background: #009c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-linkedin {
|
|
||||||
background: #0077b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
|
|
||||||
background: #0067a0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-more {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*=btn-sn-] [class*=icon-sn-] {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue