Full Code of liangliangyy/DjangoBlog for AI

master 866a07b72ce6 cached
268 files
1.0 MB
283.7k tokens
1161 symbols
1 requests
Download .txt
Showing preview only (1,109K chars total). Download the full file or copy to clipboard to get everything.
Repository: liangliangyy/DjangoBlog
Branch: master
Commit: 866a07b72ce6
Files: 268
Total size: 1.0 MB

Directory structure:
gitextract_z9fgi4z9/

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── deploy-master.yml
│       ├── django.yml
│       ├── docker.yml
│       ├── frontend.yml
│       └── publish-release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── accounts/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── templatetags/
│   │   └── __init__.py
│   ├── test_admin.py
│   ├── test_user_business_logic.py
│   ├── tests.py
│   ├── urls.py
│   ├── user_login_backend.py
│   ├── utils.py
│   └── views.py
├── blog/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── context_processors.py
│   ├── documents.py
│   ├── forms.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       ├── build_index.py
│   │       ├── build_search_words.py
│   │       ├── clear_cache.py
│   │       ├── create_testdata.py
│   │       ├── ping_baidu.py
│   │       └── sync_user_avatar.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_blogsettings_global_footer_and_more.py
│   │   ├── 0003_blogsettings_comment_need_review.py
│   │   ├── 0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
│   │   ├── 0005_alter_article_options_alter_category_options_and_more.py
│   │   ├── 0006_alter_blogsettings_options.py
│   │   ├── 0007_article_idx_type_status_pub_article_idx_status_views_and_more.py
│   │   ├── 0008_blogsettings_color_scheme.py
│   │   └── __init__.py
│   ├── models.py
│   ├── search_indexes.py
│   ├── static/
│   │   ├── account/
│   │   │   ├── css/
│   │   │   │   └── account.css
│   │   │   └── js/
│   │   │       └── account.js
│   │   ├── assets/
│   │   │   └── css/
│   │   │       └── signin.css
│   │   ├── blog/
│   │   │   ├── css/
│   │   │   │   └── oauth_style.css
│   │   │   ├── fonts/
│   │   │   │   └── open-sans.css
│   │   │   └── js/
│   │   │       └── mathjax-loader.js
│   │   └── pygments/
│   │       └── default.css
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── blog_tags.py
│   │   └── vite_tags.py
│   ├── test_admin.py
│   ├── test_article_business_logic.py
│   ├── test_context_processors.py
│   ├── test_middleware.py
│   ├── test_templatetags.py
│   ├── test_views.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── codecov.yml
├── comments/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_comment_is_enable.py
│   │   ├── 0003_alter_comment_options_remove_comment_created_time_and_more.py
│   │   ├── 0004_comment_idx_art_parent_enable_comment_idx_enable_id.py
│   │   ├── 0005_commentreaction.py
│   │   └── __init__.py
│   ├── models.py
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── comments_tags.py
│   ├── test_comment_business_logic.py
│   ├── test_views.py
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py
│   └── views.py
├── deploy/
│   ├── docker-compose/
│   │   ├── docker-compose.es.yml
│   │   └── docker-compose.yml
│   ├── entrypoint.sh
│   ├── k8s/
│   │   ├── configmap.yaml
│   │   ├── deployment.yaml
│   │   ├── gateway.yaml
│   │   ├── pv.yaml
│   │   ├── pvc.yaml
│   │   ├── service.yaml
│   │   └── storageclass.yaml
│   └── nginx.conf
├── djangoblog/
│   ├── __init__.py
│   ├── admin_site.py
│   ├── apps.py
│   ├── base_views.py
│   ├── blog_signals.py
│   ├── constants.py
│   ├── elasticsearch_backend.py
│   ├── error_views.py
│   ├── feeds.py
│   ├── logentryadmin.py
│   ├── mixins.py
│   ├── plugin_manage/
│   │   ├── base_plugin.py
│   │   ├── hook_constants.py
│   │   ├── hooks.py
│   │   └── loader.py
│   ├── settings.py
│   ├── sitemap.py
│   ├── spider_notify.py
│   ├── test_base.py
│   ├── test_email_integration.py
│   ├── test_email_integration_complete.py
│   ├── test_plugins.py
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py
│   ├── whoosh_cn_backend.py
│   └── wsgi.py
├── docs/
│   ├── README-en.md
│   ├── config-en.md
│   ├── config.md
│   ├── docker-en.md
│   ├── docker.md
│   ├── es.md
│   ├── k8s-en.md
│   ├── k8s.md
│   └── search-engine-config.md
├── frontend/
│   ├── .gitignore
│   ├── package.json
│   ├── postcss.config.js
│   ├── src/
│   │   ├── components/
│   │   │   ├── backToTop.js
│   │   │   ├── commentSystem.js
│   │   │   ├── imageLightbox.js
│   │   │   ├── navigation.js
│   │   │   └── reactionPicker.js
│   │   ├── features/
│   │   │   └── darkMode.js
│   │   ├── main.js
│   │   ├── styles/
│   │   │   └── main.css
│   │   └── utils/
│   │       └── nprogress.js
│   ├── tailwind.config.js
│   └── vite.config.js
├── locale/
│   ├── en/
│   │   └── LC_MESSAGES/
│   │       ├── django.mo
│   │       └── django.po
│   ├── zh_Hans/
│   │   └── LC_MESSAGES/
│   │       ├── django.mo
│   │       └── django.po
│   └── zh_Hant/
│       └── LC_MESSAGES/
│           ├── django.mo
│           └── django.po
├── manage.py
├── oauth/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
│   │   ├── 0003_alter_oauthuser_nickname.py
│   │   └── __init__.py
│   ├── models.py
│   ├── oauthmanager.py
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── oauth_tags.py
│   ├── test_oauth_business_logic.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── owntracks/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_owntracklog_options_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── plugins/
│   ├── __init__.py
│   ├── article_copyright/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── article_recommendation/
│   │   ├── __init__.py
│   │   ├── plugin.py
│   │   └── static/
│   │       └── article_recommendation/
│   │           ├── css/
│   │           │   └── recommendation.css
│   │           └── js/
│   │               └── recommendation.js
│   ├── cloudflare_cache/
│   │   ├── __init__.py
│   │   ├── api.py
│   │   ├── handlers.py
│   │   └── plugin.py
│   ├── external_links/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── image_lazy_loading/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── reading_time/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── seo_optimizer/
│   │   ├── __init__.py
│   │   └── plugin.py
│   └── view_count/
│       ├── __init__.py
│       └── plugin.py
├── requirements.txt
├── servermanager/
│   ├── MemcacheStorage.py
│   ├── __init__.py
│   ├── admin.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── blogapi.py
│   │   └── commonapi.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_emailsendlog_options_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── robot.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── templates/
    ├── account/
    │   ├── forget_password.html
    │   ├── login.html
    │   ├── registration_form.html
    │   └── result.html
    ├── blog/
    │   ├── article_archives.html
    │   ├── article_detail.html
    │   ├── article_index.html
    │   ├── error_page.html
    │   ├── links_list.html
    │   └── tags/
    │       ├── article_info.html
    │       ├── article_info_highlight.html
    │       ├── article_meta_info.html
    │       ├── article_pagination.html
    │       ├── article_tag_list.html
    │       ├── breadcrumb.html
    │       └── sidebar.html
    ├── comments/
    │   └── tags/
    │       ├── comment_item.html
    │       ├── comment_item_modern.html
    │       ├── comment_item_tree.html
    │       ├── comment_list.html
    │       ├── comment_list_modern.html
    │       ├── post_comment.html
    │       └── post_comment_modern.html
    ├── oauth/
    │   ├── bindsuccess.html
    │   ├── oauth_applications.html
    │   └── require_email.html
    ├── owntracks/
    │   ├── show_log_dates.html
    │   └── show_maps.html
    ├── plugins/
    │   ├── article_recommendation/
    │   │   ├── __init__.py
    │   │   ├── bottom_widget.html
    │   │   └── sidebar_widget.html
    │   ├── css_includes.html
    │   └── js_includes.html
    ├── search/
    │   ├── indexes/
    │   │   └── blog/
    │   │       └── article_text.txt
    │   └── search.html
    └── share_layout/
        ├── adsense.html
        ├── base.html
        ├── base_account.html
        ├── footer.html
        ├── nav.html
        ├── nav_node.html
        └── nav_node_mobile.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/
.github/
# Frontend build artifacts (will be built in Docker)
frontend/node_modules/
frontend/dist/
blog/static/blog/dist/
# Development files
.git/
.gitignore
.vscode/
.idea/
*.pyc
__pycache__/
*.log
.env


================================================
FILE: .gitattributes
================================================
* text=auto
*.sh text eol=lf
*.conf text eol=lf
*.mo binary
*.po text eol=lf

================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->

**我确定我已经查看了** (标注`[ ]`为`[x]`)

- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)

----

**我要申请**  (标注`[ ]`为`[x]`)

- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  # Python 依赖管理
  - package-ecosystem: "pip"
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 3  # 从 5 减少到 3
    # 将依赖更新分组,减少 PR 数量
    groups:
      production-dependencies:
        dependency-type: "production"
        update-types:
          - "minor"
          - "patch"
      development-dependencies:
        dependency-type: "development"
        update-types:
          - "minor"
          - "patch"
    labels:
      - "dependencies"
      - "python"
    commit-message:
      prefix: "chore"
      prefix-development: "chore"
      include: "scope"

  # 前端 npm 依赖管理
  - package-ecosystem: "npm"
    directory: "/frontend"
    target-branch: "dev"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 3  # 从 5 减少到 3
    # 将依赖更新分组
    groups:
      frontend-production:
        dependency-type: "production"
        update-types:
          - "minor"
          - "patch"
      frontend-development:
        dependency-type: "development"
        update-types:
          - "minor"
          - "patch"
    labels:
      - "dependencies"
      - "frontend"
    commit-message:
      prefix: "chore"
      prefix-development: "chore"
      include: "scope"
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major"]

  # GitHub Actions 依赖管理
  - package-ecosystem: "github-actions"
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 3  # 从 5 减少到 3
    # 将 GitHub Actions 更新分组
    groups:
      github-actions:
        patterns:
          - "*"
        update-types:
          - "minor"
          - "patch"
    labels:
      - "dependencies"
      - "ci"
    commit-message:
      prefix: "ci"
      include: "scope"


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "CodeQL"

on:
  push:
    branches:
      - master
      - dev
    paths-ignore:
      - '**/*.md'
      - '**/*.css'
      - '**/*.js'
      - '**/*.yml'
      - '**/*.txt'
  pull_request:
    branches:
      - master
      - dev
    paths-ignore:
      - '**/*.md'
      - '**/*.css'
      - '**/*.js'
      - '**/*.yml'
      - '**/*.txt'
  schedule:
    - cron: '30 1 * * 0'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  CodeQL-Build:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v4
        with:
          languages: python
          
      - name: Autobuild
        uses: github/codeql-action/autobuild@v4

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v4

================================================
FILE: .github/workflows/deploy-master.yml
================================================
name: 自动部署到生产环境

on:
  workflow_run:
    workflows: ["Django CI", "Frontend 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: '跳过测试直接部署 (包括Django和Frontend CI)'
        required: false
        default: false
        type: boolean

concurrency:
  group: deploy-${{ github.event.workflow_run.head_sha || github.sha }}
  cancel-in-progress: true

env:
  REGISTRY: registry.cn-shenzhen.aliyuncs.com
  IMAGE_NAME: liangliangyy/djangoblog
  NAMESPACE: djangoblog

jobs:
  # Job 1: 检查 CI 状态(自动触发时)
  check-ci:
    name: 检查 CI 状态
    runs-on: ubuntu-latest
    # 只在 workflow_run 触发且不跳过测试时运行
    if: ${{ github.event_name == 'workflow_run' }}
    outputs:
      ci_passed: ${{ steps.check.outputs.ci_passed }}

    steps:
    - name: 检查所有 CI 是否完成
      id: check
      env:
        GH_TOKEN: ${{ github.token }}
      run: |
        echo "🔍 检查 CI 状态 (触发者: ${{ github.event.workflow_run.name }})"
        COMMIT_SHA="${{ github.event.workflow_run.head_sha }}"

        # 检查 Django CI(必须完成且成功)
        DJANGO_STATUS=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=$COMMIT_SHA&event=push&status=completed" \
          | jq -r '.workflow_runs[] | select(.name == "Django CI") | .conclusion' | head -1)

        # 检查 Frontend CI 是否在运行
        FRONTEND_RUNNING=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=$COMMIT_SHA&event=push&status=in_progress" \
          | jq -r '.workflow_runs[] | select(.name == "Frontend CI") | .id' | head -1)

        # 检查 Frontend CI 完成状态
        FRONTEND_STATUS=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=$COMMIT_SHA&event=push&status=completed" \
          | jq -r '.workflow_runs[] | select(.name == "Frontend CI") | .conclusion' | head -1)

        # Django CI 必须完成且成功
        if [ -z "$DJANGO_STATUS" ]; then
          echo "⏸️  Django CI 未完成,等待..."
          echo "ci_passed=false" >> $GITHUB_OUTPUT
          exit 0  # 成功退出,但不触发部署
        elif [ "$DJANGO_STATUS" != "success" ]; then
          echo "❌ Django CI 失败"
          echo "ci_passed=false" >> $GITHUB_OUTPUT
          exit 1
        fi

        # Frontend CI 如果在运行,等待
        if [ -n "$FRONTEND_RUNNING" ]; then
          echo "⏸️  Frontend CI 运行中,等待..."
          echo "ci_passed=false" >> $GITHUB_OUTPUT
          exit 0
        fi

        # Frontend CI 如果运行了必须成功,未运行则跳过
        if [ -n "$FRONTEND_STATUS" ] && [ "$FRONTEND_STATUS" != "success" ]; then
          echo "❌ Frontend CI 失败"
          echo "ci_passed=false" >> $GITHUB_OUTPUT
          exit 1
        fi

        echo "✅ 所有 CI 通过"
        echo "ci_passed=true" >> $GITHUB_OUTPUT

  # Job 2: 构建和部署
  deploy:
    name: 构建镜像并部署到生产环境
    runs-on: ubuntu-latest
    # 手动触发 或 CI检查通过后触发
    needs: [check-ci]
    if: |
      always() &&
      (github.event_name == 'workflow_dispatch' ||
       (needs.check-ci.result == 'success' && needs.check-ci.outputs.ci_passed == 'true'))

    steps:
    - name: 检出代码
      uses: actions/checkout@v6

    - 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
        else
          echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
          echo "environment=production" >> $GITHUB_OUTPUT
          echo "image_tag=latest" >> $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 }}"
      
    - name: 设置Docker Buildx
      uses: docker/setup-buildx-action@v4
      
    - name: 登录私有镜像仓库
      uses: docker/login-action@v4
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ secrets.REGISTRY_USERNAME }}
        password: ${{ secrets.REGISTRY_PASSWORD }}
        
    - name: 提取镜像元数据
      id: meta
      uses: docker/metadata-action@v6
      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@v7
      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.2.5
      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

================================================
FILE: .github/workflows/django.yml
================================================
name: Django CI

on:
  push:
    branches:
      - master
      - dev
    paths-ignore:
      - '**/*.md'
      - '**/*.css'
      - '**/*.js'
  pull_request:
    branches:
      - master
      - dev
    paths-ignore:
      - '**/*.md'
      - '**/*.css'
      - '**/*.js'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  duplicate-check:
    runs-on: ubuntu-latest
    outputs:
      should_skip: ${{ steps.skip_check.outputs.should_skip }}
    steps:
      - id: skip_check
        uses: fkirc/skip-duplicate-actions@v5
        with:
          skip_after_successful_duplicate: 'false'
          paths_ignore: '["**/*.md", "docs/**"]'
          do_not_skip: '["workflow_dispatch", "schedule"]'
          concurrent_skipping: 'outdated_runs'
          cancel_others: 'true'

  test:
    needs: duplicate-check
    if: needs.duplicate-check.outputs.should_skip != 'true'
    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@v6
        
      - 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
        run: |
          echo "🚀 启动 Elasticsearch 8.6.1 容器"

          # 启动 Elasticsearch 容器
          docker run -d \
            --name elasticsearch \
            -p 9200:9200 \
            -p 9300:9300 \
            -e "discovery.type=single-node" \
            -e "xpack.security.enabled=false" \
            -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
            elasticsearch:8.6.1

          # 等待 Elasticsearch 启动
          echo "⏳ 等待 Elasticsearch 启动..."
          for i in {1..60}; do
            if curl -s http://localhost:9200 > /dev/null; then
              echo "✅ Elasticsearch 启动成功"
              curl -s http://localhost:9200

              # 安装 IK 分词器
              echo "📦 安装 IK 分词器插件..."
              docker exec elasticsearch elasticsearch-plugin install --batch \
                https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.6.1.zip

              # 重启 Elasticsearch
              echo "🔄 重启 Elasticsearch 以加载插件..."
              docker restart elasticsearch
              sleep 10

              # 等待重启完成
              for j in {1..30}; do
                if curl -s http://localhost:9200 > /dev/null; then
                  echo "✅ Elasticsearch 重启成功"
                  break
                fi
                echo "🔄 等待 Elasticsearch 重启... ($j/30)"
                sleep 2
              done

              break
            fi
            echo "🔄 等待 Elasticsearch 启动... ($i/60)"
            sleep 2
          done
          
      # Python环境设置 (Docker测试跳过)
      - name: 设置Python ${{ matrix.python-version }}
        if: steps.test-info.outputs.skip_python_setup == 'false'
        uses: actions/setup-python@v6
        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@v5
        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@v5
        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 && 'http://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 && 'http://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 && 'http://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@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: ./coverage.xml
          flags: unittests,${{ github.ref_name }}
          name: codecov-${{ github.ref_name }}-${{ steps.test-info.outputs.test_name }}
          fail_ci_if_error: false
          verbose: true
          
      - name: 上传覆盖率到Codecov (备用)
        if: matrix.coverage == true && failure()
        uses: codecov/codecov-action@v5
        with:
          file: ./coverage.xml
          flags: unittests,${{ github.ref_name }}
          name: codecov-${{ github.ref_name }}-${{ 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@v4
        
      - name: 设置Docker Buildx
        if: matrix.test-type == 'docker'
        uses: docker/setup-buildx-action@v4
        
      - name: Docker构建测试
        if: matrix.test-type == 'docker'
        uses: docker/build-push-action@v7
        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@v7
        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


================================================
FILE: .github/workflows/docker.yml
================================================
name: docker

on:
  push:
    paths-ignore:
      - '**/*.md'
      - '**/*.yml'
    branches:
      - 'master'
      - 'dev'
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Set env to docker dev tag
        if: endsWith(github.ref, '/dev')
        run: |
          echo "DOCKER_TAG=test" >> $GITHUB_ENV
      - name: Set env to docker latest tag
        if: endsWith(github.ref, '/master')
        run: |
         echo "DOCKER_TAG=latest" >> $GITHUB_ENV
      - name: Checkout
        uses: actions/checkout@v6
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
    
      - name: Login to DockerHub
        uses: docker/login-action@v4
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}
            ghcr.io/${{ github.repository_owner }}/djangoblog:${{env.DOCKER_TAG}}
          
    


================================================
FILE: .github/workflows/frontend.yml
================================================
name: Frontend CI

on:
  push:
    branches:
      - master
      - dev
    paths:
      - 'frontend/**'
      - '.github/workflows/frontend.yml'
  pull_request:
    branches:
      - master
      - dev
    paths:
      - 'frontend/**'
      - '.github/workflows/frontend.yml'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  duplicate-check:
    runs-on: ubuntu-latest
    outputs:
      should_skip: ${{ steps.skip_check.outputs.should_skip }}
    steps:
      - id: skip_check
        uses: fkirc/skip-duplicate-actions@v5
        with:
          skip_after_successful_duplicate: 'false'
          paths_ignore: '["**/*.md", "**/*.txt", "docs/**"]'
          do_not_skip: '["workflow_dispatch", "schedule"]'
          concurrent_skipping: 'outdated_runs'
          cancel_others: 'true'

  build-and-validate:
    needs: duplicate-check
    if: needs.duplicate-check.outputs.should_skip != 'true'
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18, 20]

    name: Frontend Build & Validation (Node ${{ matrix.node-version }})

    steps:
      - name: Checkout代码
        uses: actions/checkout@v6

      - name: 设置Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          cache-dependency-path: 'frontend/package-lock.json'

      # 缓存node_modules
      - name: 缓存Node模块
        uses: actions/cache@v5
        with:
          path: |
            frontend/node_modules
            ~/.npm
          key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('frontend/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-${{ matrix.node-version }}-
            ${{ runner.os }}-node-

      - name: 安装依赖
        working-directory: ./frontend
        run: |
          echo "📦 安装前端依赖 (Node ${{ matrix.node-version }})"
          npm ci

          # 验证关键依赖
          echo "🔍 验证关键依赖"
          npm list alpinejs
          npm list htmx.org
          npm list tailwindcss
          npm list vite

      - name: 构建前端资源
        working-directory: ./frontend
        run: |
          echo "🔨 构建前端资源"
          npm run build

          # 验证构建产物
          echo "✅ 验证构建产物"
          ls -lh ../blog/static/blog/dist/

          # 检查关键文件是否存在
          if [ ! -f "../blog/static/blog/dist/.vite/manifest.json" ]; then
            echo "❌ manifest.json 不存在"
            exit 1
          fi

          # 检查是否有CSS文件
          css_files=$(find ../blog/static/blog/dist/css -name "*.css" | wc -l)
          if [ "$css_files" -eq 0 ]; then
            echo "❌ 没有找到CSS文件"
            exit 1
          fi
          echo "✅ 找到 $css_files 个CSS文件"

          # 检查是否有JS文件
          js_files=$(find ../blog/static/blog/dist/js -name "*.js" | wc -l)
          if [ "$js_files" -eq 0 ]; then
            echo "❌ 没有找到JS文件"
            exit 1
          fi
          echo "✅ 找到 $js_files 个JS文件"

      - name: 检查文件大小
        working-directory: ./frontend
        run: |
          echo "📊 检查构建产物大小"

          # 获取CSS文件大小
          css_file=$(find ../blog/static/blog/dist/css -name "main-*.css" | head -1)
          if [ -f "$css_file" ]; then
            css_size=$(stat -c%s "$css_file")
            css_size_kb=$((css_size / 1024))
            echo "   CSS大小: ${css_size_kb}KB"

            # CSS文件大小警告阈值 (200KB)
            if [ "$css_size_kb" -gt 200 ]; then
              echo "⚠️  警告: CSS文件较大 (${css_size_kb}KB)"
            fi
          fi

          # 获取JS文件总大小
          js_total_size=$(find ../blog/static/blog/dist/js -name "*.js" -exec stat -c%s {} \; | awk '{sum+=$1} END {print sum}')
          js_total_kb=$((js_total_size / 1024))
          echo "   JS总大小: ${js_total_kb}KB"

          # JS文件大小警告阈值 (200KB)
          if [ "$js_total_kb" -gt 200 ]; then
            echo "⚠️  警告: JS文件较大 (${js_total_kb}KB)"
          fi

      - name: 验证CSS语法
        working-directory: ./frontend
        run: |
          echo "🎨 验证CSS文件"

          # 检查CSS文件是否包含关键选择器
          css_file=$(find ../blog/static/blog/dist/css -name "main-*.css" | head -1)

          if [ -f "$css_file" ]; then
            echo "✅ 检查CSS关键选择器"

            # 检查深色模式支持
            if grep -q "data-theme=dark" "$css_file"; then
              echo "   ✅ 包含深色模式支持"
            else
              echo "   ❌ 缺少深色模式支持"
              exit 1
            fi

            # 检查Tailwind基础类
            if grep -q "@tailwind" "$css_file" || grep -q "tailwind" "$css_file" || grep -q ".container" "$css_file"; then
              echo "   ✅ 包含Tailwind CSS"
            else
              echo "   ⚠️  可能缺少Tailwind CSS"
            fi

            # 检查代码块样式
            if grep -q "codehilite" "$css_file" || grep -q "code" "$css_file"; then
              echo "   ✅ 包含代码块样式"
            else
              echo "   ⚠️  可能缺少代码块样式"
            fi
          fi

      - name: 验证JS语法
        working-directory: ./frontend
        run: |
          echo "🔍 验证JS文件"

          # 检查主JS文件
          main_js=$(find ../blog/static/blog/dist/js -name "main-*.js" | head -1)

          if [ -f "$main_js" ]; then
            echo "✅ 检查JS模块"

            # 检查Alpine.js
            if grep -q "Alpine" "$main_js" || [ -f "$(find ../blog/static/blog/dist/js -name "alpine-*.js" | head -1)" ]; then
              echo "   ✅ 包含Alpine.js"
            else
              echo "   ❌ 缺少Alpine.js"
              exit 1
            fi

            # 检查HTMX
            if grep -q "htmx" "$main_js" || [ -f "$(find ../blog/static/blog/dist/js -name "htmx-*.js" | head -1)" ]; then
              echo "   ✅ 包含HTMX"
            else
              echo "   ❌ 缺少HTMX"
              exit 1
            fi
          fi

      - name: 检查manifest.json
        working-directory: ./frontend
        run: |
          echo "📋 验证Vite manifest"

          manifest="../blog/static/blog/dist/.vite/manifest.json"
          if [ -f "$manifest" ]; then
            echo "✅ manifest.json 存在"

            # 验证JSON格式
            if jq empty "$manifest" 2>/dev/null; then
              echo "   ✅ JSON格式正确"

              # 显示入口点
              echo "   📝 入口点:"
              jq -r 'keys[]' "$manifest" | sed 's/^/      - /'
            else
              echo "   ❌ JSON格式错误"
              exit 1
            fi
          else
            echo "❌ manifest.json 不存在"
            exit 1
          fi

      - name: 构建统计
        if: always()
        working-directory: ./frontend
        run: |
          echo "📊 ============ 构建统计 ============"
          echo "   🏷️  Node版本: ${{ matrix.node-version }}"
          echo "   📦 构建状态: ${{ job.status }}"

          # 文件统计
          if [ -d "../blog/static/blog/dist/" ]; then
            total_size=$(du -sh ../blog/static/blog/dist/ | cut -f1)
            file_count=$(find ../blog/static/blog/dist/ -type f | wc -l)
            echo "   📁 总文件数: $file_count"
            echo "   💾 总大小: $total_size"
          fi

          echo "   📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
          echo "======================================"

          if [ "${{ job.status }}" = "success" ]; then
            echo "🎉 前端构建成功!"
          else
            echo "❌ 前端构建失败,请检查上面的日志"
          fi


================================================
FILE: .github/workflows/publish-release.yml
================================================
name: publish release

on:
  release:
    types: [ published ]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: name/app
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Login to DockerHub
        uses: docker/login-action@v4
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          platforms: |
            linux/amd64
            linux/arm64
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover

# Translations
*.pot

# Django stuff:
*.log
logs/

# Sphinx documentation
docs/_build/

# PyBuilder
target/


# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
# virtualenv
venv/

collectedstatic/
djangoblog/whoosh_index/
whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/
.claude/
nul
deploy/docker-compose/bin/datas
static/avatar
img/
.specify/
specs/

================================================
FILE: Dockerfile
================================================
# Stage 1: Build frontend assets
FROM node:20-alpine AS frontend-builder

WORKDIR /app

# Copy frontend package files
COPY frontend/package*.json ./frontend/

# Set npm registry to official registry and install dependencies (including devDependencies for build)
RUN cd frontend && \
    npm config set registry https://registry.npmjs.org/ && \
    npm ci

# Copy frontend source files
COPY frontend/ ./frontend/

# Copy templates for Tailwind CSS content scanning
COPY templates/ ./templates/

# Build frontend assets (output goes to ../blog/static/blog/dist)
# Vite will create the output directory structure automatically
RUN cd frontend && npm run build

# Stage 2: Build final image
FROM python:3.11

ENV PYTHONUNBUFFERED=1
WORKDIR /code/djangoblog/

# Install system dependencies
RUN apt-get update && \
    apt-get install default-libmysqlclient-dev gettext -y && \
    rm -rf /var/lib/apt/lists/*

# Copy and install Python dependencies
COPY requirements.txt requirements.txt
RUN pip install --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt && \
    pip install --no-cache-dir gunicorn[gevent] && \
    pip cache purge

# Copy application code (excluding old build artifacts)
COPY . .

# Remove any old build artifacts that might have been copied
RUN rm -rf /code/djangoblog/blog/static/blog/dist

# Copy built frontend assets from frontend-builder stage
COPY --from=frontend-builder /app/blog/static/blog/dist /code/djangoblog/blog/static/blog/dist

# Verify the frontend assets were copied correctly
RUN ls -la /code/djangoblog/blog/static/blog/dist/css/ && \
    cat /code/djangoblog/blog/static/blog/dist/.vite/manifest.json

# Set execute permission for entrypoint
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh

ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2025 车亮亮

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================
FILE: README.md
================================================
# DjangoBlog

<p align="center">
  <a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
  <a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/frontend.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/frontend.yml/badge.svg" alt="Frontend CI"></a>
  <a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
 <a href="https://codecov.io/gh/liangliangyy/DjangoBlog" > 
 <img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg?token=vmnMtzuFqN"/></a> <a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>

<p align="center">
  <b>一款功能强大、设计优雅的现代化博客系统</b>
  <br>
  <a href="/docs/README-en.md">English</a> • <b>简体中文</b>
</p>

---

DjangoBlog 是一款基于 Python 3.10+ 和 Django 5.2 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能,还通过一个灵活的插件系统,让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者,DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。

## ✨ 特性亮点

- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成 Elasticsearch/Whoosh 搜索引擎,提供快速、精准的文章内容搜索,支持关键词高亮显示。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。现代化评论界面,支持无限嵌套回复。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **黑夜模式**: 支持浅色/深色主题自动切换,可跟随系统设置,提供舒适的阅读体验。
- **现代化前端**: 基于 Alpine.js + Tailwind CSS + HTMX 构建,提供 SPA 般的无刷新浏览体验,支持 HTML-over-the-wire 架构。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能,代码解耦,易于维护。已内置 9 个实用插件,包括浏览计数、SEO 优化、文章推荐、图片懒加载等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化构建**: 使用 Vite 构建前端资源,支持热更新和自动压缩优化。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。

## 🛠️ 技术栈

- **后端**: Python 3.10+, Django 5.2
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis, LocalMem (可配置)
- **前端**: Alpine.js 3.13, Tailwind CSS 3.4, HTMX 1.9, Vite 5.4
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)

## 🚀 快速开始

### 1. 环境准备

确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。

### 2. 克隆与安装

```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog

# 安装依赖
pip install -r requirements.txt
```

### 3. 项目配置

- **数据库**:
  打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。

  ```python
  DATABASES = {
      'default': {
          'ENGINE': 'django.db.backends.mysql',
          'NAME': 'djangoblog',
          'USER': 'root',
          'PASSWORD': 'your_password',
          'HOST': '127.0.0.1',
          'PORT': 3306,
      }
  }
  ```
  在 MySQL 中创建数据库:
  ```sql
  CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  ```

- **更多配置**:
  关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。

### 4. 初始化数据库

```bash
python manage.py makemigrations
python manage.py migrate

# 创建一个超级管理员账户
python manage.py createsuperuser
```

### 5. 构建前端资源

```bash
# 进入前端目录
cd frontend

# 安装依赖(首次运行需要)
npm install

# 构建生产环境资源
npm run build

# 返回项目根目录
cd ..
```

### 6. 运行项目

```bash
# (可选) 生成一些测试数据
python manage.py create_testdata

# 收集静态文件
python manage.py collectstatic --noinput

# (可选) 压缩静态文件
python manage.py compress --force

# 启动开发服务器
python manage.py runserver
```

现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!

### 开发模式

如果您需要开发前端代码,可以使用 Vite 的热更新功能:

```bash
# 在 frontend 目录下启动开发服务器
cd frontend
npm run dev
```

这将启动 Vite 开发服务器,修改前端代码后会自动重新构建。

## 部署

- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术,请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。

## 🧩 插件系统

插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。

- **工作原理**: 插件通过在预定义的"钩子"上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。

- **现有插件**: 项目已内置以下实用插件
  - `view_count` - 文章浏览计数统计
  - `seo_optimizer` - SEO 优化增强
  - `article_copyright` - 文章版权声明(现代化样式)
  - `article_recommendation` - 智能文章推荐(响应式卡片布局)
  - `external_links` - 外部链接处理(自动添加图标)
  - `image_lazy_loading` - 图片懒加载优化(淡入动画)
  - `reading_time` - 文章阅读时间估算
  - `cloudflare_cache` - Cloudflare 缓存管理

- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!

## 🤝 贡献指南

我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Issue 或 Pull Request。

## 📄 许可证

本项目基于 [MIT License](LICENSE) 开源。

---

## ❤️ 支持与赞助

如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。

<p align="center">
  <img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
  <img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
  <i>(左) 支付宝 / (右) 微信</i>
</p>

## 🙏 鸣谢

特别感谢 **JetBrains** 为本项目提供的免费开源许可证。

<p align="center">
  <a href="https://www.jetbrains.com/?from=DjangoBlog">
    <img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
  </a>
</p>

---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。


================================================
FILE: accounts/__init__.py
================================================


================================================
FILE: accounts/admin.py
================================================
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _

# Register your models here.
from .models import BlogUser


class BlogUserCreationForm(forms.ModelForm):
    password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
    password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)

    class Meta:
        model = BlogUser
        fields = ('email',)

    def clean_password2(self):
        # Check that the two password entries match
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError(_("passwords do not match"))
        return password2

    def save(self, commit=True):
        # Save the provided password in hashed format
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.source = 'adminsite'
            user.save()
        return user


class BlogUserChangeForm(UserChangeForm):
    class Meta:
        model = BlogUser
        fields = '__all__'
        field_classes = {'username': UsernameField}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


class BlogUserAdmin(UserAdmin):
    form = BlogUserChangeForm
    add_form = BlogUserCreationForm
    list_display = (
        'id',
        'nickname',
        'username',
        'email',
        'last_login',
        'date_joined',
        'source')
    list_display_links = ('id', 'username')
    ordering = ('-id',)
    search_fields = ('username', 'nickname', 'email')


================================================
FILE: accounts/apps.py
================================================
from django.apps import AppConfig


class AccountsConfig(AppConfig):
    name = 'accounts'


================================================
FILE: accounts/forms.py
================================================
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser


class LoginForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        self.fields['username'].widget = widgets.TextInput(
            attrs={'placeholder': "username", "class": "form-control"})
        self.fields['password'].widget = widgets.PasswordInput(
            attrs={'placeholder': "password", "class": "form-control"})


class RegisterForm(UserCreationForm):
    def __init__(self, *args, **kwargs):
        super(RegisterForm, self).__init__(*args, **kwargs)

        self.fields['username'].widget = widgets.TextInput(
            attrs={'placeholder': "username", "class": "form-control"})
        self.fields['email'].widget = widgets.EmailInput(
            attrs={'placeholder': "email", "class": "form-control"})
        self.fields['password1'].widget = widgets.PasswordInput(
            attrs={'placeholder': "password", "class": "form-control"})
        self.fields['password2'].widget = widgets.PasswordInput(
            attrs={'placeholder': "repeat password", "class": "form-control"})

    def clean_email(self):
        email = self.cleaned_data['email']
        if get_user_model().objects.filter(email=email).exists():
            raise ValidationError(_("email already exists"))
        return email

    class Meta:
        model = get_user_model()
        fields = ("username", "email")


class ForgetPasswordForm(forms.Form):
    new_password1 = forms.CharField(
        label=_("New password"),
        widget=forms.PasswordInput(
            attrs={
                "class": "form-control",
                'placeholder': _("New password")
            }
        ),
    )

    new_password2 = forms.CharField(
        label="确认密码",
        widget=forms.PasswordInput(
            attrs={
                "class": "form-control",
                'placeholder': _("Confirm password")
            }
        ),
    )

    email = forms.EmailField(
        label='邮箱',
        widget=forms.TextInput(
            attrs={
                'class': 'form-control',
                'placeholder': _("Email")
            }
        ),
    )

    code = forms.CharField(
        label=_('Code'),
        widget=forms.TextInput(
            attrs={
                'class': 'form-control',
                'placeholder': _("Code")
            }
        ),
    )

    def clean_new_password2(self):
        password1 = self.data.get("new_password1")
        password2 = self.data.get("new_password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError(_("passwords do not match"))
        password_validation.validate_password(password2)

        return password2

    def clean_email(self):
        user_email = self.cleaned_data.get("email")
        if not BlogUser.objects.filter(
                email=user_email
        ).exists():
            # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
            raise ValidationError(_("email does not exist"))
        return user_email

    def clean_code(self):
        code = self.cleaned_data.get("code")
        error = utils.verify(
            email=self.cleaned_data.get("email"),
            code=code,
        )
        if error:
            raise ValidationError(error)
        return code


class ForgetPasswordCodeForm(forms.Form):
    email = forms.EmailField(
        label=_('Email'),
    )


================================================
FILE: accounts/migrations/0001_initial.py
================================================
# Generated by Django 4.1.7 on 2023-03-02 07:14

import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='BlogUser',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
                ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
                ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
                ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': '用户',
                'verbose_name_plural': '用户',
                'ordering': ['-id'],
                'get_latest_by': 'id',
            },
            managers=[
                ('objects', django.contrib.auth.models.UserManager()),
            ],
        ),
    ]


================================================
FILE: accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
================================================
# Generated by Django 4.2.5 on 2023-09-06 13:13

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='bloguser',
            options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
        ),
        migrations.RemoveField(
            model_name='bloguser',
            name='created_time',
        ),
        migrations.RemoveField(
            model_name='bloguser',
            name='last_mod_time',
        ),
        migrations.AddField(
            model_name='bloguser',
            name='creation_time',
            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
        ),
        migrations.AddField(
            model_name='bloguser',
            name='last_modify_time',
            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
        ),
        migrations.AlterField(
            model_name='bloguser',
            name='nickname',
            field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
        ),
        migrations.AlterField(
            model_name='bloguser',
            name='source',
            field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
        ),
    ]


================================================
FILE: accounts/migrations/__init__.py
================================================


================================================
FILE: accounts/models.py
================================================
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site


# Create your models here.

class BlogUser(AbstractUser):
    nickname = models.CharField(_('nick name'), max_length=100, blank=True)
    creation_time = models.DateTimeField(_('creation time'), default=now)
    last_modify_time = models.DateTimeField(_('last modify time'), default=now)
    source = models.CharField(_('create source'), max_length=100, blank=True)

    def get_absolute_url(self):
        return reverse(
            'blog:author_detail', kwargs={
                'author_name': self.username})

    def __str__(self):
        return self.email

    def get_full_url(self):
        site = get_current_site().domain
        url = "https://{site}{path}".format(site=site,
                                            path=self.get_absolute_url())
        return url

    class Meta:
        ordering = ['-id']
        verbose_name = _('user')
        verbose_name_plural = verbose_name
        get_latest_by = 'id'


================================================
FILE: accounts/templatetags/__init__.py
================================================


================================================
FILE: accounts/test_admin.py
================================================
"""
Accounts Admin 测试
测试用户管理后台的各项功能
"""
from django.contrib.admin.sites import AdminSite
from django.test import TestCase, RequestFactory
from django.urls import reverse

from accounts.admin import BlogUserAdmin
from accounts.models import BlogUser
from djangoblog.test_base import BaseTestCase, AdminTestMixin


class BlogUserAdminTest(BaseTestCase, AdminTestMixin):
    """测试 BlogUser Admin"""

    def setUp(self):
        super().setUp()
        self.site = AdminSite()
        self.blog_user_admin = BlogUserAdmin(BlogUser, self.site)

    def test_admin_list_display(self):
        """测试列表显示字段"""
        self.login_admin()
        response = self.assert_admin_accessible(BlogUser)
        self.assertContains(response, self.user.username)
        self.assertContains(response, self.user.email)

    def test_admin_search(self):
        """测试搜索功能"""
        self.login_admin()
        url = self.get_admin_url(BlogUser)
        response = self.client.get(url, {'q': self.user.username})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, self.user.username)

    def test_admin_filter_by_is_staff(self):
        """测试按员工状态过滤"""
        staff_user = self.create_staff_user()
        self.login_admin()
        url = self.get_admin_url(BlogUser)
        response = self.client.get(url, {'is_staff__exact': '1'})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, staff_user.username)

    def test_admin_change_user(self):
        """测试修改用户"""
        self.login_admin()
        url = self.get_admin_change_url(self.user)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        # 修改用户信息
        response = self.client.post(url, {
            'username': self.user.username,
            'email': 'newemail@test.com',
            'date_joined_0': self.user.date_joined.strftime('%Y-%m-%d'),
            'date_joined_1': self.user.date_joined.strftime('%H:%M:%S'),
        })
        self.user.refresh_from_db()

    def test_admin_requires_login(self):
        """测试需要登录才能访问"""
        self.client.logout()
        self.assert_admin_forbidden_for_user(BlogUser)

    def test_admin_forbidden_for_normal_user(self):
        """测试普通用户无法访问"""
        self.login_user()
        self.assert_admin_forbidden_for_user(BlogUser)

    def test_get_list_filter(self):
        """测试获取列表过滤器"""
        request = self.factory.get('/')
        request.user = self.admin_user
        filters = self.blog_user_admin.get_list_filter(request)
        self.assertIn('is_staff', filters)
        self.assertIn('is_superuser', filters)

    def test_get_readonly_fields_for_superuser(self):
        """测试超级管理员看到的只读字段"""
        request = self.factory.get('/')
        request.user = self.admin_user
        readonly_fields = self.blog_user_admin.get_readonly_fields(request, self.user)
        self.assertIsInstance(readonly_fields, (list, tuple))

    def test_get_readonly_fields_for_staff(self):
        """测试员工用户看到的只读字段"""
        staff = self.create_staff_user()
        request = self.factory.get('/')
        request.user = staff
        readonly_fields = self.blog_user_admin.get_readonly_fields(request, self.user)
        # 员工用户应该看到更多只读字段
        self.assertIsInstance(readonly_fields, (list, tuple))


================================================
FILE: accounts/test_user_business_logic.py
================================================
"""
Test cases for user authentication business logic
包括用户注册、登录、密码管理、权限等核心业务逻辑
"""
from django.test import TestCase, Client
from django.contrib.auth import authenticate
from django.utils import timezone

from accounts.models import BlogUser


class UserRegistrationTest(TestCase):
    """测试用户注册业务逻辑"""

    def test_user_can_be_created(self):
        """测试用户可以被创建"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword123'
        )

        self.assertIsNotNone(user.id)
        self.assertEqual(user.username, 'testuser')
        self.assertEqual(user.email, 'test@example.com')

    def test_user_password_is_hashed(self):
        """测试用户密码被哈希存储"""
        password = 'testpassword123'
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password=password
        )

        # 密码不应该以明文存储
        self.assertNotEqual(user.password, password)
        # 密码应该被哈希
        self.assertTrue(user.password.startswith('pbkdf2_'))

    def test_user_can_check_password(self):
        """测试用户可以验证密码"""
        password = 'testpassword123'
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password=password
        )

        # 正确的密码应该通过验证
        self.assertTrue(user.check_password(password))
        # 错误的密码应该不通过验证
        self.assertFalse(user.check_password('wrongpassword'))

    def test_username_must_be_unique(self):
        """测试用户名必须唯一"""
        BlogUser.objects.create_user(
            username='testuser',
            email='test1@example.com',
            password='password'
        )

        # 尝试创建相同用户名的用户应该失败
        with self.assertRaises(Exception):
            BlogUser.objects.create_user(
                username='testuser',
                email='test2@example.com',
                password='password'
            )

    def test_email_is_stored_correctly(self):
        """测试邮箱正确存储"""
        email = 'test@example.com'
        user = BlogUser.objects.create_user(
            username='testuser',
            email=email,
            password='password'
        )

        self.assertEqual(user.email, email)

    def test_user_is_active_by_default(self):
        """测试用户默认是激活状态"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.assertTrue(user.is_active)

    def test_user_is_not_staff_by_default(self):
        """测试用户默认不是staff"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.assertFalse(user.is_staff)

    def test_user_is_not_superuser_by_default(self):
        """测试用户默认不是超级用户"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.assertFalse(user.is_superuser)


class UserAuthenticationTest(TestCase):
    """测试用户认证业务逻辑"""

    def setUp(self):
        """设置测试环境"""
        self.username = 'testuser'
        self.email = 'test@example.com'
        self.password = 'testpassword123'

        self.user = BlogUser.objects.create_user(
            username=self.username,
            email=self.email,
            password=self.password
        )

    def test_user_can_authenticate_with_correct_credentials(self):
        """测试用户可以用正确的凭据认证"""
        user = authenticate(username=self.username, password=self.password)
        self.assertIsNotNone(user)
        self.assertEqual(user, self.user)

    def test_user_cannot_authenticate_with_wrong_password(self):
        """测试用户不能用错误的密码认证"""
        user = authenticate(username=self.username, password='wrongpassword')
        self.assertIsNone(user)

    def test_user_cannot_authenticate_with_wrong_username(self):
        """测试用户不能用错误的用户名认证"""
        user = authenticate(username='wronguser', password=self.password)
        self.assertIsNone(user)

    def test_inactive_user_cannot_authenticate(self):
        """测试未激活的用户不能认证"""
        self.user.is_active = False
        self.user.save()

        user = authenticate(username=self.username, password=self.password)
        # 注意:Django的authenticate()方法会返回用户,但is_active=False
        # 实际的登录阻止发生在login()时
        # 这里我们测试用户的is_active状态
        if user:
            self.assertFalse(user.is_active)

    def test_active_user_can_authenticate(self):
        """测试激活的用户可以认证"""
        self.user.is_active = True
        self.user.save()

        user = authenticate(username=self.username, password=self.password)
        self.assertIsNotNone(user)


class UserPasswordManagementTest(TestCase):
    """测试用户密码管理业务逻辑"""

    def setUp(self):
        """设置测试环境"""
        self.user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='oldpassword123'
        )

    def test_user_can_change_password(self):
        """测试用户可以修改密码"""
        old_password = 'oldpassword123'
        new_password = 'newpassword456'

        # 验证旧密码
        self.assertTrue(self.user.check_password(old_password))

        # 修改密码
        self.user.set_password(new_password)
        self.user.save()

        # 验证新密码
        self.user.refresh_from_db()
        self.assertTrue(self.user.check_password(new_password))
        self.assertFalse(self.user.check_password(old_password))

    def test_password_change_requires_save(self):
        """测试密码修改需要保存"""
        new_password = 'newpassword456'
        old_password_hash = self.user.password

        # 只设置密码,不保存
        self.user.set_password(new_password)

        # 从数据库重新加载
        user_from_db = BlogUser.objects.get(id=self.user.id)

        # 数据库中的密码应该还是旧的
        self.assertEqual(user_from_db.password, old_password_hash)

    def test_set_unusable_password(self):
        """测试设置不可用的密码"""
        self.user.set_unusable_password()
        self.user.save()

        # 用户应该无法用任何密码认证
        self.assertFalse(self.user.has_usable_password())


class UserPermissionTest(TestCase):
    """测试用户权限业务逻辑"""

    def setUp(self):
        """设置测试环境"""
        self.normal_user = BlogUser.objects.create_user(
            username='normaluser',
            email='normal@example.com',
            password='password'
        )

        self.staff_user = BlogUser.objects.create_user(
            username='staffuser',
            email='staff@example.com',
            password='password',
            is_staff=True
        )

        self.superuser = BlogUser.objects.create_superuser(
            username='superuser',
            email='super@example.com',
            password='password'
        )

    def test_normal_user_has_no_special_privileges(self):
        """测试普通用户没有特殊权限"""
        self.assertFalse(self.normal_user.is_staff)
        self.assertFalse(self.normal_user.is_superuser)

    def test_staff_user_is_staff(self):
        """测试staff用户有staff权限"""
        self.assertTrue(self.staff_user.is_staff)
        # staff用户不一定是超级用户
        self.assertFalse(self.staff_user.is_superuser)

    def test_superuser_has_all_privileges(self):
        """测试超级用户有所有权限"""
        self.assertTrue(self.superuser.is_staff)
        self.assertTrue(self.superuser.is_superuser)
        self.assertTrue(self.superuser.is_active)

    def test_create_superuser_method(self):
        """测试创建超级用户的方法"""
        superuser = BlogUser.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='adminpassword'
        )

        self.assertTrue(superuser.is_staff)
        self.assertTrue(superuser.is_superuser)

    def test_user_can_be_promoted_to_staff(self):
        """测试用户可以提升为staff"""
        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password'
        )

        self.assertFalse(user.is_staff)

        # 提升为staff
        user.is_staff = True
        user.save()

        user.refresh_from_db()
        self.assertTrue(user.is_staff)

    def test_user_can_be_promoted_to_superuser(self):
        """测试用户可以提升为超级用户"""
        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password'
        )

        self.assertFalse(user.is_superuser)

        # 提升为超级用户
        user.is_superuser = True
        user.save()

        user.refresh_from_db()
        self.assertTrue(user.is_superuser)


class UserActivationTest(TestCase):
    """测试用户激活业务逻辑"""

    def test_user_can_be_deactivated(self):
        """测试用户可以被停用"""
        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password'
        )

        self.assertTrue(user.is_active)

        # 停用用户
        user.is_active = False
        user.save()

        user.refresh_from_db()
        self.assertFalse(user.is_active)

    def test_user_can_be_reactivated(self):
        """测试用户可以被重新激活"""
        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password',
            is_active=False
        )

        self.assertFalse(user.is_active)

        # 重新激活用户
        user.is_active = True
        user.save()

        user.refresh_from_db()
        self.assertTrue(user.is_active)


class UserProfileTest(TestCase):
    """测试用户资料业务逻辑"""

    def test_user_has_username(self):
        """测试用户有用户名"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.assertEqual(user.username, 'testuser')

    def test_user_has_email(self):
        """测试用户有邮箱"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.assertEqual(user.email, 'test@example.com')

    def test_user_can_update_email(self):
        """测试用户可以更新邮箱"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='old@example.com',
            password='password'
        )

        new_email = 'new@example.com'
        user.email = new_email
        user.save()

        user.refresh_from_db()
        self.assertEqual(user.email, new_email)

    def test_user_string_representation(self):
        """测试用户字符串表示"""
        user = BlogUser.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='password'
        )

        # __str__ 方法应该返回用户名或有意义的字符串
        user_str = str(user)
        self.assertIsInstance(user_str, str)
        self.assertTrue(len(user_str) > 0)


class UserQueryTest(TestCase):
    """测试用户查询业务逻辑"""

    def setUp(self):
        """设置测试环境"""
        # 创建多个用户
        self.users = []
        for i in range(5):
            user = BlogUser.objects.create_user(
                username=f'user{i}',
                email=f'user{i}@example.com',
                password='password'
            )
            self.users.append(user)

    def test_query_user_by_username(self):
        """测试按用户名查询用户"""
        user = BlogUser.objects.get(username='user0')
        self.assertEqual(user, self.users[0])

    def test_query_user_by_email(self):
        """测试按邮箱查询用户"""
        user = BlogUser.objects.get(email='user1@example.com')
        self.assertEqual(user, self.users[1])

    def test_query_active_users(self):
        """测试查询激活的用户"""
        # 停用一些用户
        self.users[0].is_active = False
        self.users[0].save()
        self.users[1].is_active = False
        self.users[1].save()

        # 查询激活的用户
        active_users = BlogUser.objects.filter(is_active=True)
        self.assertEqual(active_users.count(), 3)

    def test_query_staff_users(self):
        """测试查询staff用户"""
        # 提升一些用户为staff
        self.users[0].is_staff = True
        self.users[0].save()
        self.users[1].is_staff = True
        self.users[1].save()

        # 查询staff用户
        staff_users = BlogUser.objects.filter(is_staff=True)
        self.assertEqual(staff_users.count(), 2)

    def test_query_superusers(self):
        """测试查询超级用户"""
        # 创建超级用户
        BlogUser.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='password'
        )

        # 查询超级用户
        superusers = BlogUser.objects.filter(is_superuser=True)
        self.assertEqual(superusers.count(), 1)


class UserDeletionTest(TestCase):
    """测试用户删除业务逻辑"""

    def test_user_can_be_deleted(self):
        """测试用户可以被删除"""
        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password'
        )

        user_id = user.id

        # 删除用户
        user.delete()

        # 验证用户已被删除
        with self.assertRaises(BlogUser.DoesNotExist):
            BlogUser.objects.get(id=user_id)

    def test_delete_user_cascade_effects(self):
        """测试删除用户的级联效果"""
        from blog.models import Article, Category

        user = BlogUser.objects.create_user(
            username='user',
            email='user@example.com',
            password='password'
        )

        # 创建用户的文章
        category = Category.objects.create(
            name='Category',
            slug='category'
        )

        article = Article.objects.create(
            title='User Article',
            body='Content',
            author=user,
            category=category,
            status='p',
            type='a'
        )

        article_id = article.id

        # 删除用户
        user.delete()

        # 验证文章的处理(取决于外键的on_delete设置)
        # 如果是CASCADE,文章应该被删除
        with self.assertRaises(Article.DoesNotExist):
            Article.objects.get(id=article_id)


================================================
FILE: accounts/tests.py
================================================
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils


# Create your tests here.

class AccountTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.factory = RequestFactory()
        self.blog_user = BlogUser.objects.create_user(
            username="test",
            email="admin@admin.com",
            password="12345678"
        )
        self.new_test = "xxx123--="

    def test_validate_account(self):
        site = get_current_site().domain
        user = BlogUser.objects.create_superuser(
            email="liangliangyy1@gmail.com",
            username="liangliangyy1",
            password="qwer!@#$ggg")
        testuser = BlogUser.objects.get(username='liangliangyy1')

        loginresult = self.client.login(
            username='liangliangyy1',
            password='qwer!@#$ggg')
        self.assertEqual(loginresult, True)
        response = self.client.get('/admin/')
        self.assertEqual(response.status_code, 200)

        category = Category()
        category.name = "categoryaaa"
        category.creation_time = timezone.now()
        category.last_modify_time = timezone.now()
        category.save()

        article = Article()
        article.title = "nicetitleaaa"
        article.body = "nicecontentaaa"
        article.author = user
        article.category = category
        article.type = 'a'
        article.status = 'p'
        article.save()

        response = self.client.get(article.get_admin_url())
        self.assertEqual(response.status_code, 200)

    def test_validate_register(self):
        self.assertEquals(
            0, len(
                BlogUser.objects.filter(
                    email='user123@user.com')))
        response = self.client.post(reverse('account:register'), {
            'username': 'user1233',
            'email': 'user123@user.com',
            'password1': 'password123!q@wE#R$T',
            'password2': 'password123!q@wE#R$T',
        })
        self.assertEquals(
            1, len(
                BlogUser.objects.filter(
                    email='user123@user.com')))
        user = BlogUser.objects.filter(email='user123@user.com')[0]
        sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
        path = reverse('accounts:result')
        url = '{path}?type=validation&id={id}&sign={sign}'.format(
            path=path, id=user.id, sign=sign)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.client.login(username='user1233', password='password123!q@wE#R$T')
        user = BlogUser.objects.filter(email='user123@user.com')[0]
        user.is_superuser = True
        user.is_staff = True
        user.save()
        delete_sidebar_cache()
        category = Category()
        category.name = "categoryaaa"
        category.creation_time = timezone.now()
        category.last_modify_time = timezone.now()
        category.save()

        article = Article()
        article.category = category
        article.title = "nicetitle333"
        article.body = "nicecontentttt"
        article.author = user

        article.type = 'a'
        article.status = 'p'
        article.save()

        response = self.client.get(article.get_admin_url())
        self.assertEqual(response.status_code, 200)

        response = self.client.get(reverse('account:logout'))
        self.assertIn(response.status_code, [301, 302, 200])

        response = self.client.get(article.get_admin_url())
        self.assertIn(response.status_code, [301, 302, 200])

        response = self.client.post(reverse('account:login'), {
            'username': 'user1233',
            'password': 'password123'
        })
        self.assertIn(response.status_code, [301, 302, 200])

        response = self.client.get(article.get_admin_url())
        self.assertIn(response.status_code, [301, 302, 200])

    def test_verify_email_code(self):
        to_email = "admin@admin.com"
        code = generate_code()
        utils.set_code(to_email, code)
        utils.send_verify_email(to_email, code)

        err = utils.verify("admin@admin.com", code)
        self.assertEqual(err, None)

        err = utils.verify("admin@123.com", code)
        self.assertEqual(type(err), str)

    def test_forget_password_email_code_success(self):
        resp = self.client.post(
            path=reverse("account:forget_password_code"),
            data=dict(email="admin@admin.com")
        )

        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content.decode("utf-8"), "ok")

    def test_forget_password_email_code_fail(self):
        resp = self.client.post(
            path=reverse("account:forget_password_code"),
            data=dict()
        )
        self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")

        resp = self.client.post(
            path=reverse("account:forget_password_code"),
            data=dict(email="admin@com")
        )
        self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")

    def test_forget_password_email_success(self):
        code = generate_code()
        utils.set_code(self.blog_user.email, code)
        data = dict(
            new_password1=self.new_test,
            new_password2=self.new_test,
            email=self.blog_user.email,
            code=code,
        )
        resp = self.client.post(
            path=reverse("account:forget_password"),
            data=data
        )
        self.assertEqual(resp.status_code, 302)

        # 验证用户密码是否修改成功
        blog_user = BlogUser.objects.filter(
            email=self.blog_user.email,
        ).first()  # type: BlogUser
        self.assertNotEqual(blog_user, None)
        self.assertEqual(blog_user.check_password(data["new_password1"]), True)

    def test_forget_password_email_not_user(self):
        data = dict(
            new_password1=self.new_test,
            new_password2=self.new_test,
            email="123@123.com",
            code="123456",
        )
        resp = self.client.post(
            path=reverse("account:forget_password"),
            data=data
        )

        self.assertEqual(resp.status_code, 200)


    def test_forget_password_email_code_error(self):
        code = generate_code()
        utils.set_code(self.blog_user.email, code)
        data = dict(
            new_password1=self.new_test,
            new_password2=self.new_test,
            email=self.blog_user.email,
            code="111111",
        )
        resp = self.client.post(
            path=reverse("account:forget_password"),
            data=data
        )

        self.assertEqual(resp.status_code, 200)



================================================
FILE: accounts/urls.py
================================================
from django.urls import path
from django.urls import re_path

from . import views
from .forms import LoginForm

app_name = "accounts"

urlpatterns = [re_path(r'^login/$',
                       views.LoginView.as_view(success_url='/'),
                       name='login',
                       kwargs={'authentication_form': LoginForm}),
               re_path(r'^register/$',
                       views.RegisterView.as_view(success_url="/"),
                       name='register'),
               re_path(r'^logout/$',
                       views.LogoutView.as_view(),
                       name='logout'),
               path(r'account/result.html',
                    views.account_result,
                    name='result'),
               re_path(r'^forget_password/$',
                       views.ForgetPasswordView.as_view(),
                       name='forget_password'),
               re_path(r'^forget_password_code/$',
                       views.ForgetPasswordEmailCode.as_view(),
                       name='forget_password_code'),
               ]


================================================
FILE: accounts/user_login_backend.py
================================================
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class EmailOrUsernameModelBackend(ModelBackend):
    """
    允许使用用户名或邮箱登录
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if '@' in username:
            kwargs = {'email': username}
        else:
            kwargs = {'username': username}
        try:
            user = get_user_model().objects.get(**kwargs)
            if user.check_password(password):
                return user
        except get_user_model().DoesNotExist:
            return None

    def get_user(self, username):
        try:
            return get_user_model().objects.get(pk=username)
        except get_user_model().DoesNotExist:
            return None


================================================
FILE: accounts/utils.py
================================================
import typing
from datetime import timedelta

from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _

from djangoblog.utils import send_email

_code_ttl = timedelta(minutes=5)


def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
    """发送重设密码验证码
    Args:
        to_mail: 接受邮箱
        subject: 邮件主题
        code: 验证码
    """
    html_content = _(
        "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it "
        "properly") % {'code': code}
    send_email([to_mail], subject, html_content)


def verify(email: str, code: str) -> typing.Optional[str]:
    """验证code是否有效
    Args:
        email: 请求邮箱
        code: 验证码
    Return:
        如果有错误就返回错误str
    Node:
        这里的错误处理不太合理,应该采用raise抛出
        否测调用方也需要对error进行处理
    """
    cache_code = get_code(email)
    if cache_code != code:
        return gettext("Verification code error")


def set_code(email: str, code: str):
    """设置code"""
    cache.set(email, code, _code_ttl.seconds)


def get_code(email: str) -> typing.Optional[str]:
    """获取code"""
    return cache.get(email)


================================================
FILE: accounts/views.py
================================================
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View

from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from djangoblog.base_views import SecureFormView, LoginFormView, LogoutRedirectView
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser

logger = logging.getLogger(__name__)


# Create your views here.

class RegisterView(SecureFormView):
    """
    用户注册视图(重构版)

    使用 SecureFormView 基类,自动提供 CSRF 保护
    """
    form_class = RegisterForm
    template_name = 'account/registration_form.html'

    def form_valid(self, form):
        if form.is_valid():
            user = form.save(False)
            user.is_active = False
            user.source = 'Register'
            user.save(True)
            site = get_current_site().domain
            sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))

            if settings.DEBUG:
                site = '127.0.0.1:8000'
            path = reverse('account:result')
            url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
                site=site, path=path, id=user.id, sign=sign)

            content = """
                            <p>请点击下面链接验证您的邮箱</p>

                            <a href="{url}" rel="bookmark">{url}</a>

                            再次感谢您!
                            <br />
                            如果上面链接无法打开,请将此链接复制至浏览器。
                            {url}
                            """.format(url=url)
            send_email(
                emailto=[
                    user.email,
                ],
                title='验证您的电子邮箱',
                content=content)

            url = reverse('accounts:result') + \
                  '?type=register&id=' + str(user.id)
            return HttpResponseRedirect(url)
        else:
            return self.render_to_response({
                'form': form
            })


class LogoutView(LogoutRedirectView):
    """
    用户登出视图(重构版)

    使用 LogoutRedirectView 基类,自动禁用缓存
    """
    url = '/login/'

    def get(self, request, *args, **kwargs):
        logout(request)
        delete_sidebar_cache()
        # 获取响应对象并删除登录标记 cookie
        response = super(LogoutView, self).get(request, *args, **kwargs)
        response.delete_cookie('logged_user')
        return response


class LoginView(LoginFormView):
    """
    用户登录视图(重构版)

    使用 LoginFormView 基类,自动提供:
    - 敏感参数保护(password)
    - CSRF 保护
    - 禁用缓存
    """
    form_class = LoginForm
    template_name = 'account/login.html'
    success_url = '/'
    redirect_field_name = REDIRECT_FIELD_NAME

    def get_context_data(self, **kwargs):
        redirect_to = self.request.GET.get(self.redirect_field_name)
        if redirect_to is None:
            redirect_to = '/'
        kwargs['redirect_to'] = redirect_to

        return super(LoginView, self).get_context_data(**kwargs)

    def form_valid(self, form):
        form = AuthenticationForm(data=self.request.POST, request=self.request)

        if form.is_valid():
            delete_sidebar_cache()
            logger.info(self.redirect_field_name)

            auth.login(self.request, form.get_user())
            # 设置登录有效期
            if self.request.POST.get("remember"):
                self.request.session.set_expiry(settings.REMEMBER_ME_LOGIN_TTL)
                cookie_max_age = settings.REMEMBER_ME_LOGIN_TTL
            else:
                # 使用Django默认的2周
                self.request.session.set_expiry(settings.SESSION_COOKIE_AGE)
                cookie_max_age = settings.SESSION_COOKIE_AGE

            # 获取响应对象并设置登录标记 cookie
            response = super(LoginView, self).form_valid(form)
            response.set_cookie(
                'logged_user',
                'true',
                max_age=cookie_max_age,
                httponly=False,  # 允许 JavaScript 访问
                samesite='Lax'
            )
            return response
            # return HttpResponseRedirect('/')
        else:
            return self.render_to_response({
                'form': form
            })

    def get_success_url(self):

        redirect_to = self.request.POST.get(self.redirect_field_name)
        if not url_has_allowed_host_and_scheme(
                url=redirect_to, allowed_hosts=[
                    self.request.get_host()]):
            redirect_to = self.success_url
        return redirect_to


def account_result(request):
    type = request.GET.get('type')
    id = request.GET.get('id')

    user = get_object_or_404(get_user_model(), id=id)
    logger.info(type)
    if user.is_active:
        return HttpResponseRedirect('/')
    if type and type in ['register', 'validation']:
        if type == 'register':
            content = '''
    恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
    '''
            title = '注册成功'
        else:
            c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
            sign = request.GET.get('sign')
            if sign != c_sign:
                return HttpResponseForbidden()
            user.is_active = True
            user.save()
            content = '''
            恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
            '''
            title = '验证成功'
        return render(request, 'account/result.html', {
            'title': title,
            'content': content
        })
    else:
        return HttpResponseRedirect('/')


class ForgetPasswordView(SecureFormView):
    """
    忘记密码视图(重构版)

    使用 SecureFormView 基类,自动提供 CSRF 保护
    """
    form_class = ForgetPasswordForm
    template_name = 'account/forget_password.html'

    def form_valid(self, form):
        if form.is_valid():
            blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
            blog_user.password = make_password(form.cleaned_data["new_password2"])
            blog_user.save()
            return HttpResponseRedirect('/login/')
        else:
            return self.render_to_response({'form': form})


class ForgetPasswordEmailCode(View):

    def post(self, request: HttpRequest):
        form = ForgetPasswordCodeForm(request.POST)
        if not form.is_valid():
            return HttpResponse("错误的邮箱")
        to_email = form.cleaned_data["email"]

        code = generate_code()
        utils.send_verify_email(to_email, code)
        utils.set_code(to_email, code)

        return HttpResponse("ok")


================================================
FILE: blog/__init__.py
================================================


================================================
FILE: blog/admin.py
================================================
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

# Register your models here.
from .models import Article, Category, Tag, Links, SideBar, BlogSettings


class ArticleForm(forms.ModelForm):
    # body = forms.CharField(widget=AdminPagedownWidget())

    class Meta:
        model = Article
        fields = '__all__'


def makr_article_publish(modeladmin, request, queryset):
    queryset.update(status='p')


def draft_article(modeladmin, request, queryset):
    queryset.update(status='d')


def close_article_commentstatus(modeladmin, request, queryset):
    queryset.update(comment_status='c')


def open_article_commentstatus(modeladmin, request, queryset):
    queryset.update(comment_status='o')


makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')


class ArticlelAdmin(admin.ModelAdmin):
    list_per_page = 20
    search_fields = ('body', 'title')
    form = ArticleForm
    list_display = (
        'id',
        'title',
        'author',
        'link_to_category',
        'creation_time',
        'views',
        'status',
        'type',
        'article_order')
    list_display_links = ('id', 'title')
    list_filter = ('status', 'type', 'category')
    date_hierarchy = 'creation_time'
    filter_horizontal = ('tags',)
    exclude = ('creation_time', 'last_modify_time')
    view_on_site = True
    actions = [
        makr_article_publish,
        draft_article,
        close_article_commentstatus,
        open_article_commentstatus]
    raw_id_fields = ('author', 'category',)

    def link_to_category(self, obj):
        info = (obj.category._meta.app_label, obj.category._meta.model_name)
        link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
        return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))

    link_to_category.short_description = _('category')

    def get_form(self, request, obj=None, **kwargs):
        form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
        form.base_fields['author'].queryset = get_user_model(
        ).objects.filter(is_superuser=True)
        return form

    def save_model(self, request, obj, form, change):
        super(ArticlelAdmin, self).save_model(request, obj, form, change)

    def get_view_on_site_url(self, obj=None):
        if obj:
            url = obj.get_full_url()
            return url
        else:
            from djangoblog.utils import get_current_site
            site = get_current_site().domain
            return site


class TagAdmin(admin.ModelAdmin):
    exclude = ('slug', 'last_mod_time', 'creation_time')


class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'parent_category', 'index')
    exclude = ('slug', 'last_mod_time', 'creation_time')


class LinksAdmin(admin.ModelAdmin):
    exclude = ('last_mod_time', 'creation_time')


class SideBarAdmin(admin.ModelAdmin):
    list_display = ('name', 'content', 'is_enable', 'sequence')
    exclude = ('last_mod_time', 'creation_time')


class BlogSettingsAdmin(admin.ModelAdmin):
    """单例配置Admin - 直接跳转到编辑页面"""

    def has_add_permission(self, request):
        """如果已经存在配置,则禁止添加"""
        return not BlogSettings.objects.exists()

    def has_delete_permission(self, request, obj=None):
        """禁止删除配置"""
        return False

    def changelist_view(self, request, extra_context=None):
        """列表页直接跳转到编辑页面"""
        from django.http import HttpResponseRedirect
        obj = BlogSettings.objects.first()
        if obj:
            return HttpResponseRedirect(
                reverse('admin:blog_blogsettings_change', args=[obj.pk])
            )
        # 如果不存在配置,跳转到添加页面
        return HttpResponseRedirect(
            reverse('admin:blog_blogsettings_add')
        )

    def save_model(self, request, obj, form, change):
        """保存设置时清除缓存"""
        super().save_model(request, obj, form, change)
        # 确保缓存被清除
        from djangoblog.utils import cache
        cache.clear()
        self.message_user(request, '设置已保存,缓存已清除')


================================================
FILE: blog/apps.py
================================================
from django.apps import AppConfig
import logging

logger = logging.getLogger(__name__)


class BlogConfig(AppConfig):
    name = 'blog'

    def ready(self):
        """应用启动时的初始化"""
        # 检查并创建搜索索引(如果需要)
        self.check_search_index()

    def check_search_index(self):
        """检查搜索索引是否存在,不存在则创建空索引"""
        from django.conf import settings

        # 1. 检查Elasticsearch索引
        if hasattr(settings, 'ELASTICSEARCH_DSL'):
            try:
                from blog.documents import ELASTICSEARCH_ENABLED, ArticleDocumentManager
                if ELASTICSEARCH_ENABLED:
                    from elasticsearch import Elasticsearch
                    es_config = settings.ELASTICSEARCH_DSL['default']
                    es = Elasticsearch(hosts=[es_config['hosts']], verify_certs=False)

                    # 检查blog索引是否存在
                    if not es.indices.exists(index='blog'):
                        logger.info("Elasticsearch blog index not found, creating empty index...")
                        manager = ArticleDocumentManager()
                        logger.info("Elasticsearch blog index created successfully")
                    else:
                        logger.debug("Elasticsearch blog index already exists")
            except Exception as e:
                logger.warning(f"Failed to check/create Elasticsearch index: {e}")

        # 2. 检查Whoosh索引
        haystack_engine = settings.HAYSTACK_CONNECTIONS.get('default', {}).get('ENGINE', '')
        if 'whoosh' in haystack_engine.lower():
            try:
                import os
                whoosh_path = settings.HAYSTACK_CONNECTIONS['default'].get('PATH')
                if whoosh_path and not os.path.exists(whoosh_path):
                    logger.info(f"Whoosh index directory not found at {whoosh_path}, will be created on first search")
                    # Whoosh会在第一次搜索时自动创建,这里只记录日志
                elif whoosh_path and os.path.exists(whoosh_path):
                    # 检查是否有索引文件
                    index_files = [f for f in os.listdir(whoosh_path) if f.endswith('.seg') or f == '_MAIN_WRITELOCK']
                    if not index_files:
                        logger.info("Whoosh index directory exists but empty, will be initialized on first search")
                    else:
                        logger.debug("Whoosh index already exists")
            except Exception as e:
                logger.warning(f"Failed to check Whoosh index: {e}")


================================================
FILE: blog/context_processors.py
================================================
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:
        # 更新动态值(不需要缓存的内容)
        value['SITE_BASE_URL'] = requests.scheme + '://' + requests.get_host() + '/'
        value['CURRENT_YEAR'] = timezone.now().year
        return value
    else:
        logger.info('set processor cache.')
        setting = get_blog_setting()

        # 优化查询:预加载关联数据
        nav_category_list = Category.objects.all()
        nav_pages = Article.objects.filter(
            type='p',
            status='p'
        )

        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': nav_category_list,  # 保持QuerySet
            'nav_pages': nav_pages,  # 保持QuerySet
            '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,
            "COLOR_SCHEME": setting.color_scheme,
        }
        cache.set(key, value, 60 * 60 * 10)
        return value


================================================
FILE: blog/documents.py
================================================
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:
    from elasticsearch import Elasticsearch

    es_config = settings.ELASTICSEARCH_DSL['default']

    # 处理 hosts 配置,确保有 scheme
    hosts = es_config['hosts']
    if isinstance(hosts, str):
        # 如果没有 scheme,自动添加 http://
        if not hosts.startswith(('http://', 'https://')):
            hosts = f'http://{hosts}'
        hosts = [hosts]
    elif isinstance(hosts, list):
        # 处理列表中的每个 host
        processed_hosts = []
        for host in hosts:
            if not host.startswith(('http://', 'https://')):
                host = f'http://{host}'
            processed_hosts.append(host)
        hosts = processed_hosts

    # ES 连接配置(动态适配认证方式)
    es_params = {
        'hosts': hosts,
        'verify_certs': es_config.get('verify_certs', False),
    }

    # 支持用户名密码认证(ES 8.x 默认启用安全特性)
    if 'username' in es_config and 'password' in es_config:
        es_params['basic_auth'] = (es_config['username'], es_config['password'])

    # 支持 API Key 认证
    if 'api_key' in es_config:
        es_params['api_key'] = es_config['api_key']

    # 支持证书认证
    if 'ca_certs' in es_config:
        es_params['ca_certs'] = es_config['ca_certs']

    # 支持客户端证书
    if 'client_cert' in es_config and 'client_key' in es_config:
        es_params['client_cert'] = es_config['client_cert']
        es_params['client_key'] = es_config['client_key']

    # 创建连接
    es = Elasticsearch(**es_params)
    connections.create_connection(**es_params)

    # 设置 GeoIP pipeline
    try:
        es.ingest.get_pipeline(id='geoip')
    except elasticsearch.exceptions.NotFoundError:
        es.ingest.put_pipeline(id='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 ElaspedTimeDocumentManager:
    @staticmethod
    def build_index():
        from elasticsearch import Elasticsearch
        # 使用已配置好的连接参数
        client = Elasticsearch(**es_params)
        res = client.indices.exists(index="performance")
        if not res:
            ElapsedTimeDocument.init()

    @staticmethod
    def delete_index():
        from elasticsearch import Elasticsearch
        es = Elasticsearch(**es_params)
        try:
            es.indices.delete(index='performance')
        except elasticsearch.exceptions.NotFoundError:
            pass

    @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 ArticleDocumentManager():

    def __init__(self):
        self.create_index()

    def create_index(self):
        ArticleDocument.init()

    def delete_index(self):
        from elasticsearch import Elasticsearch
        es = Elasticsearch(**es_params)
        try:
            es.indices.delete(index='blog')
        except elasticsearch.exceptions.NotFoundError:
            pass

    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()


================================================
FILE: blog/forms.py
================================================
import logging

from django import forms
from haystack.forms import SearchForm

logger = logging.getLogger(__name__)


class BlogSearchForm(SearchForm):
    querydata = forms.CharField(required=True)

    def search(self):
        datas = super(BlogSearchForm, self).search()
        if not self.is_valid():
            return self.no_query_found()

        if self.cleaned_data['querydata']:
            logger.info(self.cleaned_data['querydata'])
        return datas


================================================
FILE: blog/management/__init__.py
================================================


================================================
FILE: blog/management/commands/__init__.py
================================================


================================================
FILE: blog/management/commands/build_index.py
================================================
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()


================================================
FILE: blog/management/commands/build_search_words.py
================================================
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))


================================================
FILE: blog/management/commands/clear_cache.py
================================================
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'))


================================================
FILE: blog/management/commands/create_testdata.py
================================================
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'))


================================================
FILE: blog/management/commands/ping_baidu.py
================================================
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'))


================================================
FILE: blog/management/commands/sync_user_avatar.py
================================================
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('结束同步')


================================================
FILE: blog/middleware.py
================================================
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


================================================
FILE: blog/migrations/0001_initial.py
================================================
# 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',
            },
        ),
    ]


================================================
FILE: blog/migrations/0002_blogsettings_global_footer_and_more.py
================================================
# 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='公共头部'),
        ),
    ]


================================================
FILE: blog/migrations/0003_blogsettings_comment_need_review.py
================================================
# 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='评论是否需要审核'),
        ),
    ]


================================================
FILE: blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
================================================
# 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',
        ),
    ]


================================================
FILE: blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
================================================
# 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'),
        ),
    ]


================================================
FILE: blog/migrations/0006_alter_blogsettings_options.py
================================================
# 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'},
        ),
    ]


================================================
FILE: blog/migrations/0007_article_idx_type_status_pub_article_idx_status_views_and_more.py
================================================
# Generated by Django 5.2.9 on 2025-12-25 14:36

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0006_alter_blogsettings_options'),
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.AddIndex(
            model_name='article',
            index=models.Index(fields=['type', 'status', '-pub_time'], name='idx_type_status_pub'),
        ),
        migrations.AddIndex(
            model_name='article',
            index=models.Index(fields=['status', '-views'], name='idx_status_views'),
        ),
        migrations.AddIndex(
            model_name='article',
            index=models.Index(fields=['author', 'status', 'type'], name='idx_author_status_type'),
        ),
        migrations.AddIndex(
            model_name='article',
            index=models.Index(fields=['category', 'status'], name='idx_category_status'),
        ),
    ]


================================================
FILE: blog/migrations/0008_blogsettings_color_scheme.py
================================================
# Generated by Django 5.2.9 on 2025-12-31 13:30

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0007_article_idx_type_status_pub_article_idx_status_views_and_more'),
    ]

    operations = [
        migrations.AddField(
            model_name='blogsettings',
            name='color_scheme',
            field=models.CharField(choices=[('purple', '紫色主题 - Purple Dream'), ('blue', '蓝色主题 - Ocean Blue'), ('green', '绿色主题 - Forest Green'), ('orange', '橙色主题 - Sunset Orange'), ('pink', '粉色主题 - Cherry Blossom'), ('red', '红色主题 - Ruby Red'), ('indigo', '靛蓝主题 - Midnight Indigo'), ('teal', '青色主题 - Teal Wave')], default='purple', help_text='选择网站的主题配色方案', max_length=20, verbose_name='配色方案'),
        ),
    ]


================================================
FILE: blog/migrations/__init__.py
================================================


================================================
FILE: blog/models.py
================================================
import logging
import re
from abc import abstractmethod

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify

from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
from djangoblog.constants import CacheTimeout, CacheKey

logger = logging.getLogger(__name__)


class LinkShowType(models.TextChoices):
    I = ('i', _('index'))
    L = ('l', _('list'))
    P = ('p', _('post'))
    A = ('a', _('all'))
    S = ('s', _('slide'))


class BaseModel(models.Model):
    id = models.AutoField(primary_key=True)
    creation_time = models.DateTimeField(_('creation time'), default=now)
    last_modify_time = models.DateTimeField(_('modify time'), default=now)

    def save(self, *args, **kwargs):
        is_update_views = isinstance(
            self,
            Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
        if is_update_views:
            Article.objects.filter(pk=self.pk).update(views=self.views)
        else:
            if 'slug' in self.__dict__:
                slug = getattr(
                    self, 'title') if 'title' in self.__dict__ else getattr(
                    self, 'name')
                setattr(self, 'slug', slugify(slug))
            super().save(*args, **kwargs)

    def get_full_url(self):
        site = get_current_site().domain
        url = "https://{site}{path}".format(site=site,
                                            path=self.get_absolute_url())
        return url

    class Meta:
        abstract = True

    @abstractmethod
    def get_absolute_url(self):
        pass


class Article(BaseModel):
    """文章"""
    STATUS_CHOICES = (
        ('d', _('Draft')),
        ('p', _('Published')),
    )
    COMMENT_STATUS = (
        ('o', _('Open')),
        ('c', _('Close')),
    )
    TYPE = (
        ('a', _('Article')),
        ('p', _('Page')),
    )
    title = models.CharField(_('title'), max_length=200, unique=True)
    body = MDTextField(_('body'))
    pub_time = models.DateTimeField(
        _('publish time'), blank=False, null=False, default=now)
    status = models.CharField(
        _('status'),
        max_length=1,
        choices=STATUS_CHOICES,
        default='p')
    comment_status = models.CharField(
        _('comment status'),
        max_length=1,
        choices=COMMENT_STATUS,
        default='o')
    type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
    views = models.PositiveIntegerField(_('views'), default=0)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('author'),
        blank=False,
        null=False,
        on_delete=models.CASCADE)
    article_order = models.IntegerField(
        _('order'), blank=False, null=False, default=0)
    show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
    category = models.ForeignKey(
        'Category',
        verbose_name=_('category'),
        on_delete=models.CASCADE,
        blank=False,
        null=False)
    tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)

    def body_to_string(self):
        return self.body

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-article_order', '-pub_time']
        verbose_name = _('article')
        verbose_name_plural = verbose_name
        get_latest_by = 'id'
        indexes = [
            # 优化列表查询:type + status + pub_time组合索引
            models.Index(fields=['type', 'status', '-pub_time'], name='idx_type_status_pub'),
            # 优化热门文章查询:status + views组合索引
            models.Index(fields=['status', '-views'], name='idx_status_views'),
            # 优化作者文章查询:author + status + type组合索引
            models.Index(fields=['author', 'status', 'type'], name='idx_author_status_type'),
            # 优化分类查询:category + status组合索引
            models.Index(fields=['category', 'status'], name='idx_category_status'),
        ]

    def get_absolute_url(self):
        return reverse('blog:detailbyid', kwargs={
            'article_id': self.id,
            'year': self.creation_time.year,
            'month': self.creation_time.month,
            'day': self.creation_time.day
        })

    @cache_decorator(CacheTimeout.HOUR_10)
    def get_category_tree(self):
        tree = self.category.get_category_tree()
        names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))

        return names

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

    def viewed(self):
        self.views += 1
        self.save(update_fields=['views'])

    def comment_list(self):
        cache_key = CacheKey.ARTICLE_COMMENTS.format(article_id=self.id)
        value = cache.get(cache_key)
        if value:
            logger.info(f'Cache HIT: article comments (id={self.id})')
            return value
        else:
            comments = self.comment_set.filter(is_enable=True).order_by('-id')
            cache.set(cache_key, comments, CacheTimeout.HOUR_10)
            logger.info(f'Cache MISS: article comments (id={self.id})')
            return comments

    def get_admin_url(self):
        info = (self._meta.app_label, self._meta.model_name)
        return reverse('admin:%s_%s_change' % info, args=(self.pk,))

    @cache_decorator(expiration=CacheTimeout.HOUR_10)
    def next_article(self):
        # 下一篇
        return Article.objects.filter(
            id__gt=self.id, status='p').order_by('id').first()

    @cache_decorator(expiration=CacheTimeout.HOUR_10)
    def prev_article(self):
        # 前一篇
        return Article.objects.filter(id__lt=self.id, status='p').first()

    def get_first_image_url(self):
        """
        Get the first image url from article.body.
        :return:
        """
        match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
        if match:
            return match.group(1)
        return ""


class Category(BaseModel):
    """文章分类"""
    name = models.CharField(_('category name'), max_length=30, unique=True)
    parent_category = models.ForeignKey(
        'self',
        verbose_name=_('parent category'),
        blank=True,
        null=True,
        on_delete=models.CASCADE)
    slug = models.SlugField(default='no-slug', max_length=60, blank=True)
    index = models.IntegerField(default=0, verbose_name=_('index'))

    class Meta:
        ordering = ['-index']
        verbose_name = _('category')
        verbose_name_plural = verbose_name

    def get_absolute_url(self):
        return reverse(
            'blog:category_detail', kwargs={
                'category_name': self.slug})

    def __str__(self):
        return self.name

    @cache_decorator(CacheTimeout.HOUR_10)
    def get_category_tree(self):
        """
        递归获得分类目录的父级
        :return:
        """
        categorys = []

        def parse(category):
            categorys.append(category)
            if category.parent_category:
                parse(category.parent_category)

        parse(self)
        return categorys

    @cache_decorator(CacheTimeout.HOUR_10)
    def get_sub_categorys(self):
        """
        获得当前分类目录所有子集
        :return:
        """
        categorys = []
        all_categorys = Category.objects.all()

        def parse(category):
            if category not in categorys:
                categorys.append(category)
            childs = all_categorys.filter(parent_category=category)
            for child in childs:
                if category not in categorys:
                    categorys.append(child)
                parse(child)

        parse(self)
        return categorys


class Tag(BaseModel):
    """文章标签"""
    name = models.CharField(_('tag name'), max_length=30, unique=True)
    slug = models.SlugField(default='no-slug', max_length=60, blank=True)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})

    @cache_decorator(CacheTimeout.HOUR_10)
    def get_article_count(self):
        return Article.objects.filter(tags__name=self.name).distinct().count()

    class Meta:
        ordering = ['name']
        verbose_name = _('tag')
        verbose_name_plural = verbose_name


class Links(models.Model):
    """友情链接"""

    name = models.CharField(_('link name'), max_length=30, unique=True)
    link = models.URLField(_('link'))
    sequence = models.IntegerField(_('order'), unique=True)
    is_enable = models.BooleanField(
        _('is show'), default=True, blank=False, null=False)
    show_type = models.CharField(
        _('show type'),
        max_length=1,
        choices=LinkShowType.choices,
        default=LinkShowType.I)
    creation_time = models.DateTimeField(_('creation time'), default=now)
    last_mod_time = models.DateTimeField(_('modify time'), default=now)

    class Meta:
        ordering = ['sequence']
        verbose_name = _('link')
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class SideBar(models.Model):
    """侧边栏,可以展示一些html内容"""
    name = models.CharField(_('title'), max_length=100)
    content = models.TextField(_('content'))
    sequence = models.IntegerField(_('order'), unique=True)
    is_enable = models.BooleanField(_('is enable'), default=True)
    creation_time = models.DateTimeField(_('creation time'), default=now)
    last_mod_time = models.DateTimeField(_('modify time'), default=now)

    class Meta:
        ordering = ['sequence']
        verbose_name = _('sidebar')
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class BlogSettings(models.Model):
    """blog的配置"""

    COLOR_SCHEMES = (
        ('purple', _('紫色主题 - Purple Dream')),
        ('blue', _('蓝色主题 - Ocean Blue')),
        ('green', _('绿色主题 - Forest Green')),
        ('orange', _('橙色主题 - Sunset Orange')),
        ('pink', _('粉色主题 - Cherry Blossom')),
        ('red', _('红色主题 - Ruby Red')),
        ('indigo', _('靛蓝主题 - Midnight Indigo')),
        ('teal', _('青色主题 - Teal Wave')),
    )

    site_name = models.CharField(
        _('site name'),
        max_length=200,
        null=False,
        blank=False,
        default='')
    site_description = models.TextField(
        _('site description'),
        max_length=1000,
        null=False,
        blank=False,
        default='')
    site_seo_description = models.TextField(
        _('site seo description'), max_length=1000, null=False, blank=False, default='')
    site_keywords = models.TextField(
        _('site keywords'),
        max_length=1000,
        null=False,
        blank=False,
        default='')
    article_sub_length = models.IntegerField(_('article sub length'), default=300)
    sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
    sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
    article_comment_count = models.IntegerField(_('article comment count'), default=5)
    show_google_adsense = models.BooleanField(_('show adsense'), default=False)
    google_adsense_codes = models.TextField(
        _('adsense code'), max_length=2000, null=True, blank=True, default='')
    open_site_comment = models.BooleanField(_('open site comment'), default=True)
    color_scheme = models.CharField(
        _('配色方案'),
        max_length=20,
        choices=COLOR_SCHEMES,
        default='purple',
        help_text=_('选择网站的主题配色方案'))
    global_header = models.TextField("公共头部", null=True, blank=True, default='')
    global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
    beian_code = models.CharField(
        '备案号',
        max_length=2000,
        null=True,
        blank=True,
        default='')
    analytics_code = models.TextField(
        "网站统计代码",
        max_length=1000,
        null=False,
        blank=False,
        default='')
    show_gongan_code = models.BooleanField(
        '是否显示公安备案号', default=False, null=False)
    gongan_beiancode = models.TextField(
        '公安备案号',
        max_length=2000,
        null=True,
        blank=True,
        default='')
    comment_need_review = models.BooleanField(
        '评论是否需要审核', default=False, null=False)

    class Meta:
        verbose_name = _('Website configuration')
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.site_name

    def clean(self):
        if BlogSettings.objects.exclude(id=self.id).count():
            raise ValidationError(_('There can only be one configuration'))

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        from djangoblog.utils import cache
        cache.clear()


================================================
FILE: blog/search_indexes.py
================================================
from haystack import indexes

from blog.models import Article


class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    title = indexes.CharField(model_attr='title', stored=True)
    body = indexes.CharField(model_attr='body', stored=True)

    def get_model(self):
        return Article

    def index_queryset(self, using=None):
        return self.get_model().objects.filter(status='p')


================================================
FILE: blog/static/account/css/account.css
================================================
.button {
    border: none;
    padding: 4px 80px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    margin: 4px 2px;
}

================================================
FILE: blog/static/account/js/account.js
================================================
let wait = 60;

function time(o) {
    if (wait == 0) {
        o.removeAttribute("disabled");
        o.value = "获取验证码";
        wait = 60
        return false
    } else {
        o.setAttribute("disabled", true);
        o.value = "重新发送(" + wait + ")";
        wait--;
        setTimeout(function () {
                time(o)
            },
            1000)
    }
}

document.getElementById("btn").onclick = function () {
    let id_email = $("#id_email")
    let token = $("*[name='csrfmiddlewaretoken']").val()
    let ts = this
    let myErr = $("#myErr")
    $.ajax(
        {
            url: "/forget_password_code/",
            type: "POST",
            data: {
                "email": id_email.val(),
                "csrfmiddlewaretoken": token
            },
            success: function (result) {
                if (result != "ok") {
                    myErr.remove()
                    id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
                    return
                }
                myErr.remove()
                time(ts)
            },
            error: function (e) {
                alert("发送失败,请重试")
            }
        }
    );
}


================================================
FILE: blog/static/assets/css/signin.css
================================================
body {
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #fff;
}

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


================================================
FILE: blog/static/blog/css/oauth_style.css
================================================

.icon-sn-google {
    background-position: 0 -28px;
}

.icon-sn-bg-google {
    background-color: #4285f4;
    background-position: 0 0;
}

.fa-sn-google {
    color: #4285f4;
}

.icon-sn-github {
    background-position: -28px -28px;
}

.icon-sn-bg-github {
    background-color: #333;
    background-position: -28px 0;
}

.fa-sn-github {
    color: #333;
}

.icon-sn-weibo {
    background-position: -56px -28px;
}

.icon-sn-bg-weibo {
    background-color: #e90d24;
    background-position: -56px 0;
}

.fa-sn-weibo {
    color: #e90d24;
}

.icon-sn-qq {
    background-position: -84px -28px;
}

.icon-sn-bg-qq {
    background-color: #0098e6;
    background-position: -84px 0;
}

.fa-sn-qq {
    color: #0098e6;
}

.icon-sn-twitter {
    background-position: -112px -28px;
}

.icon-sn-bg-twitter {
    background-color: #50abf1;
    background-position: -112px 0;
}

.fa-sn-twitter {
    color: #50abf1;
}

.icon-sn-facebook {
    background-position: -140px -28px;
}

.icon-sn-bg-facebook {
    background-color: #4862a3;
    background-position: -140px 0;
}

.fa-sn-facebook {
    color: #4862a3;
}

.icon-sn-renren {
    background-position: -168px -28px;
}

.icon-sn-bg-renren {
    background-color: #197bc8;
    background-position: -168px 0;
}

.fa-sn-renren {
    color: #197bc8;
}

.icon-sn-tqq {
    background-position: -196px -28px;
}

.icon-sn-bg-tqq {
    background-color: #1f9ed2;
    background-position: -196px 0;
}

.fa-sn-tqq {
    color: #1f9ed2;
}

.icon-sn-douban {
    background-position: -224px -28px;
}

.icon-sn-bg-douban {
    background-color: #279738;
    background-position: -224px 0;
}

.fa-sn-douban {
    color: #279738;
}

.icon-sn-weixin {
    background-position: -252px -28px;
}

.icon-sn-bg-weixin {
    background-color: #00b500;
    background-position: -252px 0;
}

.fa-sn-weixin {
    color: #00b500;
}

.icon-sn-dotted {
    background-position: -280px -28px;
}

.icon-sn-bg-dotted {
    background-color: #eee;
    background-position: -280px 0;
}

.fa-sn-dotted {
    color: #eee;
}

.icon-sn-site {
    background-position: -308px -28px;
}

.icon-sn-bg-site {
    background-color: #00b500;
    background-position: -308px 0;
}

.fa-sn-site {
    color: #00b500;
}

.icon-sn-linkedin {
    background-position: -336px -28px;
}

.icon-sn-bg-linkedin {
    background-color: #0077b9;
    background-position: -336px 0;
}

.fa-sn-linkedin {
    color: #0077b9;
}

[class*=icon-sn-] {
    display: inline-block;
    background-image: url('../img/icon-sn.svg');
    background-repeat: no-repeat;
    width: 28px;
    height: 28px;
    vertical-align: middle;
    background-size: auto 56px;
}

[class*=icon-sn-]:hover {
    opacity: .8;
    filter: alpha(opacity=80);
}

.btn-sn-google {
    background: #4285f4;
}

.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
    background: #2a75f3;
}

.btn-sn-github {
    background: #333;
}

.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
    background: #262626;
}

.btn-sn-weibo {
    background: #e90d24;
}

.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
    background: #d10c20;
}

.btn-sn-qq {
    background: #0098e6;
}

.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
    background: #0087cd;
}

.btn-sn-twitter {
    background: #50abf1;
}

.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
    background: #38a0ef;
}

.btn-sn-facebook {
    background: #4862a3;
}

.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
    background: #405791;
}

.btn-sn-renren {
    background: #197bc8;
}

.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
    background: #166db1;
}

.btn-sn-tqq {
    background: #1f9ed2;
}

.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
    background: #1c8dbc;
}

.btn-sn-douban {
    background: #279738;
}

.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
    background: #228330;
}

.btn-sn-weixin {
    background: #00b500;
}

.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
    background: #009c00;
}

.btn-sn-dotted {
    background: #eee;
}

.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
    background: #e1e1e1;
}

.btn-sn-site {
    background: #00b500;
}

.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
    background: #009c00;
}

.btn-sn-linkedin {
    background: #0077b9;
}

.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
    background: #0067a0;
}

[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
    border: none;
    color: #fff;
}

.btn-sn-more {
    padding: 0;
}

.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
    box-shadow: none;
}

[class*=btn-sn-] [class*=icon-sn-] {
    background-color: transparent;
}

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

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── deploy-master.yml
│       ├── django.yml
│       ├── docker.yml
│       ├── frontend.yml
│       └── publish-release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── accounts/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── templatetags/
│   │   └── __init__.py
│   ├── test_admin.py
│   ├── test_user_business_logic.py
│   ├── tests.py
│   ├── urls.py
│   ├── user_login_backend.py
│   ├── utils.py
│   └── views.py
├── blog/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── context_processors.py
│   ├── documents.py
│   ├── forms.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       ├── build_index.py
│   │       ├── build_search_words.py
│   │       ├── clear_cache.py
│   │       ├── create_testdata.py
│   │       ├── ping_baidu.py
│   │       └── sync_user_avatar.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_blogsettings_global_footer_and_more.py
│   │   ├── 0003_blogsettings_comment_need_review.py
│   │   ├── 0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
│   │   ├── 0005_alter_article_options_alter_category_options_and_more.py
│   │   ├── 0006_alter_blogsettings_options.py
│   │   ├── 0007_article_idx_type_status_pub_article_idx_status_views_and_more.py
│   │   ├── 0008_blogsettings_color_scheme.py
│   │   └── __init__.py
│   ├── models.py
│   ├── search_indexes.py
│   ├── static/
│   │   ├── account/
│   │   │   ├── css/
│   │   │   │   └── account.css
│   │   │   └── js/
│   │   │       └── account.js
│   │   ├── assets/
│   │   │   └── css/
│   │   │       └── signin.css
│   │   ├── blog/
│   │   │   ├── css/
│   │   │   │   └── oauth_style.css
│   │   │   ├── fonts/
│   │   │   │   └── open-sans.css
│   │   │   └── js/
│   │   │       └── mathjax-loader.js
│   │   └── pygments/
│   │       └── default.css
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── blog_tags.py
│   │   └── vite_tags.py
│   ├── test_admin.py
│   ├── test_article_business_logic.py
│   ├── test_context_processors.py
│   ├── test_middleware.py
│   ├── test_templatetags.py
│   ├── test_views.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── codecov.yml
├── comments/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_comment_is_enable.py
│   │   ├── 0003_alter_comment_options_remove_comment_created_time_and_more.py
│   │   ├── 0004_comment_idx_art_parent_enable_comment_idx_enable_id.py
│   │   ├── 0005_commentreaction.py
│   │   └── __init__.py
│   ├── models.py
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── comments_tags.py
│   ├── test_comment_business_logic.py
│   ├── test_views.py
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py
│   └── views.py
├── deploy/
│   ├── docker-compose/
│   │   ├── docker-compose.es.yml
│   │   └── docker-compose.yml
│   ├── entrypoint.sh
│   ├── k8s/
│   │   ├── configmap.yaml
│   │   ├── deployment.yaml
│   │   ├── gateway.yaml
│   │   ├── pv.yaml
│   │   ├── pvc.yaml
│   │   ├── service.yaml
│   │   └── storageclass.yaml
│   └── nginx.conf
├── djangoblog/
│   ├── __init__.py
│   ├── admin_site.py
│   ├── apps.py
│   ├── base_views.py
│   ├── blog_signals.py
│   ├── constants.py
│   ├── elasticsearch_backend.py
│   ├── error_views.py
│   ├── feeds.py
│   ├── logentryadmin.py
│   ├── mixins.py
│   ├── plugin_manage/
│   │   ├── base_plugin.py
│   │   ├── hook_constants.py
│   │   ├── hooks.py
│   │   └── loader.py
│   ├── settings.py
│   ├── sitemap.py
│   ├── spider_notify.py
│   ├── test_base.py
│   ├── test_email_integration.py
│   ├── test_email_integration_complete.py
│   ├── test_plugins.py
│   ├── tests.py
│   ├── urls.py
│   ├── utils.py
│   ├── whoosh_cn_backend.py
│   └── wsgi.py
├── docs/
│   ├── README-en.md
│   ├── config-en.md
│   ├── config.md
│   ├── docker-en.md
│   ├── docker.md
│   ├── es.md
│   ├── k8s-en.md
│   ├── k8s.md
│   └── search-engine-config.md
├── frontend/
│   ├── .gitignore
│   ├── package.json
│   ├── postcss.config.js
│   ├── src/
│   │   ├── components/
│   │   │   ├── backToTop.js
│   │   │   ├── commentSystem.js
│   │   │   ├── imageLightbox.js
│   │   │   ├── navigation.js
│   │   │   └── reactionPicker.js
│   │   ├── features/
│   │   │   └── darkMode.js
│   │   ├── main.js
│   │   ├── styles/
│   │   │   └── main.css
│   │   └── utils/
│   │       └── nprogress.js
│   ├── tailwind.config.js
│   └── vite.config.js
├── locale/
│   ├── en/
│   │   └── LC_MESSAGES/
│   │       ├── django.mo
│   │       └── django.po
│   ├── zh_Hans/
│   │   └── LC_MESSAGES/
│   │       ├── django.mo
│   │       └── django.po
│   └── zh_Hant/
│       └── LC_MESSAGES/
│           ├── django.mo
│           └── django.po
├── manage.py
├── oauth/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
│   │   ├── 0003_alter_oauthuser_nickname.py
│   │   └── __init__.py
│   ├── models.py
│   ├── oauthmanager.py
│   ├── templatetags/
│   │   ├── __init__.py
│   │   └── oauth_tags.py
│   ├── test_oauth_business_logic.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── owntracks/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_owntracklog_options_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── plugins/
│   ├── __init__.py
│   ├── article_copyright/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── article_recommendation/
│   │   ├── __init__.py
│   │   ├── plugin.py
│   │   └── static/
│   │       └── article_recommendation/
│   │           ├── css/
│   │           │   └── recommendation.css
│   │           └── js/
│   │               └── recommendation.js
│   ├── cloudflare_cache/
│   │   ├── __init__.py
│   │   ├── api.py
│   │   ├── handlers.py
│   │   └── plugin.py
│   ├── external_links/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── image_lazy_loading/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── reading_time/
│   │   ├── __init__.py
│   │   └── plugin.py
│   ├── seo_optimizer/
│   │   ├── __init__.py
│   │   └── plugin.py
│   └── view_count/
│       ├── __init__.py
│       └── plugin.py
├── requirements.txt
├── servermanager/
│   ├── MemcacheStorage.py
│   ├── __init__.py
│   ├── admin.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── blogapi.py
│   │   └── commonapi.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_alter_emailsendlog_options_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── robot.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── templates/
    ├── account/
    │   ├── forget_password.html
    │   ├── login.html
    │   ├── registration_form.html
    │   └── result.html
    ├── blog/
    │   ├── article_archives.html
    │   ├── article_detail.html
    │   ├── article_index.html
    │   ├── error_page.html
    │   ├── links_list.html
    │   └── tags/
    │       ├── article_info.html
    │       ├── article_info_highlight.html
    │       ├── article_meta_info.html
    │       ├── article_pagination.html
    │       ├── article_tag_list.html
    │       ├── breadcrumb.html
    │       └── sidebar.html
    ├── comments/
    │   └── tags/
    │       ├── comment_item.html
    │       ├── comment_item_modern.html
    │       ├── comment_item_tree.html
    │       ├── comment_list.html
    │       ├── comment_list_modern.html
    │       ├── post_comment.html
    │       └── post_comment_modern.html
    ├── oauth/
    │   ├── bindsuccess.html
    │   ├── oauth_applications.html
    │   └── require_email.html
    ├── owntracks/
    │   ├── show_log_dates.html
    │   └── show_maps.html
    ├── plugins/
    │   ├── article_recommendation/
    │   │   ├── __init__.py
    │   │   ├── bottom_widget.html
    │   │   └── sidebar_widget.html
    │   ├── css_includes.html
    │   └── js_includes.html
    ├── search/
    │   ├── indexes/
    │   │   └── blog/
    │   │       └── article_text.txt
    │   └── search.html
    └── share_layout/
        ├── adsense.html
        ├── base.html
        ├── base_account.html
        ├── footer.html
        ├── nav.html
        ├── nav_node.html
        └── nav_node_mobile.html
Download .txt
SYMBOL INDEX (1161 symbols across 132 files)

FILE: accounts/admin.py
  class BlogUserCreationForm (line 11) | class BlogUserCreationForm(forms.ModelForm):
    class Meta (line 15) | class Meta:
    method clean_password2 (line 19) | def clean_password2(self):
    method save (line 27) | def save(self, commit=True):
  class BlogUserChangeForm (line 37) | class BlogUserChangeForm(UserChangeForm):
    class Meta (line 38) | class Meta:
    method __init__ (line 43) | def __init__(self, *args, **kwargs):
  class BlogUserAdmin (line 47) | class BlogUserAdmin(UserAdmin):

FILE: accounts/apps.py
  class AccountsConfig (line 4) | class AccountsConfig(AppConfig):

FILE: accounts/forms.py
  class LoginForm (line 11) | class LoginForm(AuthenticationForm):
    method __init__ (line 12) | def __init__(self, *args, **kwargs):
  class RegisterForm (line 20) | class RegisterForm(UserCreationForm):
    method __init__ (line 21) | def __init__(self, *args, **kwargs):
    method clean_email (line 33) | def clean_email(self):
    class Meta (line 39) | class Meta:
  class ForgetPasswordForm (line 44) | class ForgetPasswordForm(forms.Form):
    method clean_new_password2 (line 85) | def clean_new_password2(self):
    method clean_email (line 94) | def clean_email(self):
    method clean_code (line 103) | def clean_code(self):
  class ForgetPasswordCodeForm (line 114) | class ForgetPasswordCodeForm(forms.Form):

FILE: accounts/migrations/0001_initial.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: accounts/models.py
  class BlogUser (line 11) | class BlogUser(AbstractUser):
    method get_absolute_url (line 17) | def get_absolute_url(self):
    method __str__ (line 22) | def __str__(self):
    method get_full_url (line 25) | def get_full_url(self):
    class Meta (line 31) | class Meta:

FILE: accounts/test_admin.py
  class BlogUserAdminTest (line 14) | class BlogUserAdminTest(BaseTestCase, AdminTestMixin):
    method setUp (line 17) | def setUp(self):
    method test_admin_list_display (line 22) | def test_admin_list_display(self):
    method test_admin_search (line 29) | def test_admin_search(self):
    method test_admin_filter_by_is_staff (line 37) | def test_admin_filter_by_is_staff(self):
    method test_admin_change_user (line 46) | def test_admin_change_user(self):
    method test_admin_requires_login (line 62) | def test_admin_requires_login(self):
    method test_admin_forbidden_for_normal_user (line 67) | def test_admin_forbidden_for_normal_user(self):
    method test_get_list_filter (line 72) | def test_get_list_filter(self):
    method test_get_readonly_fields_for_superuser (line 80) | def test_get_readonly_fields_for_superuser(self):
    method test_get_readonly_fields_for_staff (line 87) | def test_get_readonly_fields_for_staff(self):

FILE: accounts/test_user_business_logic.py
  class UserRegistrationTest (line 12) | class UserRegistrationTest(TestCase):
    method test_user_can_be_created (line 15) | def test_user_can_be_created(self):
    method test_user_password_is_hashed (line 27) | def test_user_password_is_hashed(self):
    method test_user_can_check_password (line 41) | def test_user_can_check_password(self):
    method test_username_must_be_unique (line 55) | def test_username_must_be_unique(self):
    method test_email_is_stored_correctly (line 71) | def test_email_is_stored_correctly(self):
    method test_user_is_active_by_default (line 82) | def test_user_is_active_by_default(self):
    method test_user_is_not_staff_by_default (line 92) | def test_user_is_not_staff_by_default(self):
    method test_user_is_not_superuser_by_default (line 102) | def test_user_is_not_superuser_by_default(self):
  class UserAuthenticationTest (line 113) | class UserAuthenticationTest(TestCase):
    method setUp (line 116) | def setUp(self):
    method test_user_can_authenticate_with_correct_credentials (line 128) | def test_user_can_authenticate_with_correct_credentials(self):
    method test_user_cannot_authenticate_with_wrong_password (line 134) | def test_user_cannot_authenticate_with_wrong_password(self):
    method test_user_cannot_authenticate_with_wrong_username (line 139) | def test_user_cannot_authenticate_with_wrong_username(self):
    method test_inactive_user_cannot_authenticate (line 144) | def test_inactive_user_cannot_authenticate(self):
    method test_active_user_can_authenticate (line 156) | def test_active_user_can_authenticate(self):
  class UserPasswordManagementTest (line 165) | class UserPasswordManagementTest(TestCase):
    method setUp (line 168) | def setUp(self):
    method test_user_can_change_password (line 176) | def test_user_can_change_password(self):
    method test_password_change_requires_save (line 193) | def test_password_change_requires_save(self):
    method test_set_unusable_password (line 207) | def test_set_unusable_password(self):
  class UserPermissionTest (line 216) | class UserPermissionTest(TestCase):
    method setUp (line 219) | def setUp(self):
    method test_normal_user_has_no_special_privileges (line 240) | def test_normal_user_has_no_special_privileges(self):
    method test_staff_user_is_staff (line 245) | def test_staff_user_is_staff(self):
    method test_superuser_has_all_privileges (line 251) | def test_superuser_has_all_privileges(self):
    method test_create_superuser_method (line 257) | def test_create_superuser_method(self):
    method test_user_can_be_promoted_to_staff (line 268) | def test_user_can_be_promoted_to_staff(self):
    method test_user_can_be_promoted_to_superuser (line 285) | def test_user_can_be_promoted_to_superuser(self):
  class UserActivationTest (line 303) | class UserActivationTest(TestCase):
    method test_user_can_be_deactivated (line 306) | def test_user_can_be_deactivated(self):
    method test_user_can_be_reactivated (line 323) | def test_user_can_be_reactivated(self):
  class UserProfileTest (line 342) | class UserProfileTest(TestCase):
    method test_user_has_username (line 345) | def test_user_has_username(self):
    method test_user_has_email (line 355) | def test_user_has_email(self):
    method test_user_can_update_email (line 365) | def test_user_can_update_email(self):
    method test_user_string_representation (line 380) | def test_user_string_representation(self):
  class UserQueryTest (line 394) | class UserQueryTest(TestCase):
    method setUp (line 397) | def setUp(self):
    method test_query_user_by_username (line 409) | def test_query_user_by_username(self):
    method test_query_user_by_email (line 414) | def test_query_user_by_email(self):
    method test_query_active_users (line 419) | def test_query_active_users(self):
    method test_query_staff_users (line 431) | def test_query_staff_users(self):
    method test_query_superusers (line 443) | def test_query_superusers(self):
  class UserDeletionTest (line 457) | class UserDeletionTest(TestCase):
    method test_user_can_be_deleted (line 460) | def test_user_can_be_deleted(self):
    method test_delete_user_cascade_effects (line 477) | def test_delete_user_cascade_effects(self):

FILE: accounts/tests.py
  class AccountTest (line 14) | class AccountTest(TestCase):
    method setUp (line 15) | def setUp(self):
    method test_validate_account (line 25) | def test_validate_account(self):
    method test_validate_register (line 58) | def test_validate_register(self):
    method test_verify_email_code (line 121) | def test_verify_email_code(self):
    method test_forget_password_email_code_success (line 133) | def test_forget_password_email_code_success(self):
    method test_forget_password_email_code_fail (line 142) | def test_forget_password_email_code_fail(self):
    method test_forget_password_email_success (line 155) | def test_forget_password_email_success(self):
    method test_forget_password_email_not_user (line 177) | def test_forget_password_email_not_user(self):
    method test_forget_password_email_code_error (line 192) | def test_forget_password_email_code_error(self):

FILE: accounts/user_login_backend.py
  class EmailOrUsernameModelBackend (line 5) | class EmailOrUsernameModelBackend(ModelBackend):
    method authenticate (line 10) | def authenticate(self, request, username=None, password=None, **kwargs):
    method get_user (line 22) | def get_user(self, username):

FILE: accounts/utils.py
  function send_verify_email (line 13) | def send_verify_email(to_mail: str, code: str, subject: str = _("Verify ...
  function verify (line 26) | def verify(email: str, code: str) -> typing.Optional[str]:
  function set_code (line 42) | def set_code(email: str, code: str):
  function get_code (line 47) | def get_code(email: str) -> typing.Optional[str]:

FILE: accounts/views.py
  class RegisterView (line 30) | class RegisterView(SecureFormView):
    method form_valid (line 39) | def form_valid(self, form):
  class LogoutView (line 80) | class LogoutView(LogoutRedirectView):
    method get (line 88) | def get(self, request, *args, **kwargs):
  class LoginView (line 97) | class LoginView(LoginFormView):
    method get_context_data (line 111) | def get_context_data(self, **kwargs):
    method form_valid (line 119) | def form_valid(self, form):
    method get_success_url (line 152) | def get_success_url(self):
  function account_result (line 162) | def account_result(request):
  class ForgetPasswordView (line 195) | class ForgetPasswordView(SecureFormView):
    method form_valid (line 204) | def form_valid(self, form):
  class ForgetPasswordEmailCode (line 214) | class ForgetPasswordEmailCode(View):
    method post (line 216) | def post(self, request: HttpRequest):

FILE: blog/admin.py
  class ArticleForm (line 12) | class ArticleForm(forms.ModelForm):
    class Meta (line 15) | class Meta:
  function makr_article_publish (line 20) | def makr_article_publish(modeladmin, request, queryset):
  function draft_article (line 24) | def draft_article(modeladmin, request, queryset):
  function close_article_commentstatus (line 28) | def close_article_commentstatus(modeladmin, request, queryset):
  function open_article_commentstatus (line 32) | def open_article_commentstatus(modeladmin, request, queryset):
  class ArticlelAdmin (line 42) | class ArticlelAdmin(admin.ModelAdmin):
    method link_to_category (line 69) | def link_to_category(self, obj):
    method get_form (line 76) | def get_form(self, request, obj=None, **kwargs):
    method save_model (line 82) | def save_model(self, request, obj, form, change):
    method get_view_on_site_url (line 85) | def get_view_on_site_url(self, obj=None):
  class TagAdmin (line 95) | class TagAdmin(admin.ModelAdmin):
  class CategoryAdmin (line 99) | class CategoryAdmin(admin.ModelAdmin):
  class LinksAdmin (line 104) | class LinksAdmin(admin.ModelAdmin):
  class SideBarAdmin (line 108) | class SideBarAdmin(admin.ModelAdmin):
  class BlogSettingsAdmin (line 113) | class BlogSettingsAdmin(admin.ModelAdmin):
    method has_add_permission (line 116) | def has_add_permission(self, request):
    method has_delete_permission (line 120) | def has_delete_permission(self, request, obj=None):
    method changelist_view (line 124) | def changelist_view(self, request, extra_context=None):
    method save_model (line 137) | def save_model(self, request, obj, form, change):

FILE: blog/apps.py
  class BlogConfig (line 7) | class BlogConfig(AppConfig):
    method ready (line 10) | def ready(self):
    method check_search_index (line 15) | def check_search_index(self):

FILE: blog/context_processors.py
  function seo_processor (line 11) | def seo_processor(requests):

FILE: blog/documents.py
  class GeoIp (line 76) | class GeoIp(InnerDoc):
  class UserAgentBrowser (line 83) | class UserAgentBrowser(InnerDoc):
  class UserAgentOS (line 88) | class UserAgentOS(UserAgentBrowser):
  class UserAgentDevice (line 92) | class UserAgentDevice(InnerDoc):
  class UserAgent (line 98) | class UserAgent(InnerDoc):
  class ElapsedTimeDocument (line 106) | class ElapsedTimeDocument(Document):
    class Index (line 114) | class Index:
  class ElaspedTimeDocumentManager (line 122) | class ElaspedTimeDocumentManager:
    method build_index (line 124) | def build_index():
    method delete_index (line 133) | def delete_index():
    method create (line 142) | def create(url, time_taken, log_datetime, useragent, ip):
  class ArticleDocument (line 174) | class ArticleDocument(Document):
    class Index (line 197) | class Index:
  class ArticleDocumentManager (line 205) | class ArticleDocumentManager():
    method __init__ (line 207) | def __init__(self):
    method create_index (line 210) | def create_index(self):
    method delete_index (line 213) | def delete_index(self):
    method convert_to_doc (line 221) | def convert_to_doc(self, articles):
    method rebuild (line 245) | def rebuild(self, articles=None):
    method update_docs (line 252) | def update_docs(self, docs):

FILE: blog/forms.py
  class BlogSearchForm (line 9) | class BlogSearchForm(SearchForm):
    method search (line 12) | def search(self):

FILE: blog/management/commands/build_index.py
  class Command (line 8) | class Command(BaseCommand):
    method handle (line 11) | def handle(self, *args, **options):

FILE: blog/management/commands/build_search_words.py
  class Command (line 7) | class Command(BaseCommand):
    method handle (line 10) | def handle(self, *args, **options):

FILE: blog/management/commands/clear_cache.py
  class Command (line 6) | class Command(BaseCommand):
    method handle (line 9) | def handle(self, *args, **options):

FILE: blog/management/commands/create_testdata.py
  class Command (line 8) | class Command(BaseCommand):
    method handle (line 11) | def handle(self, *args, **options):

FILE: blog/management/commands/ping_baidu.py
  class Command (line 10) | class Command(BaseCommand):
    method add_arguments (line 13) | def add_arguments(self, parser):
    method get_full_url (line 24) | def get_full_url(self, path):
    method handle (line 28) | def handle(self, *args, **options):

FILE: blog/management/commands/sync_user_avatar.py
  class Command (line 10) | class Command(BaseCommand):
    method test_picture (line 13) | def test_picture(self, url):
    method handle (line 20) | def handle(self, *args, **options):

FILE: blog/middleware.py
  class OnlineMiddleware (line 12) | class OnlineMiddleware(object):
    method __init__ (line 13) | def __init__(self, get_response=None):
    method __call__ (line 17) | def __call__(self, request):

FILE: blog/migrations/0001_initial.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: blog/migrations/0002_blogsettings_global_footer_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: blog/migrations/0003_blogsettings_comment_need_review.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: blog/migrations/0006_alter_blogsettings_options.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: blog/migrations/0007_article_idx_type_status_pub_article_idx_status_views_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: blog/migrations/0008_blogsettings_color_scheme.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: blog/models.py
  class LinkShowType (line 21) | class LinkShowType(models.TextChoices):
  class BaseModel (line 29) | class BaseModel(models.Model):
    method save (line 34) | def save(self, *args, **kwargs):
    method get_full_url (line 48) | def get_full_url(self):
    class Meta (line 54) | class Meta:
    method get_absolute_url (line 58) | def get_absolute_url(self):
  class Article (line 62) | class Article(BaseModel):
    method body_to_string (line 109) | def body_to_string(self):
    method __str__ (line 112) | def __str__(self):
    class Meta (line 115) | class Meta:
    method get_absolute_url (line 131) | def get_absolute_url(self):
    method get_category_tree (line 140) | def get_category_tree(self):
    method save (line 146) | def save(self, *args, **kwargs):
    method viewed (line 149) | def viewed(self):
    method comment_list (line 153) | def comment_list(self):
    method get_admin_url (line 165) | def get_admin_url(self):
    method next_article (line 170) | def next_article(self):
    method prev_article (line 176) | def prev_article(self):
    method get_first_image_url (line 180) | def get_first_image_url(self):
  class Category (line 191) | class Category(BaseModel):
    class Meta (line 203) | class Meta:
    method get_absolute_url (line 208) | def get_absolute_url(self):
    method __str__ (line 213) | def __str__(self):
    method get_category_tree (line 217) | def get_category_tree(self):
    method get_sub_categorys (line 233) | def get_sub_categorys(self):
  class Tag (line 254) | class Tag(BaseModel):
    method __str__ (line 259) | def __str__(self):
    method get_absolute_url (line 262) | def get_absolute_url(self):
    method get_article_count (line 266) | def get_article_count(self):
    class Meta (line 269) | class Meta:
  class Links (line 275) | class Links(models.Model):
    class Meta (line 291) | class Meta:
    method __str__ (line 296) | def __str__(self):
  class SideBar (line 300) | class SideBar(models.Model):
    class Meta (line 309) | class Meta:
    method __str__ (line 314) | def __str__(self):
  class BlogSettings (line 318) | class BlogSettings(models.Model):
    class Meta (line 391) | class Meta:
    method __str__ (line 395) | def __str__(self):
    method clean (line 398) | def clean(self):
    method save (line 402) | def save(self, *args, **kwargs):

FILE: blog/search_indexes.py
  class ArticleIndex (line 6) | class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
    method get_model (line 11) | def get_model(self):
    method index_queryset (line 14) | def index_queryset(self, using=None):

FILE: blog/static/account/js/account.js
  function time (line 3) | function time(o) {

FILE: blog/static/blog/js/mathjax-loader.js
  function hasMathFormulas (line 12) | function hasMathFormulas() {
  function configureMathJax (line 21) | function configureMathJax() {
  function loadMathJax (line 82) | function loadMathJax() {
  function init (line 114) | function init() {

FILE: blog/templatetags/blog_tags.py
  function head_meta (line 30) | def head_meta(context):
  function timeformat (line 35) | def timeformat(data):
  function datetimeformat (line 44) | def datetimeformat(data):
  function custom_markdown (line 54) | def custom_markdown(content):
  function sidebar_markdown (line 71) | def sidebar_markdown(content):
  function render_article_content (line 77) | def render_article_content(context, article, is_summary=False):
  function get_markdown_toc (line 131) | def get_markdown_toc(content):
  function current_nav_item (line 138) | def current_nav_item(request):
  function comment_markdown (line 156) | def comment_markdown(content):
  function to_json (line 162) | def to_json(value):
  function get_reactions_for_user (line 174) | def get_reactions_for_user(comment, user):
  function truncatechars_content (line 190) | def truncatechars_content(content):
  function truncate (line 204) | def truncate(content):
  function load_breadcrumb (line 211) | def load_breadcrumb(article):
  function load_articletags (line 232) | def load_articletags(article):
  function load_sidebar (line 252) | def load_sidebar(user, linktype):
  function load_article_metas (line 327) | def load_article_metas(article, user):
  function load_pagination_info (line 340) | def load_pagination_info(page_obj, page_type, tag_name):
  function load_article_detail (line 410) | def load_article_detail(article, isindex, user, query=None):
  function load_article_detail_with_highlight (line 431) | def load_article_detail_with_highlight(article, highlighted, isindex, us...
  function highlight_search_term (line 453) | def highlight_search_term(text, query):
  function highlight_content (line 478) | def highlight_content(html_content, query):
  function gravatar_url (line 521) | def gravatar_url(email, size=40):
  function gravatar (line 556) | def gravatar(email, size=40):
  function query (line 565) | def query(qs, **kwargs):
  function addstr (line 576) | def addstr(arg1, arg2):
  function render_plugin_widgets (line 584) | def render_plugin_widgets(context, position, **kwargs):
  function plugin_critical_head_resources (line 621) | def plugin_critical_head_resources(context):
  function plugin_head_resources (line 645) | def plugin_head_resources(context):
  function plugin_body_resources (line 665) | def plugin_body_resources(context):
  function plugin_compressed_css (line 685) | def plugin_compressed_css():
  function plugin_compressed_js (line 699) | def plugin_compressed_js():
  function plugin_widget (line 715) | def plugin_widget(context, plugin_name, widget_type='default', **kwargs):

FILE: blog/templatetags/vite_tags.py
  function load_manifest (line 26) | def load_manifest():
  function vite_asset (line 63) | def vite_asset(entry_name):
  function vite_js (line 88) | def vite_js(entry_name='src/main.js'):
  function vite_css (line 138) | def vite_css(entry_name='src/styles/main.css'):
  function vite_preload (line 170) | def vite_preload(entry_name):
  function is_vite_dev_mode (line 203) | def is_vite_dev_mode():
  function vite_dev_server_url (line 221) | def vite_dev_server_url():

FILE: blog/test_admin.py
  class ArticleAdminTest (line 14) | class ArticleAdminTest(BaseTestCase, AdminTestMixin):
    method setUp (line 17) | def setUp(self):
    method test_admin_list_display (line 22) | def test_admin_list_display(self):
    method test_admin_search_by_title (line 28) | def test_admin_search_by_title(self):
    method test_admin_filter_by_status (line 36) | def test_admin_filter_by_status(self):
    method test_admin_filter_by_category (line 43) | def test_admin_filter_by_category(self):
    method test_admin_change_article (line 50) | def test_admin_change_article(self):
    method test_save_model_sets_author (line 57) | def test_save_model_sets_author(self):
    method test_get_list_display (line 76) | def test_get_list_display(self):
    method test_formfield_for_foreignkey_author (line 85) | def test_formfield_for_foreignkey_author(self):
  class CategoryAdminTest (line 96) | class CategoryAdminTest(BaseTestCase, AdminTestMixin):
    method test_category_admin_list (line 99) | def test_category_admin_list(self):
    method test_category_admin_search (line 105) | def test_category_admin_search(self):
    method test_category_admin_change (line 113) | def test_category_admin_change(self):
  class TagAdminTest (line 121) | class TagAdminTest(BaseTestCase, AdminTestMixin):
    method test_tag_admin_list (line 124) | def test_tag_admin_list(self):
    method test_tag_admin_search (line 130) | def test_tag_admin_search(self):
    method test_tag_admin_change (line 138) | def test_tag_admin_change(self):

FILE: blog/test_article_business_logic.py
  class ArticleLifecycleTest (line 13) | class ArticleLifecycleTest(TestCase):
    method setUp (line 16) | def setUp(self):
    method test_article_created_as_draft_by_default (line 47) | def test_article_created_as_draft_by_default(self):
    method test_article_draft_to_published_transition (line 59) | def test_article_draft_to_published_transition(self):
    method test_article_published_to_draft_transition (line 82) | def test_article_published_to_draft_transition(self):
    method test_published_article_is_publicly_accessible (line 104) | def test_published_article_is_publicly_accessible(self):
    method test_draft_article_not_in_public_list (line 120) | def test_draft_article_not_in_public_list(self):
    method test_article_views_counter_increases (line 149) | def test_article_views_counter_increases(self):
    method test_article_views_multiple_increments (line 171) | def test_article_views_multiple_increments(self):
  class ArticleCommentStatusTest (line 192) | class ArticleCommentStatusTest(TestCase):
    method setUp (line 195) | def setUp(self):
    method test_article_comment_open_by_default (line 208) | def test_article_comment_open_by_default(self):
    method test_article_comment_can_be_closed (line 222) | def test_article_comment_can_be_closed(self):
    method test_closed_comment_article_status (line 241) | def test_closed_comment_article_status(self):
  class ArticlePermissionTest (line 257) | class ArticlePermissionTest(TestCase):
    method setUp (line 260) | def setUp(self):
    method test_author_is_article_owner (line 294) | def test_author_is_article_owner(self):
    method test_other_user_is_not_article_owner (line 298) | def test_other_user_is_not_article_owner(self):
    method test_admin_has_superuser_privilege (line 302) | def test_admin_has_superuser_privilege(self):
    method test_normal_user_no_staff_privilege (line 307) | def test_normal_user_no_staff_privilege(self):
    method test_article_author_can_edit (line 312) | def test_article_author_can_edit(self):
    method test_other_user_cannot_edit (line 318) | def test_other_user_cannot_edit(self):
    method test_admin_can_edit_any_article (line 324) | def test_admin_can_edit_any_article(self):
  class ArticleTypeTest (line 332) | class ArticleTypeTest(TestCase):
    method setUp (line 335) | def setUp(self):
    method test_article_type_is_article (line 348) | def test_article_type_is_article(self):
    method test_article_type_is_page (line 361) | def test_article_type_is_page(self):
    method test_articles_and_pages_are_separate (line 374) | def test_articles_and_pages_are_separate(self):
  class ArticleCategoryTagTest (line 407) | class ArticleCategoryTagTest(TestCase):
    method setUp (line 410) | def setUp(self):
    method test_article_belongs_to_category (line 423) | def test_article_belongs_to_category(self):
    method test_category_has_articles (line 436) | def test_category_has_articles(self):
    method test_article_can_change_category (line 466) | def test_article_can_change_category(self):
  class ArticleTimestampTest (line 490) | class ArticleTimestampTest(TestCase):
    method setUp (line 493) | def setUp(self):
    method test_article_has_creation_time (line 506) | def test_article_has_creation_time(self):
    method test_article_has_last_mod_time (line 522) | def test_article_has_last_mod_time(self):
    method test_article_last_mod_time_updates (line 535) | def test_article_last_mod_time_updates(self):
  class ArticleSlugTest (line 561) | class ArticleSlugTest(TestCase):
    method setUp (line 564) | def setUp(self):
    method test_article_has_id (line 577) | def test_article_has_id(self):
    method test_article_absolute_url (line 593) | def test_article_absolute_url(self):

FILE: blog/test_context_processors.py
  class SeoProcessorTest (line 15) | class SeoProcessorTest(TestCase):
    method setUp (line 18) | def setUp(self):
    method tearDown (line 48) | def tearDown(self):
    method test_processor_returns_required_variables (line 52) | def test_processor_returns_required_variables(self):
    method test_processor_caching (line 84) | def test_processor_caching(self):
    method test_processor_with_anonymous_user (line 103) | def test_processor_with_anonymous_user(self):
    method test_processor_with_https_request (line 112) | def test_processor_with_https_request(self):
    method test_processor_with_http_request (line 120) | def test_processor_with_http_request(self):
    method test_processor_current_year (line 128) | def test_processor_current_year(self):
    method test_processor_nav_category_list (line 137) | def test_processor_nav_category_list(self):
    method test_processor_nav_pages (line 150) | def test_processor_nav_pages(self):
    method test_processor_only_shows_published_pages (line 163) | def test_processor_only_shows_published_pages(self):
    method test_processor_cache_expiration (line 187) | def test_processor_cache_expiration(self):
    method test_processor_with_custom_blog_settings (line 205) | def test_processor_with_custom_blog_settings(self, mock_get_blog_setti...
    method test_processor_site_base_url_with_different_hosts (line 242) | def test_processor_site_base_url_with_different_hosts(self):
    method test_processor_dynamic_values_not_cached (line 253) | def test_processor_dynamic_values_not_cached(self):
    method test_processor_handles_empty_settings (line 270) | def test_processor_handles_empty_settings(self):
    method test_processor_logs_cache_miss (line 283) | def test_processor_logs_cache_miss(self, mock_logger):
    method test_processor_multiple_categories (line 297) | def test_processor_multiple_categories(self):
    method test_processor_multiple_pages (line 313) | def test_processor_multiple_pages(self):
    method test_processor_excludes_articles_from_nav_pages (line 343) | def test_processor_excludes_articles_from_nav_pages(self):
    method test_processor_color_scheme_setting (line 367) | def test_processor_color_scheme_setting(self):

FILE: blog/test_middleware.py
  class OnlineMiddlewareTest (line 14) | class OnlineMiddlewareTest(TestCase):
    method setUp (line 17) | def setUp(self):
    method get_response (line 22) | def get_response(self, request):
    method test_middleware_initialization (line 27) | def test_middleware_initialization(self):
    method test_middleware_processes_request_normally (line 33) | def test_middleware_processes_request_normally(self):
    method test_middleware_calculates_page_render_time (line 45) | def test_middleware_calculates_page_render_time(self):
    method test_middleware_handles_streaming_response (line 65) | def test_middleware_handles_streaming_response(self):
    method test_middleware_handles_missing_user_agent (line 85) | def test_middleware_handles_missing_user_agent(self):
    method test_middleware_elasticsearch_integration_enabled (line 97) | def test_middleware_elasticsearch_integration_enabled(self, mock_create):
    method test_middleware_elasticsearch_integration_disabled (line 117) | def test_middleware_elasticsearch_integration_disabled(self, mock_crea...
    method test_middleware_ip_detection (line 130) | def test_middleware_ip_detection(self, mock_get_client_ip):
    method test_middleware_user_agent_parsing (line 143) | def test_middleware_user_agent_parsing(self):
    method test_middleware_handles_elasticsearch_exception (line 160) | def test_middleware_handles_elasticsearch_exception(self, mock_create):
    method test_middleware_handles_content_replace_exception (line 177) | def test_middleware_handles_content_replace_exception(self):
    method test_middleware_time_format (line 196) | def test_middleware_time_format(self):
    method test_middleware_logs_exceptions (line 214) | def test_middleware_logs_exceptions(self, mock_create, mock_logger):
    method test_middleware_with_multiple_requests (line 231) | def test_middleware_with_multiple_requests(self):
    method test_middleware_preserves_response_headers (line 245) | def test_middleware_preserves_response_headers(self):
    method test_middleware_uses_correct_timezone (line 266) | def test_middleware_uses_correct_timezone(self, mock_now, mock_create):

FILE: blog/test_templatetags.py
  class BlogTagsTest (line 14) | class BlogTagsTest(BaseTestCase):
    method setUp (line 17) | def setUp(self):
    method test_load_articletags (line 21) | def test_load_articletags(self):
    method test_load_pagination_info (line 31) | def test_load_pagination_info(self):
    method test_load_pagination_info_last_page (line 46) | def test_load_pagination_info_last_page(self):
    method test_highlight_search_term (line 58) | def test_highlight_search_term(self):
    method test_highlight_search_term_no_match (line 65) | def test_highlight_search_term_no_match(self):
    method test_highlight_search_term_empty_query (line 71) | def test_highlight_search_term_empty_query(self):
    method test_highlight_content (line 77) | def test_highlight_content(self):
    method test_custom_markdown (line 83) | def test_custom_markdown(self):
    method test_article_body_rendering (line 90) | def test_article_body_rendering(self):
  class ViteTagsTest (line 99) | class ViteTagsTest(BaseTestCase):
    method test_vite_module_exists (line 102) | def test_vite_module_exists(self):
  class CustomFiltersTest (line 109) | class CustomFiltersTest(BaseTestCase):
    method test_date_format_filter (line 112) | def test_date_format_filter(self):
    method test_url_encode_filter (line 122) | def test_url_encode_filter(self):
    method test_strip_tags_filter (line 129) | def test_strip_tags_filter(self):
  class SidebarTagsTest (line 140) | class SidebarTagsTest(BaseTestCase):
    method test_sidebar_data_exists (line 143) | def test_sidebar_data_exists(self):
  class CacheTagsTest (line 151) | class CacheTagsTest(BaseTestCase):
    method test_cache_operations (line 154) | def test_cache_operations(self):
  class MarkdownExtensionsTest (line 167) | class MarkdownExtensionsTest(BaseTestCase):
    method test_code_highlight (line 170) | def test_code_highlight(self):
    method test_table_support (line 179) | def test_table_support(self):
    method test_auto_link (line 189) | def test_auto_link(self):
    method test_strikethrough (line 196) | def test_strikethrough(self):

FILE: blog/test_views.py
  class ArticleViewTest (line 11) | class ArticleViewTest(BaseTestCase, ViewTestMixin):
    method test_article_detail_view (line 14) | def test_article_detail_view(self):
    method test_article_detail_view_draft (line 21) | def test_article_detail_view_draft(self):
    method test_article_detail_increases_views (line 29) | def test_article_detail_increases_views(self):
    method test_article_archive_view (line 36) | def test_article_archive_view(self):
    method test_article_archive_by_year (line 41) | def test_article_archive_by_year(self):
    method test_article_archive_by_year_month (line 53) | def test_article_archive_by_year_month(self):
    method test_index_view (line 64) | def test_index_view(self):
    method test_index_view_pagination (line 70) | def test_index_view_pagination(self):
    method test_category_view (line 80) | def test_category_view(self):
    method test_category_view_invalid_slug (line 86) | def test_category_view_invalid_slug(self):
    method test_tag_view (line 92) | def test_tag_view(self):
    method test_tag_view_invalid_slug (line 99) | def test_tag_view_invalid_slug(self):
    method test_author_view (line 105) | def test_author_view(self):
  class SearchViewTest (line 112) | class SearchViewTest(BaseTestCase, ViewTestMixin):
    method test_search_view_accessible (line 115) | def test_search_view_accessible(self):
  class ArticlePermissionTest (line 127) | class ArticlePermissionTest(BaseTestCase, ViewTestMixin):
    method test_only_author_can_edit (line 130) | def test_only_author_can_edit(self):
    method test_article_status_visibility (line 141) | def test_article_status_visibility(self):
  class ErrorHandlingTest (line 154) | class ErrorHandlingTest(BaseTestCase, ViewTestMixin):
    method test_404_page (line 157) | def test_404_page(self):
    method test_article_404 (line 162) | def test_article_404(self):
    method test_invalid_page_number (line 172) | def test_invalid_page_number(self):
    method test_page_out_of_range (line 179) | def test_page_out_of_range(self):

FILE: blog/tests.py
  class ArticleTest (line 22) | class ArticleTest(TestCase):
    method setUp (line 23) | def setUp(self):
    method test_validate_article (line 27) | def test_validate_article(self):
    method check_pagination (line 151) | def check_pagination(self, p, type, value):
    method test_image (line 162) | def test_image(self):
    method test_errorpage (line 185) | def test_errorpage(self):
    method test_commands (line 189) | def test_commands(self):
  class SearchHighlightTest (line 235) | class SearchHighlightTest(TestCase):
    method test_highlight_search_term_filter (line 238) | def test_highlight_search_term_filter(self):
    method test_highlight_content_filter (line 266) | def test_highlight_content_filter(self):
    method test_search_page_access (line 286) | def test_search_page_access(self):
    method test_chinese_keyword_highlight (line 296) | def test_chinese_keyword_highlight(self):

FILE: blog/views.py
  class ArticleListView (line 34) | class ArticleListView(CachedListViewMixin, PageNumberMixin, ListView):
    method get_view_cache_key (line 53) | def get_view_cache_key(self):
    method get_context_data (line 56) | def get_context_data(self, **kwargs):
  class IndexView (line 61) | class IndexView(OptimizedArticleQueryMixin, ArticleListView):
    method get_queryset_data (line 70) | def get_queryset_data(self):
    method get_queryset_cache_key (line 76) | def get_queryset_cache_key(self):
    method get_context_data (line 79) | def get_context_data(self, **kwargs):
  class ArticleDetailView (line 89) | class ArticleDetailView(DetailView):
    method get_context_data (line 98) | def get_context_data(self, **kwargs):
  class CategoryDetailView (line 177) | class CategoryDetailView(SlugCachedMixin, OptimizedArticleQueryMixin, Ar...
    method get_queryset_data (line 188) | def get_queryset_data(self):
    method get_queryset_cache_key (line 197) | def get_queryset_cache_key(self):
    method get_context_data (line 202) | def get_context_data(self, **kwargs):
  class AuthorDetailView (line 224) | class AuthorDetailView(OptimizedArticleQueryMixin, ArticleListView):
    method get_queryset_cache_key (line 232) | def get_queryset_cache_key(self):
    method get_queryset_data (line 237) | def get_queryset_data(self):
    method get_context_data (line 243) | def get_context_data(self, **kwargs):
  class TagDetailView (line 258) | class TagDetailView(SlugCachedMixin, OptimizedArticleQueryMixin, Article...
    method get_queryset_data (line 269) | def get_queryset_data(self):
    method get_queryset_cache_key (line 276) | def get_queryset_cache_key(self):
    method get_context_data (line 281) | def get_context_data(self, **kwargs):
  class ArchivesView (line 296) | class ArchivesView(OptimizedArticleQueryMixin, ArticleListView):
    method get_queryset_data (line 307) | def get_queryset_data(self):
    method get_queryset_cache_key (line 310) | def get_queryset_cache_key(self):
  class LinkListView (line 314) | class LinkListView(ListView):
    method get_queryset (line 318) | def get_queryset(self):
  class EsSearchView (line 322) | class EsSearchView(SearchView):
    method build_form (line 323) | def build_form(self, form_kwargs=None):
    method get_context (line 338) | def get_context(self):
  function fileupload (line 355) | def fileupload(request):
  function clean_cache_view (line 406) | def clean_cache_view(request):

FILE: comments/admin.py
  function disable_commentstatus (line 9) | def disable_commentstatus(modeladmin, request, queryset):
  function enable_commentstatus (line 13) | def enable_commentstatus(modeladmin, request, queryset):
  class CommentAdmin (line 21) | class CommentAdmin(admin.ModelAdmin):
    method link_to_userinfo (line 37) | def link_to_userinfo(self, obj):
    method link_to_article (line 44) | def link_to_article(self, obj):
  class CommentReactionAdmin (line 54) | class CommentReactionAdmin(admin.ModelAdmin):
    method link_to_comment (line 62) | def link_to_comment(self, obj):
    method link_to_user (line 68) | def link_to_user(self, obj):

FILE: comments/apps.py
  class CommentsConfig (line 4) | class CommentsConfig(AppConfig):

FILE: comments/forms.py
  class CommentForm (line 7) | class CommentForm(ModelForm):
    class Meta (line 11) | class Meta:

FILE: comments/migrations/0001_initial.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: comments/migrations/0002_alter_comment_is_enable.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: comments/migrations/0004_comment_idx_art_parent_enable_comment_idx_enable_id.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: comments/migrations/0005_commentreaction.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: comments/models.py
  class Comment (line 11) | class Comment(models.Model):
    class Meta (line 32) | class Meta:
    method __str__ (line 44) | def __str__(self):
    method get_reactions_summary (line 47) | def get_reactions_summary(self, user=None):
  class CommentReaction (line 94) | class CommentReaction(models.Model):
    class Meta (line 127) | class Meta:
    method __str__ (line 136) | def __str__(self):

FILE: comments/templatetags/comments_tags.py
  function parse_commenttree (line 7) | def parse_commenttree(commentlist, comment):
  function show_comment_item (line 24) | def show_comment_item(comment, ischild):

FILE: comments/test_comment_business_logic.py
  class CommentCreationTest (line 13) | class CommentCreationTest(TestCase):
    method setUp (line 16) | def setUp(self):
    method test_comment_created_with_required_fields (line 45) | def test_comment_created_with_required_fields(self):
    method test_comment_has_creation_time (line 58) | def test_comment_has_creation_time(self):
    method test_comment_author_is_correct (line 71) | def test_comment_author_is_correct(self):
    method test_comment_article_relationship (line 81) | def test_comment_article_relationship(self):
  class CommentModerationTest (line 95) | class CommentModerationTest(TestCase):
    method setUp (line 98) | def setUp(self):
    method test_comment_pending_by_default_when_review_required (line 133) | def test_comment_pending_by_default_when_review_required(self):
    method test_comment_approved_directly_when_no_review_required (line 148) | def test_comment_approved_directly_when_no_review_required(self):
    method test_comment_can_be_approved (line 163) | def test_comment_can_be_approved(self):
    method test_comment_can_be_rejected (line 179) | def test_comment_can_be_rejected(self):
    method test_only_approved_comments_in_public_list (line 195) | def test_only_approved_comments_in_public_list(self):
  class CommentReplyTest (line 223) | class CommentReplyTest(TestCase):
    method setUp (line 226) | def setUp(self):
    method test_comment_can_have_no_parent (line 261) | def test_comment_can_have_no_parent(self):
    method test_comment_can_have_parent (line 272) | def test_comment_can_have_parent(self):
    method test_parent_comment_has_replies (line 291) | def test_parent_comment_has_replies(self):
    method test_nested_comment_structure (line 323) | def test_nested_comment_structure(self):
    method test_multiple_replies_to_same_comment (line 356) | def test_multiple_replies_to_same_comment(self):
  class CommentArticleStatusTest (line 385) | class CommentArticleStatusTest(TestCase):
    method setUp (line 388) | def setUp(self):
    method test_can_comment_on_open_comment_article (line 407) | def test_can_comment_on_open_comment_article(self):
    method test_comment_status_closed_validation (line 432) | def test_comment_status_closed_validation(self):
    method test_comments_belong_to_correct_article (line 450) | def test_comments_belong_to_correct_article(self):
  class CommentQueryTest (line 495) | class CommentQueryTest(TestCase):
    method setUp (line 498) | def setUp(self):
    method test_query_comments_by_article (line 526) | def test_query_comments_by_article(self):
    method test_query_comments_by_author (line 540) | def test_query_comments_by_author(self):
    method test_query_root_comments_only (line 554) | def test_query_root_comments_only(self):
    method test_comment_ordering (line 590) | def test_comment_ordering(self):
  class CommentDeletionTest (line 610) | class CommentDeletionTest(TestCase):
    method setUp (line 613) | def setUp(self):
    method test_comment_can_be_deleted (line 641) | def test_comment_can_be_deleted(self):
    method test_deleting_parent_comment_with_replies (line 659) | def test_deleting_parent_comment_with_replies(self):

FILE: comments/test_views.py
  class CommentViewTest (line 12) | class CommentViewTest(TransactionTestCase, ViewTestMixin):
    method setUp (line 15) | def setUp(self):
    method login_user (line 49) | def login_user(self):
    method test_post_comment_authenticated (line 53) | def test_post_comment_authenticated(self):
    method test_post_comment_unauthenticated (line 64) | def test_post_comment_unauthenticated(self):
    method test_post_comment_empty_body (line 73) | def test_post_comment_empty_body(self):
    method test_post_comment_invalid_article (line 81) | def test_post_comment_invalid_article(self):
    method test_post_reply_comment (line 88) | def test_post_reply_comment(self):
    method test_comment_moderation (line 108) | def test_comment_moderation(self):
    method test_comment_display_on_article (line 123) | def test_comment_display_on_article(self):
    method test_disabled_comment_not_display (line 135) | def test_disabled_comment_not_display(self):
  class CommentSpamTest (line 148) | class CommentSpamTest(TransactionTestCase, ViewTestMixin):
    method setUp (line 151) | def setUp(self):
    method login_user (line 181) | def login_user(self):
    method test_duplicate_comment (line 184) | def test_duplicate_comment(self):
    method test_comment_rate_limit (line 199) | def test_comment_rate_limit(self):
  class CommentSecurityTest (line 211) | class CommentSecurityTest(TransactionTestCase, ViewTestMixin):
    method setUp (line 214) | def setUp(self):
    method login_user (line 244) | def login_user(self):
    method test_xss_protection (line 247) | def test_xss_protection(self):
    method test_sql_injection_protection (line 261) | def test_sql_injection_protection(self):

FILE: comments/tests.py
  class CommentsTest (line 13) | class CommentsTest(TransactionTestCase):
    method setUp (line 14) | def setUp(self):
    method update_article_comment_status (line 27) | def update_article_comment_status(self, article):
    method test_validate_comment (line 33) | def test_validate_comment(self):

FILE: comments/utils.py
  function send_comment_email (line 11) | def send_comment_email(comment):

FILE: comments/views.py
  class CommentPostView (line 14) | class CommentPostView(AuthenticatedFormView):
    method get (line 25) | def get(self, request, *args, **kwargs):
    method form_invalid (line 31) | def form_invalid(self, form):
    method form_valid (line 40) | def form_valid(self, form):
  class CommentReactionView (line 68) | class CommentReactionView(View):
    method get (line 75) | def get(self, request, comment_id):
    method post (line 88) | def post(self, request, comment_id):

FILE: djangoblog/admin_site.py
  class DjangoBlogAdminSite (line 20) | class DjangoBlogAdminSite(AdminSite):
    method __init__ (line 24) | def __init__(self, name='admin'):
    method has_permission (line 27) | def has_permission(self, request):

FILE: djangoblog/apps.py
  class DjangoblogAppConfig (line 3) | class DjangoblogAppConfig(AppConfig):
    method ready (line 7) | def ready(self):

FILE: djangoblog/base_views.py
  class SecureFormView (line 17) | class SecureFormView(FormView):
    method dispatch (line 34) | def dispatch(self, *args, **kwargs):
  class AuthenticatedFormView (line 39) | class AuthenticatedFormView(FormView):
    method dispatch (line 54) | def dispatch(self, *args, **kwargs):
  class LoginFormView (line 59) | class LoginFormView(FormView):
    method dispatch (line 81) | def dispatch(self, request, *args, **kwargs):
  class LogoutRedirectView (line 86) | class LogoutRedirectView(RedirectView):
    method dispatch (line 102) | def dispatch(self, request, *args, **kwargs):
  class NoCacheFormView (line 107) | class NoCacheFormView(FormView):
    method dispatch (line 121) | def dispatch(self, request, *args, **kwargs):

FILE: djangoblog/blog_signals.py
  function send_email_signal_handler (line 27) | def send_email_signal_handler(sender, **kwargs):
  function oauth_user_login_signal_handler (line 55) | def oauth_user_login_signal_handler(sender, **kwargs):
  function model_post_save_callback (line 68) | def model_post_save_callback(
  function user_auth_callback (line 174) | def user_auth_callback(sender, request, user, **kwargs):

FILE: djangoblog/constants.py
  class CacheTimeout (line 11) | class CacheTimeout:
  class CacheKey (line 37) | class CacheKey:
  class HttpStatus (line 72) | class HttpStatus:
  class Pagination (line 89) | class Pagination:

FILE: djangoblog/elasticsearch_backend.py
  class ElasticSearchBackend (line 14) | class ElasticSearchBackend(BaseSearchBackend):
    method __init__ (line 15) | def __init__(self, connection_alias, **connection_options):
    method _get_models (line 24) | def _get_models(self, iterable):
    method _create (line 29) | def _create(self, models):
    method _delete (line 34) | def _delete(self, models):
    method _rebuild (line 39) | def _rebuild(self, models):
    method update (line 44) | def update(self, index, iterable, commit=True):
    method remove (line 49) | def remove(self, obj_or_string):
    method clear (line 53) | def clear(self, models=None, commit=True):
    method get_suggestion (line 57) | def get_suggestion(query: str) -> str:
    method search (line 75) | def search(self, query_string, **kwargs):
  class ElasticSearchQuery (line 148) | class ElasticSearchQuery(BaseSearchQuery):
    method _convert_datetime (line 149) | def _convert_datetime(self, date):
    method clean (line 155) | def clean(self, query_fragment):
    method build_query_fragment (line 180) | def build_query_fragment(self, field, filter_type, value):
    method get_count (line 183) | def get_count(self):
    method get_spelling_suggestion (line 187) | def get_spelling_suggestion(self, preferred_query=None):
    method build_params (line 190) | def build_params(self, spelling_query=None):
  class ElasticSearchModelSearchForm (line 195) | class ElasticSearchModelSearchForm(ModelSearchForm):
    method search (line 197) | def search(self):
  class ElasticSearchEngine (line 204) | class ElasticSearchEngine(BaseEngine):

FILE: djangoblog/error_views.py
  function render_error_page (line 16) | def render_error_page(request, status_code, message, exception=None):
  function page_not_found_view (line 56) | def page_not_found_view(request, exception, template_name='blog/error_pa...
  function server_error_view (line 78) | def server_error_view(request, template_name='blog/error_page.html'):
  function permission_denied_view (line 98) | def permission_denied_view(request, exception, template_name='blog/error...
  function bad_request_view (line 120) | def bad_request_view(request, exception, template_name='blog/error_page....

FILE: djangoblog/feeds.py
  class DjangoBlogFeed (line 10) | class DjangoBlogFeed(Feed):
    method author_name (line 17) | def author_name(self):
    method author_link (line 20) | def author_link(self):
    method items (line 23) | def items(self):
    method item_title (line 26) | def item_title(self, item):
    method item_description (line 29) | def item_description(self, item):
    method feed_copyright (line 32) | def feed_copyright(self):
    method item_link (line 36) | def item_link(self, item):
    method item_guid (line 39) | def item_guid(self, item):

FILE: djangoblog/logentryadmin.py
  class LogEntryAdmin (line 11) | class LogEntryAdmin(admin.ModelAdmin):
    method has_add_permission (line 33) | def has_add_permission(self, request):
    method has_change_permission (line 36) | def has_change_permission(self, request, obj=None):
    method has_delete_permission (line 42) | def has_delete_permission(self, request, obj=None):
    method object_link (line 45) | def object_link(self, obj):
    method user_link (line 65) | def user_link(self, obj):
    method get_queryset (line 83) | def get_queryset(self, request):
    method get_actions (line 87) | def get_actions(self, request):

FILE: djangoblog/mixins.py
  class TimeStampedModel (line 20) | class TimeStampedModel(models.Model):
    class Meta (line 43) | class Meta:
    method save (line 48) | def save(self, *args, **kwargs):
  class SlugCachedMixin (line 70) | class SlugCachedMixin:
    method get_slug_object (line 93) | def get_slug_object(self):
  class OptimizedArticleQueryMixin (line 118) | class OptimizedArticleQueryMixin:
    method get_optimized_article_queryset (line 131) | def get_optimized_article_queryset(self):
  class CachedListViewMixin (line 155) | class CachedListViewMixin:
    method get_queryset_cache_key (line 171) | def get_queryset_cache_key(self):
    method get_queryset_data (line 185) | def get_queryset_data(self):
    method get_queryset_from_cache (line 199) | def get_queryset_from_cache(self, cache_key):
    method get_queryset (line 221) | def get_queryset(self):
  class PageNumberMixin (line 232) | class PageNumberMixin:
    method page_number (line 246) | def page_number(self):
  class ArticleListMixin (line 264) | class ArticleListMixin(

FILE: djangoblog/plugin_manage/base_plugin.py
  class BasePlugin (line 10) | class BasePlugin:
    method __init__ (line 22) | def __init__(self):
    method _get_plugin_directory (line 33) | def _get_plugin_directory(self):
    method _get_plugin_slug (line 39) | def _get_plugin_slug(self):
    method init_plugin (line 43) | def init_plugin(self):
    method register_hooks (line 50) | def register_hooks(self):
    method render_position_widget (line 58) | def render_position_widget(self, position, context, **kwargs):
    method should_display (line 91) | def should_display(self, position, context, **kwargs):
    method render_sidebar_widget (line 107) | def render_sidebar_widget(self, context, **kwargs):
    method render_article_bottom_widget (line 111) | def render_article_bottom_widget(self, context, **kwargs):
    method render_article_top_widget (line 115) | def render_article_top_widget(self, context, **kwargs):
    method render_header_widget (line 119) | def render_header_widget(self, context, **kwargs):
    method render_footer_widget (line 123) | def render_footer_widget(self, context, **kwargs):
    method render_comment_before_widget (line 127) | def render_comment_before_widget(self, context, **kwargs):
    method render_comment_after_widget (line 131) | def render_comment_after_widget(self, context, **kwargs):
    method render_template (line 136) | def render_template(self, template_name, context=None):
    method get_static_url (line 159) | def get_static_url(self, static_file):
    method get_css_files (line 164) | def get_css_files(self):
    method get_js_files (line 168) | def get_js_files(self):
    method get_critical_head_html (line 172) | def get_critical_head_html(self, context=None):
    method get_head_html (line 184) | def get_head_html(self, context=None):
    method get_body_html (line 188) | def get_body_html(self, context=None):
    method get_plugin_info (line 192) | def get_plugin_info(self):

FILE: djangoblog/plugin_manage/hooks.py
  function register (line 8) | def register(hook_name: str, callback: callable):
  function run_action (line 18) | def run_action(hook_name: str, *args, **kwargs):
  function apply_filters (line 32) | def apply_filters(hook_name: str, value, *args, **kwargs):

FILE: djangoblog/plugin_manage/loader.py
  function load_plugins (line 10) | def load_plugins():
  function get_loaded_plugins (line 42) | def get_loaded_plugins():
  function get_plugin_by_name (line 46) | def get_plugin_by_name(plugin_name):
  function get_plugin_by_slug (line 53) | def get_plugin_by_slug(plugin_slug):
  function get_plugins_info (line 60) | def get_plugins_info():
  function get_plugins_by_position (line 64) | def get_plugins_by_position(position):

FILE: djangoblog/settings.py
  function env_to_bool (line 19) | def env_to_bool(env, default):

FILE: djangoblog/sitemap.py
  class StaticViewSitemap (line 7) | class StaticViewSitemap(Sitemap):
    method items (line 11) | def items(self):
    method location (line 14) | def location(self, item):
  class ArticleSiteMap (line 18) | class ArticleSiteMap(Sitemap):
    method items (line 22) | def items(self):
    method lastmod (line 25) | def lastmod(self, obj):
  class CategorySiteMap (line 29) | class CategorySiteMap(Sitemap):
    method items (line 33) | def items(self):
    method lastmod (line 36) | def lastmod(self, obj):
  class TagSiteMap (line 40) | class TagSiteMap(Sitemap):
    method items (line 44) | def items(self):
    method lastmod (line 47) | def lastmod(self, obj):
  class UserSiteMap (line 51) | class UserSiteMap(Sitemap):
    method items (line 55) | def items(self):
    method lastmod (line 58) | def lastmod(self, obj):

FILE: djangoblog/spider_notify.py
  class SpiderNotify (line 9) | class SpiderNotify():
    method baidu_notify (line 11) | def baidu_notify(urls):
    method notify (line 20) | def notify(url):

FILE: djangoblog/test_base.py
  class BaseTestCase (line 14) | class BaseTestCase(TestCase):
    method setUp (line 20) | def setUp(self):
    method create_user (line 30) | def create_user(self, username='testuser', email='test@test.com', pass...
    method create_admin_user (line 39) | def create_admin_user(self, username='admin', email='admin@admin.com',...
    method create_staff_user (line 48) | def create_staff_user(self, username='staff', email='staff@test.com', ...
    method create_category (line 58) | def create_category(self, name='测试分类'):
    method create_tag (line 67) | def create_tag(self, name='测试标签'):
    method create_article (line 72) | def create_article(self, title='测试文章', body='测试内容', author=None,
    method create_comment (line 90) | def create_comment(self, article=None, author=None, body='测试评论', paren...
    method create_blog_settings (line 105) | def create_blog_settings(self):
    method login_user (line 117) | def login_user(self, user=None, password='testpass123'):
    method login_admin (line 123) | def login_admin(self):
    method assert_redirect_to_login (line 127) | def assert_redirect_to_login(self, response):
    method assert_permission_denied (line 132) | def assert_permission_denied(self, response):
  class ViewTestMixin (line 137) | class ViewTestMixin:
    method assert_view_success (line 143) | def assert_view_success(self, url, status_code=200):
    method assert_view_redirect (line 149) | def assert_view_redirect(self, url, redirect_url=None):
    method assert_view_forbidden (line 157) | def assert_view_forbidden(self, url):
    method assert_post_success (line 163) | def assert_post_success(self, url, data, status_code=200):
    method assert_ajax_success (line 169) | def assert_ajax_success(self, url, data=None):
  class AdminTestMixin (line 180) | class AdminTestMixin:
    method get_admin_url (line 186) | def get_admin_url(self, model, action='changelist'):
    method get_admin_change_url (line 193) | def get_admin_change_url(self, obj):
    method assert_admin_accessible (line 200) | def assert_admin_accessible(self, model):
    method assert_admin_forbidden_for_user (line 207) | def assert_admin_forbidden_for_user(self, model):
  class PluginTestMixin (line 215) | class PluginTestMixin:
    method create_plugin_context (line 221) | def create_plugin_context(self, **kwargs):
    method assert_plugin_hook_registered (line 234) | def assert_plugin_hook_registered(self, plugin, hook_name):
    method mock_plugin_config (line 240) | def mock_plugin_config(self, plugin_name, **config):
  class MockExternalService (line 247) | class MockExternalService:
    method mock_http_response (line 254) | def mock_http_response(status_code=200, content='', json_data=None):
    method mock_elasticsearch_response (line 265) | def mock_elasticsearch_response(hits=None):
    method mock_cache_get (line 275) | def mock_cache_get(return_value=None):
    method mock_cache_set (line 281) | def mock_cache_set():

FILE: djangoblog/test_email_integration.py
  class UserRegistrationEmailTest (line 21) | class UserRegistrationEmailTest(TestCase):
    method setUp (line 24) | def setUp(self):
    method test_user_registration_sends_verification_email (line 36) | def test_user_registration_sends_verification_email(self):
    method test_registration_email_contains_verification_link (line 68) | def test_registration_email_contains_verification_link(self):
    method test_multiple_registrations_send_separate_emails (line 95) | def test_multiple_registrations_send_separate_emails(self):
  class PasswordResetEmailTest (line 127) | class PasswordResetEmailTest(TestCase):
    method setUp (line 130) | def setUp(self):
    method test_forgot_password_sends_reset_email (line 148) | def test_forgot_password_sends_reset_email(self):
    method test_reset_email_contains_verification_code (line 185) | def test_reset_email_contains_verification_code(self):
    method test_reset_email_not_sent_for_nonexistent_email (line 205) | def test_reset_email_not_sent_for_nonexistent_email(self):
  class CommentNotificationEmailTest (line 230) | class CommentNotificationEmailTest(TestCase):
    method setUp (line 233) | def setUp(self):
    method test_comment_sends_notification_to_author (line 278) | def test_comment_sends_notification_to_author(self):
    method test_reply_comment_sends_notification_to_parent_author (line 312) | def test_reply_comment_sends_notification_to_parent_author(self):
    method test_comment_with_review_required_does_not_send_immediate_notification (line 363) | def test_comment_with_review_required_does_not_send_immediate_notifica...
  class EmailIntegrationWorkflowTest (line 405) | class EmailIntegrationWorkflowTest(TestCase):
    method setUp (line 408) | def setUp(self):
    method test_complete_user_journey_with_emails (line 422) | def test_complete_user_journey_with_emails(self):
    method test_email_sending_does_not_block_operations (line 491) | def test_email_sending_does_not_block_operations(self):
  class EmailContentValidationTest (line 524) | class EmailContentValidationTest(TestCase):
    method setUp (line 527) | def setUp(self):
    method test_email_has_proper_from_address (line 537) | def test_email_has_proper_from_address(self):
    method test_email_has_subject (line 564) | def test_email_has_subject(self):
    method test_email_has_body (line 585) | def test_email_has_body(self):
    method test_email_recipient_is_correct (line 606) | def test_email_recipient_is_correct(self):

FILE: djangoblog/test_email_integration_complete.py
  class CompleteUserRegistrationFlowTest (line 21) | class CompleteUserRegistrationFlowTest(TestCase):
    method setUp (line 24) | def setUp(self):
    method test_complete_registration_verification_flow (line 34) | def test_complete_registration_verification_flow(self):
    method test_registration_with_duplicate_email_fails (line 119) | def test_registration_with_duplicate_email_fails(self):
  class CompletePasswordResetFlowTest (line 160) | class CompletePasswordResetFlowTest(TestCase):
    method setUp (line 163) | def setUp(self):
    method test_complete_password_reset_flow (line 180) | def test_complete_password_reset_flow(self):
    method test_password_reset_with_invalid_code_fails (line 243) | def test_password_reset_with_invalid_code_fails(self):
  class CompleteCommentNotificationFlowTest (line 279) | class CompleteCommentNotificationFlowTest(TestCase):
    method setUp (line 282) | def setUp(self):
    method test_complete_comment_notification_flow (line 325) | def test_complete_comment_notification_flow(self):
    method test_comment_notification_respects_review_setting (line 430) | def test_comment_notification_respects_review_setting(self):
  class CompleteUserJourneyIntegrationTest (line 487) | class CompleteUserJourneyIntegrationTest(TestCase):
    method setUp (line 490) | def setUp(self):
    method test_complete_user_lifecycle (line 503) | def test_complete_user_lifecycle(self):

FILE: djangoblog/test_plugins.py
  class PluginHooksTest (line 20) | class PluginHooksTest(TestCase, PluginTestMixin):
    method setUp (line 23) | def setUp(self):
    method test_register_hook (line 27) | def test_register_hook(self):
    method test_apply_filters (line 36) | def test_apply_filters(self):
    method test_run_action (line 45) | def test_run_action(self):
    method test_multiple_hooks (line 56) | def test_multiple_hooks(self):
    method test_hook_error_handling (line 75) | def test_hook_error_handling(self):
  class BasePluginTest (line 92) | class BasePluginTest(BaseTestCase):
    method _create_test_plugin (line 95) | def _create_test_plugin(self):
    method test_plugin_initialization (line 105) | def test_plugin_initialization(self):
    method test_plugin_config (line 111) | def test_plugin_config(self):
    method test_plugin_register_hooks (line 117) | def test_plugin_register_hooks(self):
    method test_plugin_get_context (line 123) | def test_plugin_get_context(self):
  class PluginLoaderTest (line 131) | class PluginLoaderTest(TestCase):
    method test_load_plugins (line 135) | def test_load_plugins(self, mock_logger):
    method test_load_plugins_handles_errors (line 140) | def test_load_plugins_handles_errors(self):
  class ReadingTimePluginTest (line 149) | class ReadingTimePluginTest(BaseTestCase, PluginTestMixin):
    method test_reading_time_plugin_loaded (line 152) | def test_reading_time_plugin_loaded(self):
    method test_calculate_reading_time (line 158) | def test_calculate_reading_time(self):
  class ViewCountPluginTest (line 177) | class ViewCountPluginTest(BaseTestCase, PluginTestMixin):
    method test_view_count_plugin_loaded (line 180) | def test_view_count_plugin_loaded(self):
    method test_view_count_increment (line 186) | def test_view_count_increment(self):
  class SEOOptimizerPluginTest (line 196) | class SEOOptimizerPluginTest(BaseTestCase, PluginTestMixin):
    method test_seo_optimizer_plugin_loaded (line 199) | def test_seo_optimizer_plugin_loaded(self):
    method test_seo_meta_tags (line 205) | def test_seo_meta_tags(self):
  class ArticleCopyrightPluginTest (line 216) | class ArticleCopyrightPluginTest(BaseTestCase, PluginTestMixin):
    method test_copyright_plugin_loaded (line 219) | def test_copyright_plugin_loaded(self):
    method test_copyright_notice_added (line 225) | def test_copyright_notice_added(self):
  class ExternalLinksPluginTest (line 233) | class ExternalLinksPluginTest(BaseTestCase, PluginTestMixin):
    method test_external_links_plugin_loaded (line 236) | def test_external_links_plugin_loaded(self):
    method test_external_links_processing (line 242) | def test_external_links_processing(self):

FILE: djangoblog/tests.py
  class DjangoBlogTest (line 6) | class DjangoBlogTest(TestCase):
    method setUp (line 7) | def setUp(self):
    method test_utils (line 10) | def test_utils(self):

FILE: djangoblog/urls.py
  function health_check (line 46) | def health_check(request):

FILE: djangoblog/utils.py
  function get_max_articleid_commentid (line 24) | def get_max_articleid_commentid():
  function get_sha256 (line 30) | def get_sha256(value):
  function cache_decorator (line 36) | def cache_decorator(expiration=3 * 60):
  function expire_view_cache (line 72) | def expire_view_cache(path, servername, serverport, key_prefix=None):
  function get_current_site (line 98) | def get_current_site():
  class CommonMarkdown (line 103) | class CommonMarkdown:
    method _convert_markdown (line 105) | def _convert_markdown(value):
    method get_markdown_with_toc (line 119) | def get_markdown_with_toc(value):
    method get_markdown (line 124) | def get_markdown(value):
  function send_email (line 129) | def send_email(emailto, title, content):
  function generate_code (line 138) | def generate_code() -> str:
  function parse_dict_to_url (line 143) | def parse_dict_to_url(dict):
  function get_blog_setting (line 150) | def get_blog_setting():
  function save_user_avatar (line 178) | def save_user_avatar(url):
  function delete_sidebar_cache (line 206) | def delete_sidebar_cache():
  function delete_view_cache (line 214) | def delete_view_cache(prefix, keys):
  function get_resource_url (line 220) | def get_resource_url():
  function class_filter (line 248) | def class_filter(tag, name, value):
  function sanitize_html (line 274) | def sanitize_html(html):

FILE: djangoblog/whoosh_cn_backend.py
  class WhooshHtmlFormatter (line 56) | class WhooshHtmlFormatter(HtmlFormatter):
  class WhooshSearchBackend (line 65) | class WhooshSearchBackend(BaseSearchBackend):
    method __init__ (line 82) | def __init__(self, connection_alias, **connection_options):
    method setup (line 106) | def setup(self):
    method build_schema (line 147) | def build_schema(self, fields):
    method update (line 202) | def update(self, index, iterable, commit=True):
    method remove (line 247) | def remove(self, obj_or_string, commit=True):
    method clear (line 269) | def clear(self, models=None, commit=True):
    method delete_index (line 306) | def delete_index(self):
    method optimize (line 317) | def optimize(self):
    method calculate_page (line 324) | def calculate_page(self, start_offset=0, end_offset=None):
    method search (line 349) | def search(
    method more_like_this (line 563) | def more_like_this(
    method _process_results (line 678) | def _process_results(
    method create_spelling_suggestion (line 819) | def create_spelling_suggestion(self, query_string):
    method _from_python (line 848) | def _from_python(self, value):
    method _to_python (line 871) | def _to_python(self, value):
  class WhooshSearchQuery (line 922) | class WhooshSearchQuery(BaseSearchQuery):
    method _convert_datetime (line 923) | def _convert_datetime(self, date):
    method clean (line 929) | def clean(self, query_fragment):
    method build_query_fragment (line 954) | def build_query_fragment(self, field, filter_type, value):
  class WhooshEngine (line 1091) | class WhooshEngine(BaseEngine):

FILE: frontend/src/components/backToTop.js
  method init (line 12) | init() {
  method destroy (line 24) | destroy() {
  method checkScroll (line 29) | checkScroll() {
  method scrollToTop (line 35) | scrollToTop() {
  method debounce (line 59) | debounce(func, wait) {

FILE: frontend/src/components/commentSystem.js
  method init (line 16) | init() {
  method loadComments (line 28) | async loadComments() {
  method startReply (line 49) | startReply(commentId) {
  method cancelReply (line 64) | cancelReply() {
  method submitReply (line 71) | async submitReply(commentId) {
  method submitComment (line 103) | async submitComment() {
  method getCsrfToken (line 152) | getCsrfToken() {
  method showNotification (line 169) | showNotification(message) {
  method isReplying (line 183) | isReplying(commentId) {
  method canReply (line 187) | canReply() {

FILE: frontend/src/components/imageLightbox.js
  function imageLightbox (line 5) | function imageLightbox() {

FILE: frontend/src/components/navigation.js
  method init (line 14) | init() {
  method toggleMenu (line 37) | toggleMenu() {
  method closeMobileMenu (line 52) | closeMobileMenu() {
  method toggleSearch (line 58) | toggleSearch() {
  method submitSearch (line 71) | submitSearch() {
  method toggleTheme (line 78) | toggleTheme() {

FILE: frontend/src/components/reactionPicker.js
  method init (line 14) | init() {
  method loadFromDataAttribute (line 20) | loadFromDataAttribute() {
  method loadFromAPI (line 37) | async loadFromAPI() {
  method formatUsersText (line 65) | formatUsersText(users, totalCount) {
  method isAuthenticated (line 89) | isAuthenticated() {
  method showLoginPrompt (line 94) | showLoginPrompt() {
  method toggleReaction (line 151) | async toggleReaction(emoji) {
  method showNotification (line 214) | showNotification(message, type = 'info') {
  method getCsrfToken (line 249) | getCsrfToken() {

FILE: frontend/src/features/darkMode.js
  constant STORAGE_KEY (line 6) | const STORAGE_KEY = 'dark-mode-enabled';
  constant THEME_ATTR (line 7) | const THEME_ATTR = 'data-theme';
  constant ENABLE_SYSTEM (line 8) | const ENABLE_SYSTEM = true;
  function getPreferredTheme (line 13) | function getPreferredTheme() {
  function applyTheme (line 34) | function applyTheme(theme) {
  function getCurrentTheme (line 47) | function getCurrentTheme() {
  function setTheme (line 54) | function setTheme(theme) {
  function toggleTheme (line 75) | function toggleTheme() {
  function initTheme (line 85) | function initTheme() {
  function setupKeyboardShortcut (line 93) | function setupKeyboardShortcut() {
  function setupSystemThemeListener (line 105) | function setupSystemThemeListener() {
  function initDarkMode (line 127) | function initDarkMode() {

FILE: frontend/src/utils/nprogress.js
  method configure (line 18) | configure(options) {
  method set (line 23) | set(n) {
  method isStarted (line 54) | isStarted() {
  method start (line 58) | start() {
  method done (line 74) | done(force) {
  method inc (line 79) | inc(amount) {
  method trickle (line 94) | trickle() {
  method render (line 98) | render(fromStart) {
  method remove (line 120) | remove() {
  method isRendered (line 125) | isRendered() {
  method clamp (line 129) | clamp(n, min, max) {
  method toBarPerc (line 135) | toBarPerc(n) {
  function next (line 142) | function next() {

FILE: oauth/admin.py
  class OAuthUserAdmin (line 11) | class OAuthUserAdmin(admin.ModelAdmin):
    method get_readonly_fields (line 26) | def get_readonly_fields(self, request, obj=None):
    method has_add_permission (line 31) | def has_add_permission(self, request):
    method link_to_usermodel (line 34) | def link_to_usermodel(self, obj):
    method show_user_image (line 42) | def show_user_image(self, obj):
  class OAuthConfigAdmin (line 52) | class OAuthConfigAdmin(admin.ModelAdmin):

FILE: oauth/apps.py
  class OauthConfig (line 4) | class OauthConfig(AppConfig):

FILE: oauth/forms.py
  class RequireEmailForm (line 5) | class RequireEmailForm(forms.Form):
    method __init__ (line 9) | def __init__(self, *args, **kwargs):

FILE: oauth/migrations/0001_initial.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: oauth/migrations/0003_alter_oauthuser_nickname.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: oauth/models.py
  class OAuthUser (line 9) | class OAuthUser(models.Model):
    method __str__ (line 26) | def __str__(self):
    class Meta (line 29) | class Meta:
  class OAuthConfig (line 35) | class OAuthConfig(models.Model):
    method clean (line 56) | def clean(self):
    method __str__ (line 61) | def __str__(self):
    class Meta (line 64) | class Meta:

FILE: oauth/oauthmanager.py
  class OAuthAccessTokenException (line 15) | class OAuthAccessTokenException(Exception):
  class BaseOauthManager (line 21) | class BaseOauthManager(metaclass=ABCMeta):
    method __init__ (line 31) | def __init__(self, access_token=None, openid=None):
    method is_access_token_set (line 36) | def is_access_token_set(self):
    method is_authorized (line 40) | def is_authorized(self):
    method get_authorization_url (line 44) | def get_authorization_url(self, nexturl='/'):
    method get_access_token_by_code (line 48) | def get_access_token_by_code(self, code):
    method get_oauth_userinfo (line 52) | def get_oauth_userinfo(self):
    method get_picture (line 56) | def get_picture(self, metadata):
    method do_get (line 59) | def do_get(self, url, params, headers=None):
    method do_post (line 64) | def do_post(self, url, params, headers=None):
    method get_config (line 69) | def get_config(self):
  class WBOauthManager (line 74) | class WBOauthManager(BaseOauthManager):
    method __init__ (line 80) | def __init__(self, access_token=None, openid=None):
    method get_authorization_url (line 91) | def get_authorization_url(self, nexturl='/'):
    method get_access_token_by_code (line 100) | def get_access_token_by_code(self, code):
    method get_oauth_userinfo (line 119) | def get_oauth_userinfo(self):
    method get_picture (line 144) | def get_picture(self, metadata):
  class ProxyManagerMixin (line 149) | class ProxyManagerMixin:
    method __init__ (line 150) | def __init__(self, *args, **kwargs):
    method do_get (line 159) | def do_get(self, url, params, headers=None):
    method do_post (line 164) | def do_post(self, url, params, headers=None):
  class GoogleOauthManager (line 170) | class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
    method __init__ (line 176) | def __init__(self, access_token=None, openid=None):
    method get_authorization_url (line 187) | def get_authorization_url(self, nexturl='/'):
    method get_access_token_by_code (line 197) | def get_access_token_by_code(self, code):
    method get_oauth_userinfo (line 218) | def get_oauth_userinfo(self):
    method get_picture (line 243) | def get_picture(self, metadata):
  class GitHubOauthManager (line 248) | class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
    method __init__ (line 254) | def __init__(self, access_token=None, openid=None):
    method get_authorization_url (line 265) | def get_authorization_url(self, next_url='/'):
    method get_access_token_by_code (line 275) | def get_access_token_by_code(self, code):
    method get_oauth_userinfo (line 294) | def get_oauth_userinfo(self):
    method get_picture (line 316) | def get_picture(self, metadata):
  class FaceBookOauthManager (line 321) | class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
    method __init__ (line 327) | def __init__(self, access_token=None, openid=None):
    method get_authorization_url (line 338) | def get_authorization_url(self, next_url='/'):
    method get_access_token_by_code (line 348) | def get_access_token_by_code(self, code):
    method get_oauth_userinfo (line 367) | def get_oauth_userinfo(self):
    method get_picture (line 390) | def get_picture(self, metadata):
  class QQOauthManager (line 395) | class QQOauthManager(BaseOauthManager):
    method __init__ (line 402) | def __init__(self, access_token=None, openid=None):
    method get_authorization_url (line 413) | def get_authorization_url(self, next_url='/'):
    method get_access_token_by_code (line 422) | def get_access_token_by_code(self, code):
    method get_open_id (line 440) | def get_open_id(self):
    method get_oauth_userinfo (line 456) | def get_oauth_userinfo(self):
    method get_picture (line 479) | def get_picture(self, metadata):
  function get_oauth_apps (line 485) | def get_oauth_apps():
  function get_manager_by_type (line 495) | def get_manager_by_type(type):

FILE: oauth/templatetags/oauth_tags.py
  function load_oauth_applications (line 10) | def load_oauth_applications(request):

FILE: oauth/test_oauth_business_logic.py
  class OAuthConfigTest (line 11) | class OAuthConfigTest(TestCase):
    method test_oauth_config_can_be_created (line 14) | def test_oauth_config_can_be_created(self):
    method test_oauth_config_has_required_fields (line 27) | def test_oauth_config_has_required_fields(self):
    method test_oauth_config_type_uniqueness (line 41) | def test_oauth_config_type_uniqueness(self):
    method test_oauth_config_query_by_type (line 64) | def test_oauth_config_query_by_type(self):
    method test_oauth_config_is_enable_field (line 85) | def test_oauth_config_is_enable_field(self):
  class OAuthUserTest (line 105) | class OAuthUserTest(TestCase):
    method setUp (line 108) | def setUp(self):
    method test_oauth_user_can_be_created (line 116) | def test_oauth_user_can_be_created(self):
    method test_oauth_user_links_to_blog_user (line 130) | def test_oauth_user_links_to_blog_user(self):
    method test_oauth_user_has_openid (line 146) | def test_oauth_user_has_openid(self):
    method test_oauth_user_has_type (line 158) | def test_oauth_user_has_type(self):
    method test_oauth_user_token_storage (line 170) | def test_oauth_user_token_storage(self):
    method test_oauth_user_nickname_storage (line 183) | def test_oauth_user_nickname_storage(self):
  class OAuthUserQueryTest (line 197) | class OAuthUserQueryTest(TestCase):
    method setUp (line 200) | def setUp(self):
    method test_query_oauth_user_by_openid (line 214) | def test_query_oauth_user_by_openid(self):
    method test_query_oauth_user_by_type (line 228) | def test_query_oauth_user_by_type(self):
    method test_query_oauth_users_by_blog_user (line 256) | def test_query_oauth_users_by_blog_user(self):
    method test_user_can_have_multiple_oauth_accounts (line 281) | def test_user_can_have_multiple_oauth_accounts(self):
  class OAuthUserBindingTest (line 299) | class OAuthUserBindingTest(TestCase):
    method setUp (line 302) | def setUp(self):
    method test_bind_oauth_to_existing_user (line 310) | def test_bind_oauth_to_existing_user(self):
    method test_unbind_oauth_from_user (line 327) | def test_unbind_oauth_from_user(self):
    method test_change_oauth_binding (line 349) | def test_change_oauth_binding(self):
  class OAuthTokenManagementTest (line 373) | class OAuthTokenManagementTest(TestCase):
    method setUp (line 376) | def setUp(self):
    method test_oauth_token_can_be_updated (line 384) | def test_oauth_token_can_be_updated(self):
    method test_oauth_user_token_storage (line 402) | def test_oauth_user_token_storage(self):
  class OAuthUserDeletionTest (line 418) | class OAuthUserDeletionTest(TestCase):
    method setUp (line 421) | def setUp(self):
    method test_delete_oauth_user (line 429) | def test_delete_oauth_user(self):
    method test_delete_blog_user_cascade_oauth (line 448) | def test_delete_blog_user_cascade_oauth(self):
  class OAuthMetadataTest (line 469) | class OAuthMetadataTest(TestCase):
    method setUp (line 472) | def setUp(self):
    method test_oauth_user_metadata_field (line 480) | def test_oauth_user_metadata_field(self):
    method test_oauth_user_email_field (line 499) | def test_oauth_user_email_field(self):

FILE: oauth/tests.py
  class OAuthConfigTest (line 15) | class OAuthConfigTest(TestCase):
    method setUp (line 16) | def setUp(self):
    method test_oauth_login_test (line 23) | def test_oauth_login_test(self):
  class OauthLoginTest (line 39) | class OauthLoginTest(TestCase):
    method setUp (line 40) | def setUp(self) -> None:
    method init_apps (line 48) | def init_apps(self):
    method get_app_by_type (line 58) | def get_app_by_type(self, type):
    method test_weibo_login (line 65) | def test_weibo_login(self, mock_do_get, mock_do_post):
    method test_google_login (line 84) | def test_google_login(self, mock_do_get, mock_do_post):
    method test_github_login (line 105) | def test_github_login(self, mock_do_get, mock_do_post):
    method test_facebook_login (line 125) | def test_facebook_login(self, mock_do_get, mock_do_post):
    method test_qq_login (line 157) | def test_qq_login(self, mock_do_get):
    method test_weibo_authoriz_login_with_email (line 168) | def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_po...
    method test_weibo_authoriz_login_without_email (line 208) | def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do...

FILE: oauth/views.py
  function get_redirecturl (line 30) | def get_redirecturl(request):
  function oauthlogin (line 48) | def oauthlogin(request):
  function authorize (line 60) | def authorize(request):
  function emailconfirm (line 143) | def emailconfirm(request, id, sign):
  class RequireEmailView (line 200) | class RequireEmailView(FormView):
    method get (line 204) | def get(self, request, *args, **kwargs):
    method get_initial (line 213) | def get_initial(self):
    method get_context_data (line 220) | def get_context_data(self, **kwargs):
    method form_valid (line 227) | def form_valid(self, form):
  function bindsuccess (line 263) | def bindsuccess(request, oauthid):

FILE: owntracks/admin.py
  class OwnTrackLogsAdmin (line 6) | class OwnTrackLogsAdmin(admin.ModelAdmin):

FILE: owntracks/apps.py
  class OwntracksConfig (line 4) | class OwntracksConfig(AppConfig):

FILE: owntracks/migrations/0001_initial.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: owntracks/migrations/0002_alter_owntracklog_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: owntracks/models.py
  class OwnTrackLog (line 7) | class OwnTrackLog(models.Model):
    method __str__ (line 13) | def __str__(self):
    class Meta (line 16) | class Meta:

FILE: owntracks/tests.py
  class OwnTrackLogTest (line 11) | class OwnTrackLogTest(TestCase):
    method setUp (line 12) | def setUp(self):
    method test_own_track_log (line 16) | def test_own_track_log(self):

FILE: owntracks/views.py
  function manage_owntrack_log (line 23) | def manage_owntrack_log(request):
  function show_maps (line 48) | def show_maps(request):
  function show_log_dates (line 62) | def show_log_dates(request):
  function convert_to_amap (line 72) | def convert_to_amap(locations):
  function get_datas (line 98) | def get_datas(request):

FILE: plugins/article_copyright/plugin.py
  class ArticleCopyrightPlugin (line 6) | class ArticleCopyrightPlugin(BasePlugin):
    method register_hooks (line 13) | def register_hooks(self):
    method add_copyright_to_content (line 17) | def add_copyright_to_content(self, content, *args, **kwargs):

FILE: plugins/article_recommendation/plugin.py
  class ArticleRecommendationPlugin (line 10) | class ArticleRecommendationPlugin(BasePlugin):
    method register_hooks (line 32) | def register_hooks(self):
    method on_article_detail_load (line 36) | def on_article_detail_load(self, article, context, request, *args, **k...
    method should_display (line 42) | def should_display(self, position, context, **kwargs):
    method render_article_bottom_widget (line 53) | def render_article_bottom_widget(self, context, **kwargs):
    method render_sidebar_widget (line 83) | def render_sidebar_widget(self, context, **kwargs):
    method get_css_files (line 119) | def get_css_files(self):
    method get_js_files (line 123) | def get_js_files(self):
    method get_recommendations (line 127) | def get_recommendations(self, article, count=5):
    method get_popular_articles (line 197) | def get_popular_articles(self, count=3):

FILE: plugins/article_recommendation/static/article_recommendation/js/recommendation.js
  function initRecommendations (line 13) | function initRecommendations() {
  function trackRecommendationClicks (line 21) | function trackRecommendationClicks() {
  function lazyLoadRecommendations (line 44) | function lazyLoadRecommendations() {
  function addAnimations (line 71) | function addAnimations() {

FILE: plugins/cloudflare_cache/api.py
  class CloudflareAPI (line 14) | class CloudflareAPI:
    method __init__ (line 19) | def __init__(self, zone_id: str, api_token: str):
    method purge_urls (line 34) | def purge_urls(self, urls: List[str]) -> Dict:
    method purge_all (line 95) | def purge_all(self) -> Dict:
    method purge_by_tags (line 132) | def purge_by_tags(self, tags: List[str]) -> Dict:
    method validate_credentials (line 172) | def validate_credentials(self) -> bool:

FILE: plugins/cloudflare_cache/handlers.py
  class CloudflareCacheHandler (line 14) | class CloudflareCacheHandler:
    method __init__ (line 17) | def __init__(self, config: dict):
    method on_model_save (line 35) | def on_model_save(self, sender, instance, created, update_fields, **kw...
    method _collect_article_urls (line 78) | def _collect_article_urls(self, article, is_new: bool) -> List[str]:
    method _collect_comment_urls (line 142) | def _collect_comment_urls(self, comment, is_new: bool) -> List[str]:
    method _purge_cache_batch (line 178) | def _purge_cache_batch(self, urls: List[str]):
    method purge_all (line 215) | def purge_all(self):

FILE: plugins/cloudflare_cache/plugin.py
  class CloudflareCachePlugin (line 28) | class CloudflareCachePlugin(BasePlugin):
    method init_plugin (line 66) | def init_plugin(self):
    method _validate_config (line 93) | def _validate_config(self) -> bool:
    method _test_api_connection (line 122) | def _test_api_connection(self):
    method _purge_on_startup (line 141) | def _purge_on_startup(self):
    method register_hooks (line 181) | def register_hooks(self):
    method purge_all_cache (line 226) | def purge_all_cache(self):
    method get_plugin_status (line 246) | def get_plugin_status(self) -> dict:

FILE: plugins/external_links/plugin.py
  class ExternalLinksPlugin (line 8) | class ExternalLinksPlugin(BasePlugin):
    method register_hooks (line 14) | def register_hooks(self):
    method process_external_links (line 17) | def process_external_links(self, content, *args, **kwargs):

FILE: plugins/image_lazy_loading/plugin.py
  class ImageOptimizationPlugin (line 9) | class ImageOptimizationPlugin(BasePlugin):
    method __init__ (line 15) | def __init__(self):
    method register_hooks (line 27) | def register_hooks(self):
    method optimize_images (line 30) | def optimize_images(self, content, *args, **kwargs):
    method _parse_img_attributes (line 66) | def _parse_img_attributes(self, attr_string):
    method _apply_optimizations (line 82) | def _apply_optimizations(self, attrs, image_index):
    method _build_img_tag (line 153) | def _build_img_tag(self, attrs):
    method _get_current_domain (line 170) | def _get_current_domain(self):

FILE: plugins/reading_time/plugin.py
  class ReadingTimePlugin (line 8) | class ReadingTimePlugin(BasePlugin):
    method register_hooks (line 14) | def register_hooks(self):
    method add_reading_time (line 17) | def add_reading_time(self, content, *args, **kwargs):

FILE: plugins/seo_optimizer/plugin.py
  class SeoOptimizerPlugin (line 10) | class SeoOptimizerPlugin(BasePlugin):
    method register_hooks (line 16) | def register_hooks(self):
    method _get_article_seo_data (line 19) | def _get_article_seo_data(self, context, request, blog_setting):
    method _get_category_seo_data (line 73) | def _get_category_seo_data(self, context, request, blog_setting):
    method _get_tag_seo_data (line 99) | def _get_tag_seo_data(self, context, request, blog_setting):
    method _get_author_seo_data (line 127) | def _get_author_seo_data(self, context, request, blog_setting):
    method _get_default_seo_data (line 151) | def _get_default_seo_data(self, context, request, blog_setting):
    method dispatch_seo_generation (line 170) | def dispatch_seo_generation(self, metas, context):

FILE: plugins/view_count/plugin.py
  class ViewCountPlugin (line 5) | class ViewCountPlugin(BasePlugin):
    method register_hooks (line 11) | def register_hooks(self):
    method record_view (line 14) | def record_view(self, article, *args, **kwargs):

FILE: servermanager/MemcacheStorage.py
  class MemcacheStorage (line 7) | class MemcacheStorage(SessionStorage):
    method __init__ (line 8) | def __init__(self, prefix='ws_'):
    method is_available (line 13) | def is_available(self):
    method key_name (line 18) | def key_name(self, s):
    method get (line 21) | def get(self, id):
    method set (line 26) | def set(self, id, value):
    method delete (line 30) | def delete(self, id):

FILE: servermanager/admin.py
  class CommandsAdmin (line 5) | class CommandsAdmin(admin.ModelAdmin):
  class EmailSendLogAdmin (line 9) | class EmailSendLogAdmin(admin.ModelAdmin):
    method has_add_permission (line 18) | def has_add_permission(self, request):

FILE: servermanager/api/blogapi.py
  class BlogApi (line 6) | class BlogApi:
    method __init__ (line 7) | def __init__(self):
    method search_articles (line 12) | def search_articles(self, query):
    method get_category_lists (line 17) | def get_category_lists(self):
    method get_category_articles (line 20) | def get_category_articles(self, categoryname):
    method get_recent_articles (line 26) | def get_recent_articles(self):

FILE: servermanager/api/commonapi.py
  class ChatGPT (line 15) | class ChatGPT:
    method chat (line 18) | def chat(prompt):
  class CommandHandler (line 28) | class CommandHandler:
    method __init__ (line 29) | def __init__(self):
    method run (line 32) | def run(self, title):
    method __run_command__ (line 47) | def __run_command__(self, cmd):
    method get_help (line 54) | def get_help(self):

FILE: servermanager/apps.py
  class ServermanagerConfig (line 4) | class ServermanagerConfig(AppConfig):

FILE: servermanager/migrations/0001_initial.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: servermanager/models.py
  class commands (line 5) | class commands(models.Model):
    method __str__ (line 12) | def __str__(self):
    class Meta (line 15) | class Meta:
  class EmailSendLog (line 20) | class EmailSendLog(models.Model):
    method __str__ (line 27) | def __str__(self):
    class Meta (line 30) | class Meta:

FILE: servermanager/robot.py
  function convert_to_article_reply (line 31) | def convert_to_article_reply(articles, message):
  function search (line 50) | def search(message, session):
  function category (line 63) | def category(message, session):
  function recents (line 70) | def recents(message, session):
  function help (line 80) | def help(message, session):
  function weather (line 102) | def weather(message, session):
  function idcard (line 107) | def idcard(message, session):
  function echo (line 112) | def echo(message, session):
  class MessageHandler (line 117) | class MessageHandler:
    method __init__ (line 118) | def __init__(self, message, session):
    method is_admin (line 131) | def is_admin(self):
    method is_password_set (line 135) | def is_password_set(self):
    method save_session (line 138) | def save_session(self):
    method handler (line 142) | def handler(self):
  class WxUserInfo (line 182) | class WxUserInfo():
    method __init__ (line 183) | def __init__(self):

FILE: servermanager/tests.py
  class ServerManagerTest (line 14) | class ServerManagerTest(TestCase):
    method setUp (line 15) | def setUp(self):
    method test_chat_gpt (line 19) | def test_chat_gpt(self):
    method test_validate_comment (line 23) | def test_validate_comment(self):
Condensed preview — 268 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,128K chars).
[
  {
    "path": ".dockerignore",
    "chars": 359,
    "preview": "bin/data/\r\n# virtualenv\r\nvenv/\r\ncollectedstatic/\r\ndjangoblog/whoosh_index/\r\nuploads/\r\nsettings_production.py\r\n*.md\r\ndocs"
  },
  {
    "path": ".gitattributes",
    "chars": 76,
    "preview": "* text=auto\n*.sh text eol=lf\n*.conf text eol=lf\n*.mo binary\n*.po text eol=lf"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 470,
    "preview": "<!--\n如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。\n提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way\n-->\n\n**"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 1816,
    "preview": "version: 2\nupdates:\n  # Python 依赖管理\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    target-branch: \"dev\"\n    schedul"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 978,
    "preview": "name: \"CodeQL\"\n\non:\n  push:\n    branches:\n      - master\n      - dev\n    paths-ignore:\n      - '**/*.md'\n      - '**/*.c"
  },
  {
    "path": ".github/workflows/deploy-master.yml",
    "chars": 7860,
    "preview": "name: 自动部署到生产环境\n\non:\n  workflow_run:\n    workflows: [\"Django CI\", \"Frontend CI\"]\n    types:\n      - completed\n    branch"
  },
  {
    "path": ".github/workflows/django.yml",
    "chars": 14092,
    "preview": "name: Django CI\n\non:\n  push:\n    branches:\n      - master\n      - dev\n    paths-ignore:\n      - '**/*.md'\n      - '**/*."
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1581,
    "preview": "name: docker\n\non:\n  push:\n    paths-ignore:\n      - '**/*.md'\n      - '**/*.yml'\n    branches:\n      - 'master'\n      - "
  },
  {
    "path": ".github/workflows/frontend.yml",
    "chars": 7246,
    "preview": "name: Frontend CI\n\non:\n  push:\n    branches:\n      - master\n      - dev\n    paths:\n      - 'frontend/**'\n      - '.githu"
  },
  {
    "path": ".github/workflows/publish-release.yml",
    "chars": 1023,
    "preview": "name: publish release\n\non:\n  release:\n    types: [ published ]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github."
  },
  {
    "path": ".gitignore",
    "chars": 1151,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": "Dockerfile",
    "chars": 1803,
    "preview": "# Stage 1: Build frontend assets\nFROM node:20-alpine AS frontend-builder\n\nWORKDIR /app\n\n# Copy frontend package files\nCO"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2025 车亮亮\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 5252,
    "preview": "# DjangoBlog\n\n<p align=\"center\">\n  <a href=\"https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml\"><im"
  },
  {
    "path": "accounts/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "accounts/admin.py",
    "chars": 1848,
    "preview": "from django import forms\nfrom django.contrib.auth.admin import UserAdmin\nfrom django.contrib.auth.forms import UserChang"
  },
  {
    "path": "accounts/apps.py",
    "chars": 91,
    "preview": "from django.apps import AppConfig\n\n\nclass AccountsConfig(AppConfig):\n    name = 'accounts'\n"
  },
  {
    "path": "accounts/forms.py",
    "chars": 3744,
    "preview": "from django import forms\nfrom django.contrib.auth import get_user_model, password_validation\nfrom django.contrib.auth.fo"
  },
  {
    "path": "accounts/migrations/0001_initial.py",
    "chars": 3328,
    "preview": "# Generated by Django 4.1.7 on 2023-03-02 07:14\n\nimport django.contrib.auth.models\nimport django.contrib.auth.validators"
  },
  {
    "path": "accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py",
    "chars": 1502,
    "preview": "# Generated by Django 4.2.5 on 2023-09-06 13:13\n\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n"
  },
  {
    "path": "accounts/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "accounts/models.py",
    "chars": 1184,
    "preview": "from django.contrib.auth.models import AbstractUser\nfrom django.db import models\nfrom django.urls import reverse\nfrom dj"
  },
  {
    "path": "accounts/templatetags/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "accounts/test_admin.py",
    "chars": 3315,
    "preview": "\"\"\"\nAccounts Admin 测试\n测试用户管理后台的各项功能\n\"\"\"\nfrom django.contrib.admin.sites import AdminSite\nfrom django.test import TestCas"
  },
  {
    "path": "accounts/test_user_business_logic.py",
    "chars": 14064,
    "preview": "\"\"\"\nTest cases for user authentication business logic\n包括用户注册、登录、密码管理、权限等核心业务逻辑\n\"\"\"\nfrom django.test import TestCase, Cli"
  },
  {
    "path": "accounts/tests.py",
    "chars": 6900,
    "preview": "from django.test import Client, RequestFactory, TestCase\nfrom django.urls import reverse\nfrom django.utils import timezo"
  },
  {
    "path": "accounts/urls.py",
    "chars": 1075,
    "preview": "from django.urls import path\nfrom django.urls import re_path\n\nfrom . import views\nfrom .forms import LoginForm\n\napp_name"
  },
  {
    "path": "accounts/user_login_backend.py",
    "chars": 784,
    "preview": "from django.contrib.auth import get_user_model\nfrom django.contrib.auth.backends import ModelBackend\n\n\nclass EmailOrUser"
  },
  {
    "path": "accounts/utils.py",
    "chars": 1210,
    "preview": "import typing\nfrom datetime import timedelta\n\nfrom django.core.cache import cache\nfrom django.utils.translation import g"
  },
  {
    "path": "accounts/views.py",
    "chars": 7147,
    "preview": "import logging\nfrom django.utils.translation import gettext_lazy as _\nfrom django.conf import settings\nfrom django.contr"
  },
  {
    "path": "blog/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "blog/admin.py",
    "chars": 4401,
    "preview": "from django import forms\nfrom django.contrib import admin\nfrom django.contrib.auth import get_user_model\nfrom django.url"
  },
  {
    "path": "blog/apps.py",
    "chars": 2463,
    "preview": "from django.apps import AppConfig\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\nclass BlogConfig(AppConfig):\n  "
  },
  {
    "path": "blog/context_processors.py",
    "chars": 1975,
    "preview": "import logging\n\nfrom django.utils import timezone\n\nfrom djangoblog.utils import cache, get_blog_setting\nfrom .models imp"
  },
  {
    "path": "blog/documents.py",
    "chars": 7301,
    "preview": "import time\n\nimport elasticsearch.client\nfrom django.conf import settings\nfrom elasticsearch_dsl import Document, InnerD"
  },
  {
    "path": "blog/forms.py",
    "chars": 470,
    "preview": "import logging\n\nfrom django import forms\nfrom haystack.forms import SearchForm\n\nlogger = logging.getLogger(__name__)\n\n\nc"
  },
  {
    "path": "blog/management/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "blog/management/commands/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "blog/management/commands/build_index.py",
    "chars": 565,
    "preview": "from django.core.management.base import BaseCommand\n\nfrom blog.documents import ElapsedTimeDocument, ArticleDocumentMana"
  },
  {
    "path": "blog/management/commands/build_search_words.py",
    "chars": 357,
    "preview": "from django.core.management.base import BaseCommand\n\nfrom blog.models import Tag, Category\n\n\n# TODO 参数化\nclass Command(Ba"
  },
  {
    "path": "blog/management/commands/clear_cache.py",
    "chars": 281,
    "preview": "from django.core.management.base import BaseCommand\n\nfrom djangoblog.utils import cache\n\n\nclass Command(BaseCommand):\n  "
  },
  {
    "path": "blog/management/commands/create_testdata.py",
    "chars": 1328,
    "preview": "from django.contrib.auth import get_user_model\nfrom django.contrib.auth.hashers import make_password\nfrom django.core.ma"
  },
  {
    "path": "blog/management/commands/ping_baidu.py",
    "chars": 1668,
    "preview": "from django.core.management.base import BaseCommand\n\nfrom djangoblog.spider_notify import SpiderNotify\nfrom djangoblog.u"
  },
  {
    "path": "blog/management/commands/sync_user_avatar.py",
    "chars": 1581,
    "preview": "import requests\nfrom django.core.management.base import BaseCommand\nfrom django.templatetags.static import static\n\nfrom "
  },
  {
    "path": "blog/middleware.py",
    "chars": 1471,
    "preview": "import logging\nimport time\n\nfrom ipware import get_client_ip\nfrom user_agents import parse\n\nfrom blog.documents import E"
  },
  {
    "path": "blog/migrations/0001_initial.py",
    "chars": 8095,
    "preview": "# Generated by Django 4.1.7 on 2023-03-02 07:14\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "blog/migrations/0002_blogsettings_global_footer_and_more.py",
    "chars": 629,
    "preview": "# Generated by Django 4.1.7 on 2023-03-29 06:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "blog/migrations/0003_blogsettings_comment_need_review.py",
    "chars": 444,
    "preview": "# Generated by Django 4.2.1 on 2023-05-09 07:45\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py",
    "chars": 694,
    "preview": "# Generated by Django 4.2.1 on 2023-05-09 07:51\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "blog/migrations/0005_alter_article_options_alter_category_options_and_more.py",
    "chars": 11736,
    "preview": "# Generated by Django 4.2.5 on 2023-09-06 13:13\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "blog/migrations/0006_alter_blogsettings_options.py",
    "chars": 450,
    "preview": "# Generated by Django 4.2.7 on 2024-01-26 02:41\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "blog/migrations/0007_article_idx_type_status_pub_article_idx_status_views_and_more.py",
    "chars": 1011,
    "preview": "# Generated by Django 5.2.9 on 2025-12-25 14:36\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "blog/migrations/0008_blogsettings_color_scheme.py",
    "chars": 777,
    "preview": "# Generated by Django 5.2.9 on 2025-12-31 13:30\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "blog/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "blog/models.py",
    "chars": 12936,
    "preview": "import logging\nimport re\nfrom abc import abstractmethod\n\nfrom django.conf import settings\nfrom django.core.exceptions im"
  },
  {
    "path": "blog/search_indexes.py",
    "chars": 462,
    "preview": "from haystack import indexes\n\nfrom blog.models import Article\n\n\nclass ArticleIndex(indexes.SearchIndex, indexes.Indexabl"
  },
  {
    "path": "blog/static/account/css/account.css",
    "chars": 172,
    "preview": ".button {\n    border: none;\n    padding: 4px 80px;\n    text-align: center;\n    text-decoration: none;\n    display: inlin"
  },
  {
    "path": "blog/static/account/js/account.js",
    "chars": 1210,
    "preview": "let wait = 60;\n\nfunction time(o) {\n    if (wait == 0) {\n        o.removeAttribute(\"disabled\");\n        o.value = \"获取验证码\""
  },
  {
    "path": "blog/static/assets/css/signin.css",
    "chars": 1087,
    "preview": "body {\n  padding-top: 40px;\n  padding-bottom: 40px;\n  background-color: #fff;\n}\n\n.form-signin {\n  max-width: 330px;\n  pa"
  },
  {
    "path": "blog/static/blog/css/oauth_style.css",
    "chars": 4845,
    "preview": "\n.icon-sn-google {\n    background-position: 0 -28px;\n}\n\n.icon-sn-bg-google {\n    background-color: #4285f4;\n    backgrou"
  },
  {
    "path": "blog/static/blog/fonts/open-sans.css",
    "chars": 33711,
    "preview": "/* cyrillic-ext */\n@font-face {\n  font-family: 'Open Sans';\n  font-style: italic;\n  font-weight: 300;\n  font-stretch: 10"
  },
  {
    "path": "blog/static/blog/js/mathjax-loader.js",
    "chars": 4515,
    "preview": "/**\n * MathJax 智能加载器\n * 检测页面是否包含数学公式,如果有则动态加载和配置MathJax\n */\n(function() {\n    'use strict';\n    \n    /**\n     * 检测页面是否包含"
  },
  {
    "path": "blog/static/pygments/default.css",
    "chars": 3649,
    "preview": ".codehilite .hll {\n    background-color: #ffffcc\n}\n\n.codehilite {\n    background: #ffffff;\n}\n\n.codehilite .c {\n    color"
  },
  {
    "path": "blog/templatetags/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "blog/templatetags/blog_tags.py",
    "chars": 20997,
    "preview": "import hashlib\nimport json\nimport logging\nimport random\nimport urllib\n\nfrom django import template\nfrom django.conf impo"
  },
  {
    "path": "blog/templatetags/vite_tags.py",
    "chars": 5362,
    "preview": "\"\"\"\nVite资源加载Django模板标签\n用于在Django模板中加载Vite构建的前端资源\n\n使用方法:\n    {% load vite_tags %}\n    {% vite_js 'src/main.js' %}\n    {% "
  },
  {
    "path": "blog/test_admin.py",
    "chars": 4686,
    "preview": "\"\"\"\nBlog Admin 测试\n测试文章、分类、标签等后台管理功能\n\"\"\"\nfrom django.contrib.admin.sites import AdminSite\nfrom django.test import Request"
  },
  {
    "path": "blog/test_article_business_logic.py",
    "chars": 16476,
    "preview": "\"\"\"\nTest cases for article business logic\n包括文章状态转换、权限控制、评论控制等核心业务逻辑\n\"\"\"\nfrom django.test import TestCase, Client\nfrom dj"
  },
  {
    "path": "blog/test_context_processors.py",
    "chars": 11313,
    "preview": "\"\"\"\nTest cases for blog context processors\n\"\"\"\nfrom unittest.mock import patch, Mock\n\nfrom django.test import TestCase, "
  },
  {
    "path": "blog/test_middleware.py",
    "chars": 10055,
    "preview": "\"\"\"\nTest cases for blog middleware\n\"\"\"\nimport time\nfrom unittest.mock import Mock, patch, MagicMock\n\nfrom django.test im"
  },
  {
    "path": "blog/test_templatetags.py",
    "chars": 5927,
    "preview": "\"\"\"\n模板标签测试\n测试各种模板标签和过滤器的功能\n\"\"\"\nfrom django.core.paginator import Paginator\nfrom django.template import Context, Template"
  },
  {
    "path": "blog/test_views.py",
    "chars": 6036,
    "preview": "\"\"\"\nBlog Views 测试\n测试视图层的错误处理、权限验证和边界条件\n\"\"\"\nfrom django.urls import reverse\n\nfrom blog.models import Article\nfrom djangob"
  },
  {
    "path": "blog/tests.py",
    "chars": 10707,
    "preview": "import os\n\nfrom django.conf import settings\nfrom django.core.files.uploadedfile import SimpleUploadedFile\nfrom django.co"
  },
  {
    "path": "blog/urls.py",
    "chars": 1666,
    "preview": "from django.urls import path\nfrom django.views.decorators.cache import cache_page\n\nfrom . import views\n\napp_name = \"blog"
  },
  {
    "path": "blog/views.py",
    "chars": 13597,
    "preview": "import logging\nimport os\nimport uuid\n\nfrom django.conf import settings\nfrom django.core.paginator import Paginator\nfrom "
  },
  {
    "path": "codecov.yml",
    "chars": 1367,
    "preview": "codecov:\n  require_ci_to_pass: yes\n\ncoverage:\n  precision: 2\n  round: down\n  range: \"70...100\"\n\n  status:\n    project:\n "
  },
  {
    "path": "comments/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "comments/admin.py",
    "chars": 2919,
    "preview": "from django.contrib import admin\nfrom django.urls import reverse\nfrom django.utils.html import format_html\nfrom django.u"
  },
  {
    "path": "comments/apps.py",
    "chars": 91,
    "preview": "from django.apps import AppConfig\n\n\nclass CommentsConfig(AppConfig):\n    name = 'comments'\n"
  },
  {
    "path": "comments/forms.py",
    "chars": 282,
    "preview": "from django import forms\nfrom django.forms import ModelForm\n\nfrom .models import Comment\n\n\nclass CommentForm(ModelForm):"
  },
  {
    "path": "comments/migrations/0001_initial.py",
    "chars": 1651,
    "preview": "# Generated by Django 4.1.7 on 2023-03-02 07:14\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "comments/migrations/0002_alter_comment_is_enable.py",
    "chars": 404,
    "preview": "# Generated by Django 4.1.7 on 2023-04-24 13:48\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py",
    "chars": 2257,
    "preview": "# Generated by Django 4.2.5 on 2023-09-06 13:13\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "comments/migrations/0004_comment_idx_art_parent_enable_comment_idx_enable_id.py",
    "chars": 806,
    "preview": "# Generated by Django 5.2.9 on 2025-12-25 14:36\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "comments/migrations/0005_commentreaction.py",
    "chars": 1623,
    "preview": "# Generated by Django 5.2.9 on 2026-01-22 14:13\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "comments/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "comments/models.py",
    "chars": 4180,
    "preview": "from django.conf import settings\nfrom django.db import models\nfrom django.utils.timezone import now\nfrom django.utils.tr"
  },
  {
    "path": "comments/templatetags/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "comments/templatetags/comments_tags.py",
    "chars": 682,
    "preview": "from django import template\n\nregister = template.Library()\n\n\n@register.simple_tag\ndef parse_commenttree(commentlist, com"
  },
  {
    "path": "comments/test_comment_business_logic.py",
    "chars": 18931,
    "preview": "\"\"\"\nTest cases for comment business logic\n包括评论审核工作流、嵌套回复、权限控制等核心业务逻辑\n\"\"\"\nfrom django.test import TestCase, Client\nfrom d"
  },
  {
    "path": "comments/test_views.py",
    "chars": 8889,
    "preview": "\"\"\"\nComments Views 测试\n测试评论功能的错误处理和边界条件\n\"\"\"\nfrom django.test import TransactionTestCase\nfrom django.urls import reverse\n\n"
  },
  {
    "path": "comments/tests.py",
    "chars": 3591,
    "preview": "from django.test import Client, RequestFactory, TransactionTestCase\nfrom django.urls import reverse\n\nfrom accounts.model"
  },
  {
    "path": "comments/urls.py",
    "chars": 347,
    "preview": "from django.urls import path\n\nfrom . import views\n\napp_name = \"comments\"\nurlpatterns = [\n    path(\n        'article/<int"
  },
  {
    "path": "comments/utils.py",
    "chars": 1743,
    "preview": "import logging\n\nfrom django.utils.translation import gettext_lazy as _\n\nfrom djangoblog.utils import get_current_site\nfr"
  },
  {
    "path": "comments/views.py",
    "chars": 4101,
    "preview": "# Create your views here.\nfrom django.core.exceptions import ValidationError\nfrom django.http import HttpResponseRedirec"
  },
  {
    "path": "deploy/docker-compose/docker-compose.es.yml",
    "chars": 1131,
    "preview": "version: '3'\n\nservices:\n  es:\n    image: liangliangyy/elasticsearch-analysis-ik:8.6.1\n    container_name: es\n    restart"
  },
  {
    "path": "deploy/docker-compose/docker-compose.yml",
    "chars": 1482,
    "preview": "version: '3'\n\nservices:\n  db:\n    image: mysql:latest\n    restart: always\n    environment:\n      - MYSQL_DATABASE=django"
  },
  {
    "path": "deploy/entrypoint.sh",
    "chars": 1087,
    "preview": "#!/usr/bin/env bash\nNAME=\"djangoblog\"\nDJANGODIR=/code/djangoblog\nUSER=root\nGROUP=root\nNUM_WORKERS=1\nDJANGO_WSGI_MODULE=d"
  },
  {
    "path": "deploy/k8s/configmap.yaml",
    "chars": 3352,
    "preview": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: web-nginx-config\n  namespace: djangoblog\ndata:\n  nginx.conf: |\n    user"
  },
  {
    "path": "deploy/k8s/deployment.yaml",
    "chars": 6148,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: djangoblog\n  namespace: djangoblog\n  labels:\n    app: djangoblog\n"
  },
  {
    "path": "deploy/k8s/gateway.yaml",
    "chars": 334,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: nginx\n  namespace: djangoblog\nspec:\n  ingressClassName:"
  },
  {
    "path": "deploy/k8s/pv.yaml",
    "chars": 1963,
    "preview": "apiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: local-pv-db\nspec:\n  capacity:\n    storage: 10Gi\n  volumeMode: Fi"
  },
  {
    "path": "deploy/k8s/pvc.yaml",
    "chars": 1025,
    "preview": "apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: db-pvc\n  namespace: djangoblog\nspec:\n  storageClassName: lo"
  },
  {
    "path": "deploy/k8s/service.yaml",
    "chars": 1154,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: djangoblog\n  namespace: djangoblog\n  labels:\n    app: djangoblog\nspec:\n  "
  },
  {
    "path": "deploy/k8s/storageclass.yaml",
    "chars": 227,
    "preview": "apiVersion: storage.k8s.io/v1\nkind: StorageClass\nmetadata:\n  name: local-storage\n  annotations:\n    storageclass.kuberne"
  },
  {
    "path": "deploy/nginx.conf",
    "chars": 1090,
    "preview": "user  nginx;\nworker_processes  auto;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\n\nevent"
  },
  {
    "path": "djangoblog/__init__.py",
    "chars": 59,
    "preview": "default_app_config = 'djangoblog.apps.DjangoblogAppConfig'\n"
  },
  {
    "path": "djangoblog/admin_site.py",
    "chars": 1879,
    "preview": "from django.contrib.admin import AdminSite\nfrom django.contrib.admin.models import LogEntry\nfrom django.contrib.sites.ad"
  },
  {
    "path": "djangoblog/apps.py",
    "chars": 317,
    "preview": "from django.apps import AppConfig\n\nclass DjangoblogAppConfig(AppConfig):\n    default_auto_field = 'django.db.models.BigA"
  },
  {
    "path": "djangoblog/base_views.py",
    "chars": 3017,
    "preview": "#!/usr/bin/env python\n# encoding: utf-8\n\n\"\"\"\nDjango Blog 基础视图类\n提供带有常用装饰器的视图基类,减少重复的 dispatch 方法定义\n\"\"\"\n\nfrom django.contr"
  },
  {
    "path": "djangoblog/blog_signals.py",
    "chars": 5300,
    "preview": "import _thread\nimport logging\n\nimport django.dispatch\nfrom django.conf import settings\nfrom django.contrib.admin.models "
  },
  {
    "path": "djangoblog/constants.py",
    "chars": 1902,
    "preview": "#!/usr/bin/env python\n# encoding: utf-8\n\n\"\"\"\nDjango Blog 全局常量定义\n包含缓存超时时间、缓存键模板等配置\n\"\"\"\n\n\n# ===== 缓存过期时间(秒)=====\nclass Cac"
  },
  {
    "path": "djangoblog/elasticsearch_backend.py",
    "chars": 6779,
    "preview": "from django.utils.encoding import force_str\nfrom elasticsearch_dsl import Q\nfrom haystack.backends import BaseEngine, Ba"
  },
  {
    "path": "djangoblog/error_views.py",
    "chars": 2877,
    "preview": "#!/usr/bin/env python\n# encoding: utf-8\n\n\"\"\"\nDjango Blog 统一错误处理视图\n提供统一的错误页面渲染,减少重复代码\n\"\"\"\n\nimport logging\nfrom django.sho"
  },
  {
    "path": "djangoblog/feeds.py",
    "chars": 1086,
    "preview": "from django.contrib.auth import get_user_model\nfrom django.contrib.syndication.views import Feed\nfrom django.utils impor"
  },
  {
    "path": "djangoblog/logentryadmin.py",
    "chars": 2976,
    "preview": "from django.contrib import admin\nfrom django.contrib.admin.models import DELETION\nfrom django.contrib.contenttypes.model"
  },
  {
    "path": "djangoblog/mixins.py",
    "chars": 6879,
    "preview": "#!/usr/bin/env python\n# encoding: utf-8\n\n\"\"\"\nDjango Blog 混入类 (Mixins)\n提供可复用的功能模块,减少代码重复\n\"\"\"\n\nimport logging\nfrom django."
  },
  {
    "path": "djangoblog/plugin_manage/base_plugin.py",
    "chars": 5422,
    "preview": "import logging\nfrom pathlib import Path\n\nfrom django.template import TemplateDoesNotExist\nfrom django.template.loader im"
  },
  {
    "path": "djangoblog/plugin_manage/hook_constants.py",
    "chars": 591,
    "preview": "ARTICLE_DETAIL_LOAD = 'article_detail_load'\nARTICLE_CREATE = 'article_create'\nARTICLE_UPDATE = 'article_update'\nARTICLE_"
  },
  {
    "path": "djangoblog/plugin_manage/hooks.py",
    "chars": 1316,
    "preview": "import logging\n\nlogger = logging.getLogger(__name__)\n\n_hooks = {}\n\n\ndef register(hook_name: str, callback: callable):\n  "
  },
  {
    "path": "djangoblog/plugin_manage/loader.py",
    "chars": 2264,
    "preview": "import os\nimport logging\nfrom django.conf import settings\n\nlogger = logging.getLogger(__name__)\n\n# 全局插件注册表\n_loaded_plugi"
  },
  {
    "path": "djangoblog/settings.py",
    "chars": 13391,
    "preview": "\"\"\"\nDjango settings for djangoblog project.\n\nGenerated by 'django-admin startproject' using Django 1.10.2.\n\nFor more inf"
  },
  {
    "path": "djangoblog/sitemap.py",
    "chars": 1170,
    "preview": "from django.contrib.sitemaps import Sitemap\nfrom django.urls import reverse\n\nfrom blog.models import Article, Category, "
  },
  {
    "path": "djangoblog/spider_notify.py",
    "chars": 469,
    "preview": "import logging\n\nimport requests\nfrom django.conf import settings\n\nlogger = logging.getLogger(__name__)\n\n\nclass SpiderNot"
  },
  {
    "path": "djangoblog/test_base.py",
    "chars": 8583,
    "preview": "\"\"\"\n可复用的测试基类和工具\n提供通用的测试数据创建和断言方法\n\"\"\"\nfrom django.contrib.auth.models import Permission\nfrom django.test import TestCase,"
  },
  {
    "path": "djangoblog/test_email_integration.py",
    "chars": 17607,
    "preview": "\"\"\"\nEmail Integration Tests\n邮件集成测试 - 测试完整的邮件发送流程\n包括:注册验证、密码重置、评论通知等\n\"\"\"\nimport re\nfrom django.test import TestCase, Clie"
  },
  {
    "path": "djangoblog/test_email_integration_complete.py",
    "chars": 19245,
    "preview": "\"\"\"\nComplete Email Integration Tests - End to End\n完整的邮件集成测试 - 端到端测试完整业务流程\n\"\"\"\nimport re\nfrom django.test import TestCase"
  },
  {
    "path": "djangoblog/test_plugins.py",
    "chars": 7715,
    "preview": "\"\"\"\n插件系统测试\n测试插件加载、钩子注册和执行\n\"\"\"\nimport os\nfrom unittest.mock import Mock, patch\n\nfrom django.test import TestCase\n\nfrom dj"
  },
  {
    "path": "djangoblog/tests.py",
    "chars": 607,
    "preview": "from django.test import TestCase\n\nfrom djangoblog.utils import *\n\n\nclass DjangoBlogTest(TestCase):\n    def setUp(self):\n"
  },
  {
    "path": "djangoblog/urls.py",
    "chars": 2973,
    "preview": "\"\"\"djangoblog URL Configuration\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https"
  },
  {
    "path": "djangoblog/utils.py",
    "chars": 8576,
    "preview": "#!/usr/bin/env python\n# encoding: utf-8\n\n\nimport logging\nimport os\nimport random\nimport string\nimport uuid\nimport hashli"
  },
  {
    "path": "djangoblog/whoosh_cn_backend.py",
    "chars": 39167,
    "preview": "# encoding: utf-8\n\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport json\nimpor"
  },
  {
    "path": "djangoblog/wsgi.py",
    "chars": 398,
    "preview": "\"\"\"\nWSGI config for djangoblog project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n"
  },
  {
    "path": "docs/README-en.md",
    "chars": 8925,
    "preview": "# DjangoBlog\n\n<p align=\"center\">\n  <a href=\"https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml\"><im"
  },
  {
    "path": "docs/config-en.md",
    "chars": 5893,
    "preview": "# Introduction to Main Features Settings\n\n## Cache Configuration\n\nThe cache uses `localmem` (local memory cache) by defa"
  },
  {
    "path": "docs/config.md",
    "chars": 3528,
    "preview": "# 主要功能配置介绍\n\n## 缓存配置\n\n缓存默认使用 `localmem`(本地内存缓存)。如果你有 Redis 环境,可以通过设置 `DJANGO_REDIS_URL` 环境变量来自动切换到 Redis 缓存。\n\n### 使用 Redi"
  },
  {
    "path": "docs/docker-en.md",
    "chars": 7459,
    "preview": "# Deploying DjangoBlog with Docker\n\n![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog)\n![Docke"
  },
  {
    "path": "docs/docker.md",
    "chars": 5610,
    "preview": "# 使用 Docker 部署 DjangoBlog\n\n![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog)\n![Docker Image V"
  },
  {
    "path": "docs/es.md",
    "chars": 602,
    "preview": "# 集成Elasticsearch\n如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单,\n首先需要注意如下几点:\n1. 你的`Elasticsearch`支"
  },
  {
    "path": "docs/k8s-en.md",
    "chars": 5353,
    "preview": "# Deploying DjangoBlog with Kubernetes\n\nThis document guides you through deploying the DjangoBlog application on a Kuber"
  },
  {
    "path": "docs/k8s.md",
    "chars": 3130,
    "preview": "# 使用 Kubernetes 部署 DjangoBlog\n\n本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目"
  },
  {
    "path": "docs/search-engine-config.md",
    "chars": 5003,
    "preview": "# 搜索引擎配置说明\n\n## 概述\n\nDjangoBlog 支持两种搜索引擎:\n- **Whoosh** - 纯 Python 实现,开箱即用(默认)\n- **Elasticsearch** - 高性能分布式搜索引擎(推荐生产环境)\n\n配置"
  },
  {
    "path": "frontend/.gitignore",
    "chars": 106,
    "preview": "node_modules/\ndist/\n.DS_Store\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n.vite/\n"
  },
  {
    "path": "frontend/package.json",
    "chars": 763,
    "preview": "{\n  \"name\": \"djangoblog-frontend\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Modern frontend for DjangoBlog\",\n  \"type\": \"m"
  },
  {
    "path": "frontend/postcss.config.js",
    "chars": 1914,
    "preview": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n    // 生产环境 CSS 压缩\n    ...(process.env.NODE_ENV"
  },
  {
    "path": "frontend/src/components/backToTop.js",
    "chars": 1564,
    "preview": "/**\n * 回到顶部组件\n * 替代原有的jQuery实现\n */\n\nexport default () => ({\n  // ==================== 状态 ====================\n  isVisibl"
  },
  {
    "path": "frontend/src/components/commentSystem.js",
    "chars": 4783,
    "preview": "/**\n * 评论系统组件\n * 使用Alpine.js重构,替代原有的jQuery实现\n */\n\nexport default () => ({\n  // ==================== 状态管理 ==============="
  },
  {
    "path": "frontend/src/components/imageLightbox.js",
    "chars": 1347,
    "preview": "/**\n * 图片灯箱组件\n * 点击文章内容中的图片可以查看大图\n */\nexport default function imageLightbox() {\n  return {\n    showLightbox: false,\n    "
  },
  {
    "path": "frontend/src/components/navigation.js",
    "chars": 2238,
    "preview": "/**\n * 导航栏组件\n * 处理移动端菜单、搜索等交互\n */\n\nexport default () => ({\n  // ==================== 状态 ====================\n  menuOpen:"
  },
  {
    "path": "frontend/src/components/reactionPicker.js",
    "chars": 8079,
    "preview": "/**\n * Emoji Reaction Picker 组件\n * 为评论添加 GitHub 风格的 emoji 反应功能\n */\n\nexport default (commentId) => {\n  return {\n    // =="
  },
  {
    "path": "frontend/src/features/darkMode.js",
    "chars": 2922,
    "preview": "/**\n * Dark Mode 核心功能\n * 实现主题切换、持久化存储和系统主题跟随\n */\n\nconst STORAGE_KEY = 'dark-mode-enabled';\nconst THEME_ATTR = 'data-them"
  },
  {
    "path": "frontend/src/main.js",
    "chars": 2558,
    "preview": "/**\n * DjangoBlog 前端主入口文件\n * 使用 Alpine.js + HTMX 实现现代化服务端渲染\n */\n\n// 导入样式文件(Vite开发模式必需)\nimport './styles/main.css';\n\nimpo"
  },
  {
    "path": "frontend/src/styles/main.css",
    "chars": 78586,
    "preview": "/**\n * DjangoBlog 现代化样式系统\n * 完全基于 Tailwind CSS 重写,移除所有旧的CSS依赖\n *\n * 架构:\n * 1. Tailwind 基础层 - 重置和基础样式\n * 2. 组件层 - 可复用的组件类"
  },
  {
    "path": "frontend/src/utils/nprogress.js",
    "chars": 3126,
    "preview": "/**\n * 简化版 NProgress 进度条\n * 保留原有功能\n */\n\nconst NProgress = {\n  settings: {\n    minimum: 0.08,\n    easing: 'ease',\n    spe"
  },
  {
    "path": "frontend/tailwind.config.js",
    "chars": 3350,
    "preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n  // 扫描这些文件以提取使用的CSS类\n  content: [\n    \"./src/**/*.{js,jsx,"
  },
  {
    "path": "frontend/vite.config.js",
    "chars": 3161,
    "preview": "import { defineConfig } from 'vite';\nimport path from 'path';\n\nexport default defineConfig({\n  // 构建配置\n  build: {\n    //"
  },
  {
    "path": "locale/en/LC_MESSAGES/django.po",
    "chars": 16729,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "locale/zh_Hans/LC_MESSAGES/django.po",
    "chars": 14205,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "locale/zh_Hant/LC_MESSAGES/django.po",
    "chars": 14214,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "manage.py",
    "chars": 808,
    "preview": "#!/usr/bin/env python\nimport os\nimport sys\n\nif __name__ == \"__main__\":\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE"
  },
  {
    "path": "oauth/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "oauth/admin.py",
    "chars": 1637,
    "preview": "import logging\n\nfrom django.contrib import admin\n# Register your models here.\nfrom django.urls import reverse\nfrom djang"
  },
  {
    "path": "oauth/apps.py",
    "chars": 85,
    "preview": "from django.apps import AppConfig\n\n\nclass OauthConfig(AppConfig):\n    name = 'oauth'\n"
  },
  {
    "path": "oauth/forms.py",
    "chars": 481,
    "preview": "from django.contrib.auth.forms import forms\nfrom django.forms import widgets\n\n\nclass RequireEmailForm(forms.Form):\n    e"
  },
  {
    "path": "oauth/migrations/0001_initial.py",
    "chars": 2898,
    "preview": "# Generated by Django 4.1.7 on 2023-03-07 09:53\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py",
    "chars": 3252,
    "preview": "# Generated by Django 4.2.5 on 2023-09-06 13:13\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "oauth/migrations/0003_alter_oauthuser_nickname.py",
    "chars": 455,
    "preview": "# Generated by Django 4.2.7 on 2024-01-26 02:41\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "oauth/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "oauth/models.py",
    "chars": 2429,
    "preview": "# Create your models here.\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom djan"
  },
  {
    "path": "oauth/oauthmanager.py",
    "chars": 16203,
    "preview": "import json\nimport logging\nimport os\nimport urllib.parse\nfrom abc import ABCMeta, abstractmethod\n\nimport requests\n\nfrom "
  },
  {
    "path": "oauth/templatetags/__init__.py",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "oauth/templatetags/oauth_tags.py",
    "chars": 617,
    "preview": "from django import template\nfrom django.urls import reverse\n\nfrom oauth.oauthmanager import get_oauth_apps\n\nregister = t"
  },
  {
    "path": "oauth/test_oauth_business_logic.py",
    "chars": 14403,
    "preview": "\"\"\"\nTest cases for OAuth business logic\n包括OAuth配置、OAuth用户、第三方登录等核心业务逻辑\n\"\"\"\nfrom django.test import TestCase\n\nfrom accoun"
  },
  {
    "path": "oauth/tests.py",
    "chars": 9985,
    "preview": "import json\nfrom unittest.mock import patch\n\nfrom django.conf import settings\nfrom django.contrib import auth\nfrom djang"
  },
  {
    "path": "oauth/urls.py",
    "chars": 612,
    "preview": "from django.urls import path\n\nfrom . import views\n\napp_name = \"oauth\"\nurlpatterns = [\n    path(\n        r'oauth/authoriz"
  },
  {
    "path": "oauth/views.py",
    "chars": 10205,
    "preview": "import logging\n# Create your views here.\nfrom urllib.parse import urlparse\n\nfrom django.conf import settings\nfrom django"
  },
  {
    "path": "owntracks/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "owntracks/admin.py",
    "chars": 117,
    "preview": "from django.contrib import admin\n\n# Register your models here.\n\n\nclass OwnTrackLogsAdmin(admin.ModelAdmin):\n    pass\n"
  },
  {
    "path": "owntracks/apps.py",
    "chars": 93,
    "preview": "from django.apps import AppConfig\n\n\nclass OwntracksConfig(AppConfig):\n    name = 'owntracks'\n"
  },
  {
    "path": "owntracks/migrations/0001_initial.py",
    "chars": 1014,
    "preview": "# Generated by Django 4.1.7 on 2023-03-02 07:14\n\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n"
  },
  {
    "path": "owntracks/migrations/0002_alter_owntracklog_options_and_more.py",
    "chars": 611,
    "preview": "# Generated by Django 4.2.5 on 2023-09-06 13:19\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "owntracks/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "owntracks/models.py",
    "chars": 583,
    "preview": "from django.db import models\nfrom django.utils.timezone import now\n\n\n# Create your models here.\n\nclass OwnTrackLog(model"
  },
  {
    "path": "owntracks/tests.py",
    "chars": 1828,
    "preview": "import json\n\nfrom django.test import Client, RequestFactory, TestCase\n\nfrom accounts.models import BlogUser\nfrom .models"
  },
  {
    "path": "owntracks/urls.py",
    "chars": 381,
    "preview": "from django.urls import path\n\nfrom . import views\n\napp_name = \"owntracks\"\n\nurlpatterns = [\n    path('owntracks/logtracks"
  },
  {
    "path": "owntracks/views.py",
    "chars": 3882,
    "preview": "# Create your views here.\nimport datetime\nimport itertools\nimport json\nimport logging\nfrom datetime import timezone\nfrom"
  },
  {
    "path": "plugins/__init__.py",
    "chars": 40,
    "preview": "# This file makes this a Python package\n"
  },
  {
    "path": "plugins/article_copyright/__init__.py",
    "chars": 40,
    "preview": "# This file makes this a Python package\n"
  },
  {
    "path": "plugins/article_copyright/plugin.py",
    "chars": 1195,
    "preview": "from djangoblog.plugin_manage.base_plugin import BasePlugin\nfrom djangoblog.plugin_manage import hooks\nfrom djangoblog.p"
  },
  {
    "path": "plugins/article_recommendation/__init__.py",
    "chars": 9,
    "preview": "# 文章推荐插件\n"
  },
  {
    "path": "plugins/article_recommendation/plugin.py",
    "chars": 6782,
    "preview": "import logging\nfrom djangoblog.plugin_manage.base_plugin import BasePlugin\nfrom djangoblog.plugin_manage import hooks\nfr"
  },
  {
    "path": "plugins/article_recommendation/static/article_recommendation/css/recommendation.css",
    "chars": 3969,
    "preview": "/* 文章推荐插件样式 - 与网站风格保持一致 */\n\n/* 文章底部推荐样式 */\n.article-recommendations {\n    margin: 30px 0;\n    padding: 20px;\n    backgro"
  },
  {
    "path": "plugins/article_recommendation/static/article_recommendation/js/recommendation.js",
    "chars": 2821,
    "preview": "/**\n * 文章推荐插件JavaScript\n */\n\n(function() {\n    'use strict';\n    \n    // 等待DOM加载完成\n    document.addEventListener('DOMCon"
  },
  {
    "path": "plugins/cloudflare_cache/__init__.py",
    "chars": 131,
    "preview": "\"\"\"\nCloudflare Cache Plugin for DjangoBlog\n\nAutomatically purges Cloudflare cache when content changes.\n\"\"\"\n\n__version__"
  },
  {
    "path": "plugins/cloudflare_cache/api.py",
    "chars": 5709,
    "preview": "\"\"\"\nCloudflare API 封装\n\n提供与Cloudflare API交互的功能,用于清除缓存。\n\"\"\"\n\nimport logging\nimport requests\nfrom typing import List, Dict,"
  },
  {
    "path": "plugins/cloudflare_cache/handlers.py",
    "chars": 7377,
    "preview": "\"\"\"\nDjango信号处理器\n\n监听模型变更事件,触发Cloudflare缓存清除。\n\"\"\"\n\nimport logging\nfrom typing import List\nfrom django.contrib.admin.models"
  },
  {
    "path": "plugins/cloudflare_cache/plugin.py",
    "chars": 8678,
    "preview": "\"\"\"\nCloudflare 缓存管理插件\n\n自动清除Cloudflare CDN缓存,确保内容更新后用户能看到最新内容。\n\n功能特性:\n- 文章发布/修改时自动清除相关页面缓存\n- 评论发布时清除文章页面缓存\n- 智能过滤浏览量更新,避免"
  },
  {
    "path": "plugins/external_links/__init__.py",
    "chars": 40,
    "preview": "# This file makes this a Python package\n"
  },
  {
    "path": "plugins/external_links/plugin.py",
    "chars": 1623,
    "preview": "import re\nfrom urllib.parse import urlparse\nfrom djangoblog.plugin_manage.base_plugin import BasePlugin\nfrom djangoblog."
  },
  {
    "path": "plugins/image_lazy_loading/__init__.py",
    "chars": 28,
    "preview": "# Image Lazy Loading Plugin\n"
  }
]

// ... and 68 more files (download for full content)

About this extraction

This page contains the full source code of the liangliangyy/DjangoBlog GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 268 files (1.0 MB), approximately 283.7k tokens, and a symbol index with 1161 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!