Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
@ -0,0 +1,10 @@
|
|||||||
|
[run]
|
||||||
|
source = .
|
||||||
|
include = *.py
|
||||||
|
omit =
|
||||||
|
*migrations*
|
||||||
|
*tests*
|
||||||
|
*.html
|
||||||
|
*whoosh_cn_backend*
|
||||||
|
*settings.py*
|
||||||
|
*venv*
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
name: Django CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.css'
|
||||||
|
- '**/*.js'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.css'
|
||||||
|
- '**/*.js'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-normal:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10","3.11" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Start MySQL
|
||||||
|
uses: samin/mysql-action@v1.3
|
||||||
|
with:
|
||||||
|
host port: 3306
|
||||||
|
container port: 3306
|
||||||
|
character set server: utf8mb4
|
||||||
|
collation server: utf8mb4_general_ci
|
||||||
|
mysql version: latest
|
||||||
|
mysql root password: root
|
||||||
|
mysql database: djangoblog
|
||||||
|
mysql user: root
|
||||||
|
mysql password: root
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
- name: Run Tests
|
||||||
|
env:
|
||||||
|
DJANGO_MYSQL_PASSWORD: root
|
||||||
|
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||||
|
run: |
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py test
|
||||||
|
|
||||||
|
build-with-es:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10","3.11" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Start MySQL
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Configure sysctl limits
|
||||||
|
run: |
|
||||||
|
sudo swapoff -a
|
||||||
|
sudo sysctl -w vm.swappiness=1
|
||||||
|
sudo sysctl -w fs.file-max=262144
|
||||||
|
sudo sysctl -w vm.max_map_count=262144
|
||||||
|
|
||||||
|
- uses: miyataka/elasticsearch-github-actions@1
|
||||||
|
|
||||||
|
with:
|
||||||
|
stack-version: '7.12.1'
|
||||||
|
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
|
||||||
|
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
- name: Run Tests
|
||||||
|
env:
|
||||||
|
DJANGO_MYSQL_PASSWORD: root
|
||||||
|
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||||
|
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
|
||||||
|
run: |
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
coverage run manage.py test
|
||||||
|
coverage xml
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
tags: djangoblog/djangoblog:dev
|
||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@ -0,0 +1 @@
|
|||||||
|
undefined
|
||||||
@ -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
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*!
|
||||||
|
* 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)
|
||||||
|
*/(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)}})();;/*!
|
||||||
|
* 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/.
|
||||||
|
*/(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(){var jscriptVersion=new Function('/*@cc_on return @_jscript_version; @*/')()
|
||||||
|
if(jscriptVersion===undefined){return 11}
|
||||||
|
if(jscriptVersion<9){return 8}
|
||||||
|
return jscriptVersion}
|
||||||
|
var ua=window.navigator.userAgent
|
||||||
|
if(ua.indexOf('Opera')>-1||ua.indexOf('Presto')>-1){return}
|
||||||
|
var emulated=emulatedIEMajorVersion()
|
||||||
|
if(emulated===null){return}
|
||||||
|
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,120 +0,0 @@
|
|||||||
|
|
||||||
# 导入Django管理后台相关模块
|
|
||||||
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 _
|
|
||||||
|
|
||||||
# 注册模型
|
|
||||||
from .models import Article
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 文章模型的自定义表单
|
|
||||||
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') # 过滤器
|
|
||||||
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]
|
|
||||||
|
|
||||||
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,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,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("发送失败,请重试")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
@ -1,378 +0,0 @@
|
|||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: fallback;
|
|
||||||
src: url(mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||