diff --git a/src/DjangoBlog-master/.coveragerc b/src/DjangoBlog-master/.coveragerc
new file mode 100644
index 0000000..9757484
--- /dev/null
+++ b/src/DjangoBlog-master/.coveragerc
@@ -0,0 +1,10 @@
+[run]
+source = .
+include = *.py
+omit =
+ *migrations*
+ *tests*
+ *.html
+ *whoosh_cn_backend*
+ *settings.py*
+ *venv*
diff --git a/src/DjangoBlog-master/.dockerignore b/src/DjangoBlog-master/.dockerignore
new file mode 100644
index 0000000..2818c38
--- /dev/null
+++ b/src/DjangoBlog-master/.dockerignore
@@ -0,0 +1,11 @@
+bin/data/
+# virtualenv
+venv/
+collectedstatic/
+djangoblog/whoosh_index/
+uploads/
+settings_production.py
+*.md
+docs/
+logs/
+static/
\ No newline at end of file
diff --git a/src/DjangoBlog-master/.gitattributes b/src/DjangoBlog-master/.gitattributes
new file mode 100644
index 0000000..fd52ece
--- /dev/null
+++ b/src/DjangoBlog-master/.gitattributes
@@ -0,0 +1,6 @@
+blog/static/* linguist-vendored
+*.js linguist-vendored
+*.css linguist-vendored
+* text=auto
+*.sh text eol=lf
+*.conf text eol=lf
\ No newline at end of file
diff --git a/src/DjangoBlog-master/.github/ISSUE_TEMPLATE.md b/src/DjangoBlog-master/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..2b5b7aa
--- /dev/null
+++ b/src/DjangoBlog-master/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,18 @@
+
+
+**我确定我已经查看了** (标注`[ ]`为`[x]`)
+
+- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
+- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
+- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
+
+----
+
+**我要申请** (标注`[ ]`为`[x]`)
+
+- [ ] BUG 反馈
+- [ ] 添加新的特性或者功能
+- [ ] 请求技术支持
diff --git a/src/DjangoBlog-master/.github/workflows/codeql-analysis.yml b/src/DjangoBlog-master/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..6b76522
--- /dev/null
+++ b/src/DjangoBlog-master/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,47 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ - '**/*.yml'
+ - '**/*.txt'
+ pull_request:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ - '**/*.yml'
+ - '**/*.txt'
+ schedule:
+ - cron: '30 1 * * 0'
+
+
+jobs:
+ CodeQL-Build:
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
\ No newline at end of file
diff --git a/src/DjangoBlog-master/.github/workflows/django.yml b/src/DjangoBlog-master/.github/workflows/django.yml
new file mode 100644
index 0000000..94baea9
--- /dev/null
+++ b/src/DjangoBlog-master/.github/workflows/django.yml
@@ -0,0 +1,136 @@
+name: Django CI
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ pull_request:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+
+jobs:
+ build-normal:
+ runs-on: ubuntu-latest
+ strategy:
+ max-parallel: 4
+ matrix:
+ python-version: ["3.10","3.11" ]
+
+ steps:
+ - name: Start MySQL
+ uses: samin/mysql-action@v1.3
+ with:
+ host port: 3306
+ container port: 3306
+ character set server: utf8mb4
+ collation server: utf8mb4_general_ci
+ mysql version: latest
+ mysql root password: root
+ mysql database: djangoblog
+ mysql user: root
+ mysql password: root
+
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ - name: Install Dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ - name: Run Tests
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ run: |
+ python manage.py makemigrations
+ python manage.py migrate
+ python manage.py test
+
+ build-with-es:
+ runs-on: ubuntu-latest
+ strategy:
+ max-parallel: 4
+ matrix:
+ python-version: ["3.10","3.11" ]
+
+ steps:
+ - name: Start MySQL
+ uses: samin/mysql-action@v1.3
+ with:
+ host port: 3306
+ container port: 3306
+ character set server: utf8mb4
+ collation server: utf8mb4_general_ci
+ mysql version: latest
+ mysql root password: root
+ mysql database: djangoblog
+ mysql user: root
+ mysql password: root
+
+ - name: Configure sysctl limits
+ run: |
+ sudo swapoff -a
+ sudo sysctl -w vm.swappiness=1
+ sudo sysctl -w fs.file-max=262144
+ sudo sysctl -w vm.max_map_count=262144
+
+ - uses: miyataka/elasticsearch-github-actions@1
+
+ with:
+ stack-version: '7.12.1'
+ plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
+
+
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ - name: Install Dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ - name: Run Tests
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
+ run: |
+ python manage.py makemigrations
+ python manage.py migrate
+ coverage run manage.py test
+ coverage xml
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v1
+
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ push: false
+ tags: djangoblog/djangoblog:dev
diff --git a/src/DjangoBlog-master/.github/workflows/docker.yml b/src/DjangoBlog-master/.github/workflows/docker.yml
new file mode 100644
index 0000000..a312e2f
--- /dev/null
+++ b/src/DjangoBlog-master/.github/workflows/docker.yml
@@ -0,0 +1,43 @@
+name: docker
+
+on:
+ push:
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.yml'
+ branches:
+ - 'master'
+ - 'dev'
+
+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@v3
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ push: true
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}
+
+
diff --git a/src/DjangoBlog-master/.github/workflows/publish-release.yml b/src/DjangoBlog-master/.github/workflows/publish-release.yml
new file mode 100644
index 0000000..5eb0853
--- /dev/null
+++ b/src/DjangoBlog-master/.github/workflows/publish-release.yml
@@ -0,0 +1,39 @@
+name: publish release
+
+on:
+ release:
+ types: [ published ]
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v3
+ with:
+ images: name/app
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ push: true
+ platforms: |
+ linux/amd64
+ linux/arm64
+ linux/arm/v7
+ linux/arm/v6
+ linux/386
+ tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}
diff --git a/src/DjangoBlog-master/.gitignore b/src/DjangoBlog-master/.gitignore
new file mode 100644
index 0000000..3015816
--- /dev/null
+++ b/src/DjangoBlog-master/.gitignore
@@ -0,0 +1,80 @@
+# 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
+static/
+# virtualenv
+venv/
+
+collectedstatic/
+djangoblog/whoosh_index/
+google93fd32dbd906620a.html
+baidu_verify_FlHL7cUyC9.html
+BingSiteAuth.xml
+cb9339dbe2ff86a5aa169d28dba5f615.txt
+werobot_session.*
+django.jpg
+uploads/
+settings_production.py
+werobot_session.db
+bin/datas/
diff --git a/src/DjangoBlog-master/Dockerfile b/src/DjangoBlog-master/Dockerfile
new file mode 100644
index 0000000..80b46ac
--- /dev/null
+++ b/src/DjangoBlog-master/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.11
+ENV PYTHONUNBUFFERED 1
+WORKDIR /code/djangoblog/
+RUN apt-get update && \
+ apt-get install default-libmysqlclient-dev gettext -y && \
+ rm -rf /var/lib/apt/lists/*
+ADD requirements.txt requirements.txt
+RUN pip install --upgrade pip && \
+ pip install --no-cache-dir -r requirements.txt && \
+ pip install --no-cache-dir gunicorn[gevent] && \
+ pip cache purge
+
+ADD . .
+RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
+ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
diff --git a/src/DjangoBlog-master/LICENSE b/src/DjangoBlog-master/LICENSE
new file mode 100644
index 0000000..3b08474
--- /dev/null
+++ b/src/DjangoBlog-master/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2025 车亮亮
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/src/DjangoBlog-master/README.md b/src/DjangoBlog-master/README.md
new file mode 100644
index 0000000..56aa4cc
--- /dev/null
+++ b/src/DjangoBlog-master/README.md
@@ -0,0 +1,158 @@
+# DjangoBlog
+
+
+
+
+
+
+
+
+
+ 一款功能强大、设计优雅的现代化博客系统
+
+ English • 简体中文
+
+
+---
+
+DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能,还通过一个灵活的插件系统,让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者,DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
+
+## ✨ 特性亮点
+
+- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
+- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
+- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
+- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
+- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
+- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
+- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
+- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能,代码解耦,易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
+- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
+- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
+- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
+
+## 🛠️ 技术栈
+
+- **后端**: Python 3.10, Django 4.0
+- **数据库**: MySQL, SQLite (可配置)
+- **缓存**: Redis
+- **前端**: HTML5, CSS3, JavaScript
+- **搜索**: Whoosh, Elasticsearch (可配置)
+- **编辑器**: Markdown (mdeditor)
+
+## 🚀 快速开始
+
+### 1. 环境准备
+
+确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
+
+### 2. 克隆与安装
+
+```bash
+# 克隆项目到本地
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
+
+# 安装依赖
+pip install -r requirements.txt
+```
+
+### 3. 项目配置
+
+- **数据库**:
+ 打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ 在 MySQL 中创建数据库:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **更多配置**:
+ 关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
+
+### 4. 初始化数据库
+
+```bash
+python manage.py makemigrations
+python manage.py migrate
+
+# 创建一个超级管理员账户
+python manage.py createsuperuser
+```
+
+### 5. 运行项目
+
+```bash
+# (可选) 生成一些测试数据
+python manage.py create_testdata
+
+# (可选) 收集和压缩静态文件
+python manage.py collectstatic --noinput
+python manage.py compress --force
+
+# 启动开发服务器
+python manage.py runserver
+```
+
+现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
+
+## 部署
+
+- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
+- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术,请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
+- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
+
+## 🧩 插件系统
+
+插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
+
+- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
+- **现有插件**: `view_count`(浏览计数), `seo_optimizer`(SEO优化)等都是通过插件系统实现的。
+- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
+
+## 🤝 贡献指南
+
+我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Issue 或 Pull Request。
+
+## 📄 许可证
+
+本项目基于 [MIT License](LICENSE) 开源。
+
+---
+
+## ❤️ 支持与赞助
+
+如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
+
+
+
+
+
+
+ (左) 支付宝 / (右) 微信
+
+
+## 🙏 鸣谢
+
+特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
+
+
+
+
+
+
+
+---
+> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。
diff --git a/src/DjangoBlog-master/accounts/__init__.py b/src/DjangoBlog-master/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/accounts/admin.py b/src/DjangoBlog-master/accounts/admin.py
new file mode 100644
index 0000000..32e483c
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/admin.py
@@ -0,0 +1,59 @@
+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',)
diff --git a/src/DjangoBlog-master/accounts/apps.py b/src/DjangoBlog-master/accounts/apps.py
new file mode 100644
index 0000000..9b3fc5a
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'accounts'
diff --git a/src/DjangoBlog-master/accounts/forms.py b/src/DjangoBlog-master/accounts/forms.py
new file mode 100644
index 0000000..fce4137
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/forms.py
@@ -0,0 +1,117 @@
+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'),
+ )
diff --git a/src/DjangoBlog-master/accounts/migrations/0001_initial.py b/src/DjangoBlog-master/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..d2fbcab
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BlogUser',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+ ],
+ options={
+ 'verbose_name': '用户',
+ 'verbose_name_plural': '用户',
+ 'ordering': ['-id'],
+ 'get_latest_by': 'id',
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ ]
diff --git a/src/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
new file mode 100644
index 0000000..1a9f509
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -0,0 +1,46 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='bloguser',
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='nickname',
+ field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='source',
+ field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/accounts/migrations/__init__.py b/src/DjangoBlog-master/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/accounts/models.py b/src/DjangoBlog-master/accounts/models.py
new file mode 100644
index 0000000..3baddbb
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/models.py
@@ -0,0 +1,35 @@
+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'
diff --git a/src/DjangoBlog-master/accounts/templatetags/__init__.py b/src/DjangoBlog-master/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/accounts/tests.py b/src/DjangoBlog-master/accounts/tests.py
new file mode 100644
index 0000000..6893411
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/tests.py
@@ -0,0 +1,207 @@
+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)
+
diff --git a/src/DjangoBlog-master/accounts/urls.py b/src/DjangoBlog-master/accounts/urls.py
new file mode 100644
index 0000000..107a801
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/urls.py
@@ -0,0 +1,28 @@
+from django.urls import path
+from django.urls import re_path
+
+from . import views
+from .forms import LoginForm
+
+app_name = "accounts"
+
+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'),
+ ]
diff --git a/src/DjangoBlog-master/accounts/user_login_backend.py b/src/DjangoBlog-master/accounts/user_login_backend.py
new file mode 100644
index 0000000..73cdca1
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/user_login_backend.py
@@ -0,0 +1,26 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+
+
+class EmailOrUsernameModelBackend(ModelBackend):
+ """
+ 允许使用用户名或邮箱登录
+ """
+
+ 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
diff --git a/src/DjangoBlog-master/accounts/utils.py b/src/DjangoBlog-master/accounts/utils.py
new file mode 100644
index 0000000..4b94bdf
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/utils.py
@@ -0,0 +1,49 @@
+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)
diff --git a/src/DjangoBlog-master/accounts/views.py b/src/DjangoBlog-master/accounts/views.py
new file mode 100644
index 0000000..ae67aec
--- /dev/null
+++ b/src/DjangoBlog-master/accounts/views.py
@@ -0,0 +1,204 @@
+import logging
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth import get_user_model
+from django.contrib.auth import logout
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.hashers import make_password
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.views import View
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
+from django.views.generic import FormView, RedirectView
+
+from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
+from . import utils
+from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
+from .models import BlogUser
+
+logger = logging.getLogger(__name__)
+
+
+# Create your views here.
+
+class RegisterView(FormView):
+ form_class = RegisterForm
+ template_name = 'account/registration_form.html'
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ return super(RegisterView, self).dispatch(*args, **kwargs)
+
+ 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 = """
+ 请点击下面链接验证您的邮箱
+
+ {url}
+
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {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(RedirectView):
+ url = '/login/'
+
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ return super(LogoutView, self).dispatch(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ logout(request)
+ delete_sidebar_cache()
+ return super(LogoutView, self).get(request, *args, **kwargs)
+
+
+class LoginView(FormView):
+ form_class = LoginForm
+ template_name = 'account/login.html'
+ success_url = '/'
+ redirect_field_name = REDIRECT_FIELD_NAME
+ login_ttl = 2626560 # 一个月的时间
+
+ @method_decorator(sensitive_post_parameters('password'))
+ @method_decorator(csrf_protect)
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+
+ return super(LoginView, self).dispatch(request, *args, **kwargs)
+
+ 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(self.login_ttl)
+ return super(LoginView, self).form_valid(form)
+ # 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(FormView):
+ 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")
diff --git a/src/DjangoBlog-master/blog/__init__.py b/src/DjangoBlog-master/blog/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/blog/admin.py b/src/DjangoBlog-master/blog/admin.py
new file mode 100644
index 0000000..46c3420
--- /dev/null
+++ b/src/DjangoBlog-master/blog/admin.py
@@ -0,0 +1,112 @@
+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
+
+
+class ArticleForm(forms.ModelForm):
+ # body = forms.CharField(widget=AdminPagedownWidget())
+
+ class Meta:
+ model = Article
+ fields = '__all__'
+
+
+def makr_article_publish(modeladmin, request, queryset):
+ queryset.update(status='p')
+
+
+def draft_article(modeladmin, request, queryset):
+ queryset.update(status='d')
+
+
+def close_article_commentstatus(modeladmin, request, queryset):
+ queryset.update(comment_status='c')
+
+
+def open_article_commentstatus(modeladmin, request, queryset):
+ queryset.update(comment_status='o')
+
+
+makr_article_publish.short_description = _('Publish selected articles')
+draft_article.short_description = _('Draft selected articles')
+close_article_commentstatus.short_description = _('Close article comments')
+open_article_commentstatus.short_description = _('Open article comments')
+
+
+class ArticlelAdmin(admin.ModelAdmin):
+ list_per_page = 20
+ search_fields = ('body', 'title')
+ form = ArticleForm
+ list_display = (
+ 'id',
+ 'title',
+ 'author',
+ 'link_to_category',
+ 'creation_time',
+ 'views',
+ 'status',
+ 'type',
+ 'article_order')
+ list_display_links = ('id', 'title')
+ list_filter = ('status', 'type', 'category')
+ filter_horizontal = ('tags',)
+ exclude = ('creation_time', 'last_modify_time')
+ view_on_site = True
+ actions = [
+ makr_article_publish,
+ draft_article,
+ close_article_commentstatus,
+ open_article_commentstatus]
+
+ def link_to_category(self, obj):
+ info = (obj.category._meta.app_label, obj.category._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
+ return format_html(u'%s ' % (link, obj.category.name))
+
+ link_to_category.short_description = _('category')
+
+ def get_form(self, request, obj=None, **kwargs):
+ form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
+ form.base_fields['author'].queryset = get_user_model(
+ ).objects.filter(is_superuser=True)
+ return form
+
+ def save_model(self, request, obj, form, change):
+ super(ArticlelAdmin, self).save_model(request, obj, form, change)
+
+ def get_view_on_site_url(self, obj=None):
+ if obj:
+ url = obj.get_full_url()
+ return url
+ else:
+ from djangoblog.utils import get_current_site
+ site = get_current_site().domain
+ return site
+
+
+class TagAdmin(admin.ModelAdmin):
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = ('name', 'parent_category', 'index')
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+class LinksAdmin(admin.ModelAdmin):
+ exclude = ('last_mod_time', 'creation_time')
+
+
+class SideBarAdmin(admin.ModelAdmin):
+ list_display = ('name', 'content', 'is_enable', 'sequence')
+ exclude = ('last_mod_time', 'creation_time')
+
+
+class BlogSettingsAdmin(admin.ModelAdmin):
+ pass
diff --git a/src/DjangoBlog-master/blog/apps.py b/src/DjangoBlog-master/blog/apps.py
new file mode 100644
index 0000000..7930587
--- /dev/null
+++ b/src/DjangoBlog-master/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/src/DjangoBlog-master/blog/context_processors.py b/src/DjangoBlog-master/blog/context_processors.py
new file mode 100644
index 0000000..73e3088
--- /dev/null
+++ b/src/DjangoBlog-master/blog/context_processors.py
@@ -0,0 +1,43 @@
+import logging
+
+from django.utils import timezone
+
+from djangoblog.utils import cache, get_blog_setting
+from .models import Category, Article
+
+logger = logging.getLogger(__name__)
+
+
+def seo_processor(requests):
+ key = 'seo_processor'
+ value = cache.get(key)
+ if value:
+ return value
+ else:
+ logger.info('set processor cache.')
+ setting = get_blog_setting()
+ value = {
+ 'SITE_NAME': setting.site_name,
+ 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
+ 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
+ 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
+ 'SITE_DESCRIPTION': setting.site_description,
+ 'SITE_KEYWORDS': setting.site_keywords,
+ 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
+ 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
+ 'nav_category_list': Category.objects.all(),
+ 'nav_pages': Article.objects.filter(
+ type='p',
+ status='p'),
+ 'OPEN_SITE_COMMENT': setting.open_site_comment,
+ 'BEIAN_CODE': setting.beian_code,
+ 'ANALYTICS_CODE': setting.analytics_code,
+ "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
+ "SHOW_GONGAN_CODE": setting.show_gongan_code,
+ "CURRENT_YEAR": timezone.now().year,
+ "GLOBAL_HEADER": setting.global_header,
+ "GLOBAL_FOOTER": setting.global_footer,
+ "COMMENT_NEED_REVIEW": setting.comment_need_review,
+ }
+ cache.set(key, value, 60 * 60 * 10)
+ return value
diff --git a/src/DjangoBlog-master/blog/documents.py b/src/DjangoBlog-master/blog/documents.py
new file mode 100644
index 0000000..0f1db7b
--- /dev/null
+++ b/src/DjangoBlog-master/blog/documents.py
@@ -0,0 +1,213 @@
+import time
+
+import elasticsearch.client
+from django.conf import settings
+from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
+from elasticsearch_dsl.connections import connections
+
+from blog.models import Article
+
+ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
+
+if ELASTICSEARCH_ENABLED:
+ connections.create_connection(
+ hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
+ from elasticsearch import Elasticsearch
+
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ from elasticsearch.client import IngestClient
+
+ c = IngestClient(es)
+ try:
+ c.get_pipeline('geoip')
+ except elasticsearch.exceptions.NotFoundError:
+ c.put_pipeline('geoip', body='''{
+ "description" : "Add geoip info",
+ "processors" : [
+ {
+ "geoip" : {
+ "field" : "ip"
+ }
+ }
+ ]
+ }''')
+
+
+class GeoIp(InnerDoc):
+ continent_name = Keyword()
+ country_iso_code = Keyword()
+ country_name = Keyword()
+ location = GeoPoint()
+
+
+class UserAgentBrowser(InnerDoc):
+ Family = Keyword()
+ Version = Keyword()
+
+
+class UserAgentOS(UserAgentBrowser):
+ pass
+
+
+class UserAgentDevice(InnerDoc):
+ Family = Keyword()
+ Brand = Keyword()
+ Model = Keyword()
+
+
+class UserAgent(InnerDoc):
+ browser = Object(UserAgentBrowser, required=False)
+ os = Object(UserAgentOS, required=False)
+ device = Object(UserAgentDevice, required=False)
+ string = Text()
+ is_bot = Boolean()
+
+
+class ElapsedTimeDocument(Document):
+ url = Keyword()
+ time_taken = Long()
+ log_datetime = Date()
+ ip = Keyword()
+ geoip = Object(GeoIp, required=False)
+ useragent = Object(UserAgent, required=False)
+
+ class Index:
+ name = 'performance'
+ settings = {
+ "number_of_shards": 1,
+ "number_of_replicas": 0
+ }
+
+ class Meta:
+ doc_type = 'ElapsedTime'
+
+
+class ElaspedTimeDocumentManager:
+ @staticmethod
+ def build_index():
+ from elasticsearch import Elasticsearch
+ client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ res = client.indices.exists(index="performance")
+ if not res:
+ ElapsedTimeDocument.init()
+
+ @staticmethod
+ def delete_index():
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ es.indices.delete(index='performance', ignore=[400, 404])
+
+ @staticmethod
+ def create(url, time_taken, log_datetime, useragent, ip):
+ ElaspedTimeDocumentManager.build_index()
+ ua = UserAgent()
+ ua.browser = UserAgentBrowser()
+ ua.browser.Family = useragent.browser.family
+ ua.browser.Version = useragent.browser.version_string
+
+ ua.os = UserAgentOS()
+ ua.os.Family = useragent.os.family
+ ua.os.Version = useragent.os.version_string
+
+ ua.device = UserAgentDevice()
+ ua.device.Family = useragent.device.family
+ ua.device.Brand = useragent.device.brand
+ ua.device.Model = useragent.device.model
+ ua.string = useragent.ua_string
+ ua.is_bot = useragent.is_bot
+
+ doc = ElapsedTimeDocument(
+ meta={
+ 'id': int(
+ round(
+ time.time() *
+ 1000))
+ },
+ url=url,
+ time_taken=time_taken,
+ log_datetime=log_datetime,
+ useragent=ua, ip=ip)
+ doc.save(pipeline="geoip")
+
+
+class ArticleDocument(Document):
+ body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ author = Object(properties={
+ 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ category = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ tags = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+
+ pub_time = Date()
+ status = Text()
+ comment_status = Text()
+ type = Text()
+ views = Integer()
+ article_order = Integer()
+
+ class Index:
+ name = 'blog'
+ settings = {
+ "number_of_shards": 1,
+ "number_of_replicas": 0
+ }
+
+ class Meta:
+ doc_type = 'Article'
+
+
+class ArticleDocumentManager():
+
+ def __init__(self):
+ self.create_index()
+
+ def create_index(self):
+ ArticleDocument.init()
+
+ def delete_index(self):
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ es.indices.delete(index='blog', ignore=[400, 404])
+
+ def convert_to_doc(self, articles):
+ return [
+ ArticleDocument(
+ meta={
+ 'id': article.id},
+ body=article.body,
+ title=article.title,
+ author={
+ 'nickname': article.author.username,
+ 'id': article.author.id},
+ category={
+ 'name': article.category.name,
+ 'id': article.category.id},
+ tags=[
+ {
+ 'name': t.name,
+ 'id': t.id} for t in article.tags.all()],
+ pub_time=article.pub_time,
+ status=article.status,
+ comment_status=article.comment_status,
+ type=article.type,
+ views=article.views,
+ article_order=article.article_order) for article in articles]
+
+ def rebuild(self, articles=None):
+ ArticleDocument.init()
+ articles = articles if articles else Article.objects.all()
+ docs = self.convert_to_doc(articles)
+ for doc in docs:
+ doc.save()
+
+ def update_docs(self, docs):
+ for doc in docs:
+ doc.save()
diff --git a/src/DjangoBlog-master/blog/forms.py b/src/DjangoBlog-master/blog/forms.py
new file mode 100644
index 0000000..715be76
--- /dev/null
+++ b/src/DjangoBlog-master/blog/forms.py
@@ -0,0 +1,19 @@
+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
diff --git a/src/DjangoBlog-master/blog/management/__init__.py b/src/DjangoBlog-master/blog/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/blog/management/commands/__init__.py b/src/DjangoBlog-master/blog/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/blog/management/commands/build_index.py b/src/DjangoBlog-master/blog/management/commands/build_index.py
new file mode 100644
index 0000000..3c4acd7
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/build_index.py
@@ -0,0 +1,18 @@
+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()
diff --git a/src/DjangoBlog-master/blog/management/commands/build_search_words.py b/src/DjangoBlog-master/blog/management/commands/build_search_words.py
new file mode 100644
index 0000000..cfe7e0d
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/build_search_words.py
@@ -0,0 +1,13 @@
+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))
diff --git a/src/DjangoBlog-master/blog/management/commands/clear_cache.py b/src/DjangoBlog-master/blog/management/commands/clear_cache.py
new file mode 100644
index 0000000..0d66172
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/clear_cache.py
@@ -0,0 +1,11 @@
+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'))
diff --git a/src/DjangoBlog-master/blog/management/commands/create_testdata.py b/src/DjangoBlog-master/blog/management/commands/create_testdata.py
new file mode 100644
index 0000000..675d2ba
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/create_testdata.py
@@ -0,0 +1,40 @@
+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'))
diff --git a/src/DjangoBlog-master/blog/management/commands/ping_baidu.py b/src/DjangoBlog-master/blog/management/commands/ping_baidu.py
new file mode 100644
index 0000000..2c7fbdd
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/ping_baidu.py
@@ -0,0 +1,50 @@
+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'))
diff --git a/src/DjangoBlog-master/blog/management/commands/sync_user_avatar.py b/src/DjangoBlog-master/blog/management/commands/sync_user_avatar.py
new file mode 100644
index 0000000..d0f4612
--- /dev/null
+++ b/src/DjangoBlog-master/blog/management/commands/sync_user_avatar.py
@@ -0,0 +1,47 @@
+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('结束同步')
diff --git a/src/DjangoBlog-master/blog/middleware.py b/src/DjangoBlog-master/blog/middleware.py
new file mode 100644
index 0000000..94dd70c
--- /dev/null
+++ b/src/DjangoBlog-master/blog/middleware.py
@@ -0,0 +1,42 @@
+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'', str.encode(str(cast_time)[:5]))
+ except Exception as e:
+ logger.error("Error OnlineMiddleware: %s" % e)
+
+ return response
diff --git a/src/DjangoBlog-master/blog/migrations/0001_initial.py b/src/DjangoBlog-master/blog/migrations/0001_initial.py
new file mode 100644
index 0000000..3d391b6
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0001_initial.py
@@ -0,0 +1,137 @@
+# 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',
+ },
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
new file mode 100644
index 0000000..adbaa36
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -0,0 +1,23 @@
+# 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='公共头部'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py b/src/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py
new file mode 100644
index 0000000..e9f5502
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -0,0 +1,17 @@
+# 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='评论是否需要审核'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
new file mode 100644
index 0000000..ceb1398
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -0,0 +1,27 @@
+# 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',
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
new file mode 100644
index 0000000..d08e853
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -0,0 +1,300 @@
+# 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'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py b/src/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py
new file mode 100644
index 0000000..e36feb4
--- /dev/null
+++ b/src/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py
@@ -0,0 +1,17 @@
+# 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'},
+ ),
+ ]
diff --git a/src/DjangoBlog-master/blog/migrations/__init__.py b/src/DjangoBlog-master/blog/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/blog/models.py b/src/DjangoBlog-master/blog/models.py
new file mode 100644
index 0000000..083788b
--- /dev/null
+++ b/src/DjangoBlog-master/blog/models.py
@@ -0,0 +1,376 @@
+import logging
+import re
+from abc import abstractmethod
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from mdeditor.fields import MDTextField
+from uuslug import slugify
+
+from djangoblog.utils import cache_decorator, cache
+from djangoblog.utils import get_current_site
+
+logger = logging.getLogger(__name__)
+
+
+class LinkShowType(models.TextChoices):
+ I = ('i', _('index'))
+ L = ('l', _('list'))
+ P = ('p', _('post'))
+ A = ('a', _('all'))
+ S = ('s', _('slide'))
+
+
+class BaseModel(models.Model):
+ id = models.AutoField(primary_key=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('modify time'), default=now)
+
+ def save(self, *args, **kwargs):
+ is_update_views = isinstance(
+ self,
+ Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
+ if is_update_views:
+ Article.objects.filter(pk=self.pk).update(views=self.views)
+ else:
+ if 'slug' in self.__dict__:
+ slug = getattr(
+ self, 'title') if 'title' in self.__dict__ else getattr(
+ self, 'name')
+ setattr(self, 'slug', slugify(slug))
+ super().save(*args, **kwargs)
+
+ def get_full_url(self):
+ site = get_current_site().domain
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ abstract = True
+
+ @abstractmethod
+ def get_absolute_url(self):
+ pass
+
+
+class Article(BaseModel):
+ """文章"""
+ STATUS_CHOICES = (
+ ('d', _('Draft')),
+ ('p', _('Published')),
+ )
+ COMMENT_STATUS = (
+ ('o', _('Open')),
+ ('c', _('Close')),
+ )
+ TYPE = (
+ ('a', _('Article')),
+ ('p', _('Page')),
+ )
+ title = models.CharField(_('title'), max_length=200, unique=True)
+ body = MDTextField(_('body'))
+ pub_time = models.DateTimeField(
+ _('publish time'), blank=False, null=False, default=now)
+ status = models.CharField(
+ _('status'),
+ max_length=1,
+ choices=STATUS_CHOICES,
+ default='p')
+ comment_status = models.CharField(
+ _('comment status'),
+ max_length=1,
+ choices=COMMENT_STATUS,
+ default='o')
+ type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
+ views = models.PositiveIntegerField(_('views'), default=0)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ blank=False,
+ null=False,
+ on_delete=models.CASCADE)
+ article_order = models.IntegerField(
+ _('order'), blank=False, null=False, default=0)
+ show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
+ category = models.ForeignKey(
+ 'Category',
+ verbose_name=_('category'),
+ on_delete=models.CASCADE,
+ blank=False,
+ null=False)
+ tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
+
+ def body_to_string(self):
+ return self.body
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['-article_order', '-pub_time']
+ verbose_name = _('article')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def get_absolute_url(self):
+ return reverse('blog:detailbyid', kwargs={
+ 'article_id': self.id,
+ 'year': self.creation_time.year,
+ 'month': self.creation_time.month,
+ 'day': self.creation_time.day
+ })
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ tree = self.category.get_category_tree()
+ names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
+
+ return names
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+
+ def viewed(self):
+ self.views += 1
+ self.save(update_fields=['views'])
+
+ def comment_list(self):
+ cache_key = 'article_comments_{id}'.format(id=self.id)
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get article comments:{id}'.format(id=self.id))
+ return value
+ else:
+ comments = self.comment_set.filter(is_enable=True).order_by('-id')
+ cache.set(cache_key, comments, 60 * 100)
+ logger.info('set article comments:{id}'.format(id=self.id))
+ return comments
+
+ def get_admin_url(self):
+ info = (self._meta.app_label, self._meta.model_name)
+ return reverse('admin:%s_%s_change' % info, args=(self.pk,))
+
+ @cache_decorator(expiration=60 * 100)
+ def next_article(self):
+ # 下一篇
+ return Article.objects.filter(
+ id__gt=self.id, status='p').order_by('id').first()
+
+ @cache_decorator(expiration=60 * 100)
+ def prev_article(self):
+ # 前一篇
+ return Article.objects.filter(id__lt=self.id, status='p').first()
+
+ def get_first_image_url(self):
+ """
+ Get the first image url from article.body.
+ :return:
+ """
+ match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
+ if match:
+ return match.group(1)
+ return ""
+
+
+class Category(BaseModel):
+ """文章分类"""
+ name = models.CharField(_('category name'), max_length=30, unique=True)
+ parent_category = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent category'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ index = models.IntegerField(default=0, verbose_name=_('index'))
+
+ class Meta:
+ ordering = ['-index']
+ verbose_name = _('category')
+ verbose_name_plural = verbose_name
+
+ def get_absolute_url(self):
+ return reverse(
+ 'blog:category_detail', kwargs={
+ 'category_name': self.slug})
+
+ def __str__(self):
+ return self.name
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ """
+ 递归获得分类目录的父级
+ :return:
+ """
+ categorys = []
+
+ def parse(category):
+ categorys.append(category)
+ if category.parent_category:
+ parse(category.parent_category)
+
+ parse(self)
+ return categorys
+
+ @cache_decorator(60 * 60 * 10)
+ def get_sub_categorys(self):
+ """
+ 获得当前分类目录所有子集
+ :return:
+ """
+ categorys = []
+ all_categorys = Category.objects.all()
+
+ def parse(category):
+ if category not in categorys:
+ categorys.append(category)
+ childs = all_categorys.filter(parent_category=category)
+ for child in childs:
+ if category not in categorys:
+ categorys.append(child)
+ parse(child)
+
+ parse(self)
+ return categorys
+
+
+class Tag(BaseModel):
+ """文章标签"""
+ name = models.CharField(_('tag name'), max_length=30, unique=True)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
+
+ @cache_decorator(60 * 60 * 10)
+ def get_article_count(self):
+ return Article.objects.filter(tags__name=self.name).distinct().count()
+
+ class Meta:
+ ordering = ['name']
+ verbose_name = _('tag')
+ verbose_name_plural = verbose_name
+
+
+class Links(models.Model):
+ """友情链接"""
+
+ name = models.CharField(_('link name'), max_length=30, unique=True)
+ link = models.URLField(_('link'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(
+ _('is show'), default=True, blank=False, null=False)
+ show_type = models.CharField(
+ _('show type'),
+ max_length=1,
+ choices=LinkShowType.choices,
+ default=LinkShowType.I)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('link')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+class SideBar(models.Model):
+ """侧边栏,可以展示一些html内容"""
+ name = models.CharField(_('title'), max_length=100)
+ content = models.TextField(_('content'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(_('is enable'), default=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('sidebar')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+class BlogSettings(models.Model):
+ """blog的配置"""
+ site_name = models.CharField(
+ _('site name'),
+ max_length=200,
+ null=False,
+ blank=False,
+ default='')
+ site_description = models.TextField(
+ _('site description'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ site_seo_description = models.TextField(
+ _('site seo description'), max_length=1000, null=False, blank=False, default='')
+ site_keywords = models.TextField(
+ _('site keywords'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ article_sub_length = models.IntegerField(_('article sub length'), default=300)
+ sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
+ sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
+ article_comment_count = models.IntegerField(_('article comment count'), default=5)
+ show_google_adsense = models.BooleanField(_('show adsense'), default=False)
+ google_adsense_codes = models.TextField(
+ _('adsense code'), max_length=2000, null=True, blank=True, default='')
+ open_site_comment = models.BooleanField(_('open site comment'), default=True)
+ global_header = models.TextField("公共头部", null=True, blank=True, default='')
+ global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
+ beian_code = models.CharField(
+ '备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ analytics_code = models.TextField(
+ "网站统计代码",
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ show_gongan_code = models.BooleanField(
+ '是否显示公安备案号', default=False, null=False)
+ gongan_beiancode = models.TextField(
+ '公安备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ comment_need_review = models.BooleanField(
+ '评论是否需要审核', default=False, null=False)
+
+ class Meta:
+ verbose_name = _('Website configuration')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.site_name
+
+ def clean(self):
+ if BlogSettings.objects.exclude(id=self.id).count():
+ raise ValidationError(_('There can only be one configuration'))
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ from djangoblog.utils import cache
+ cache.clear()
diff --git a/src/DjangoBlog-master/blog/search_indexes.py b/src/DjangoBlog-master/blog/search_indexes.py
new file mode 100644
index 0000000..7f1dfac
--- /dev/null
+++ b/src/DjangoBlog-master/blog/search_indexes.py
@@ -0,0 +1,13 @@
+from haystack import indexes
+
+from blog.models import Article
+
+
+class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ text = indexes.CharField(document=True, use_template=True)
+
+ def get_model(self):
+ return Article
+
+ def index_queryset(self, using=None):
+ return self.get_model().objects.filter(status='p')
diff --git a/src/DjangoBlog-master/blog/templatetags/__init__.py b/src/DjangoBlog-master/blog/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/blog/templatetags/blog_tags.py b/src/DjangoBlog-master/blog/templatetags/blog_tags.py
new file mode 100644
index 0000000..d6cd5d5
--- /dev/null
+++ b/src/DjangoBlog-master/blog/templatetags/blog_tags.py
@@ -0,0 +1,344 @@
+import hashlib
+import logging
+import random
+import urllib
+
+from django import template
+from django.conf import settings
+from django.db.models import Q
+from django.shortcuts import get_object_or_404
+from django.template.defaultfilters import stringfilter
+from django.templatetags.static import static
+from django.urls import reverse
+from django.utils.safestring import mark_safe
+
+from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
+from comments.models import Comment
+from djangoblog.utils import CommonMarkdown, sanitize_html
+from djangoblog.utils import cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+from djangoblog.plugin_manage import hooks
+
+logger = logging.getLogger(__name__)
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def head_meta(context):
+ return mark_safe(hooks.apply_filters('head_meta', '', context))
+
+
+@register.simple_tag
+def timeformat(data):
+ try:
+ return data.strftime(settings.TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.simple_tag
+def datetimeformat(data):
+ try:
+ return data.strftime(settings.DATE_TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.filter()
+@stringfilter
+def custom_markdown(content):
+ return mark_safe(CommonMarkdown.get_markdown(content))
+
+
+@register.simple_tag
+def get_markdown_toc(content):
+ from djangoblog.utils import CommonMarkdown
+ body, toc = CommonMarkdown.get_markdown_with_toc(content)
+ return mark_safe(toc)
+
+
+@register.filter()
+@stringfilter
+def comment_markdown(content):
+ content = CommonMarkdown.get_markdown(content)
+ return mark_safe(sanitize_html(content))
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncatechars_content(content):
+ """
+ 获得文章内容的摘要
+ :param content:
+ :return:
+ """
+ from django.template.defaultfilters import truncatechars_html
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ return truncatechars_html(content, blogsetting.article_sub_length)
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncate(content):
+ from django.utils.html import strip_tags
+
+ return strip_tags(content)[:150]
+
+
+@register.inclusion_tag('blog/tags/breadcrumb.html')
+def load_breadcrumb(article):
+ """
+ 获得文章面包屑
+ :param article:
+ :return:
+ """
+ names = article.get_category_tree()
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ site = get_current_site().domain
+ names.append((blogsetting.site_name, '/'))
+ names = names[::-1]
+
+ return {
+ 'names': names,
+ 'title': article.title,
+ 'count': len(names) + 1
+ }
+
+
+@register.inclusion_tag('blog/tags/article_tag_list.html')
+def load_articletags(article):
+ """
+ 文章标签
+ :param article:
+ :return:
+ """
+ tags = article.tags.all()
+ tags_list = []
+ for tag in tags:
+ url = tag.get_absolute_url()
+ count = tag.get_article_count()
+ tags_list.append((
+ url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
+ ))
+ return {
+ 'article_tags_list': tags_list
+ }
+
+
+@register.inclusion_tag('blog/tags/sidebar.html')
+def load_sidebar(user, linktype):
+ """
+ 加载侧边栏
+ :return:
+ """
+ value = cache.get("sidebar" + linktype)
+ if value:
+ value['user'] = user
+ return value
+ else:
+ logger.info('load sidebar')
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ recent_articles = Article.objects.filter(
+ status='p')[:blogsetting.sidebar_article_count]
+ sidebar_categorys = Category.objects.all()
+ extra_sidebars = SideBar.objects.filter(
+ is_enable=True).order_by('sequence')
+ most_read_articles = Article.objects.filter(status='p').order_by(
+ '-views')[:blogsetting.sidebar_article_count]
+ dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
+ links = Links.objects.filter(is_enable=True).filter(
+ Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
+ commment_list = Comment.objects.filter(is_enable=True).order_by(
+ '-id')[:blogsetting.sidebar_comment_count]
+ # 标签云 计算字体大小
+ # 根据总数计算出平均值 大小为 (数目/平均值)*步长
+ increment = 5
+ tags = Tag.objects.all()
+ sidebar_tags = None
+ if tags and len(tags) > 0:
+ s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
+ count = sum([t[1] for t in s])
+ dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
+ import random
+ sidebar_tags = list(
+ map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
+ random.shuffle(sidebar_tags)
+
+ value = {
+ 'recent_articles': recent_articles,
+ 'sidebar_categorys': sidebar_categorys,
+ 'most_read_articles': most_read_articles,
+ 'article_dates': dates,
+ 'sidebar_comments': commment_list,
+ 'sidabar_links': links,
+ 'show_google_adsense': blogsetting.show_google_adsense,
+ 'google_adsense_codes': blogsetting.google_adsense_codes,
+ 'open_site_comment': blogsetting.open_site_comment,
+ 'show_gongan_code': blogsetting.show_gongan_code,
+ 'sidebar_tags': sidebar_tags,
+ 'extra_sidebars': extra_sidebars
+ }
+ cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
+ logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
+ value['user'] = user
+ return value
+
+
+@register.inclusion_tag('blog/tags/article_meta_info.html')
+def load_article_metas(article, user):
+ """
+ 获得文章meta信息
+ :param article:
+ :return:
+ """
+ return {
+ 'article': article,
+ 'user': user
+ }
+
+
+@register.inclusion_tag('blog/tags/article_pagination.html')
+def load_pagination_info(page_obj, page_type, tag_name):
+ previous_url = ''
+ next_url = ''
+ if page_type == '':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse('blog:index_page', kwargs={'page': next_number})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:index_page', kwargs={
+ 'page': previous_number})
+ if page_type == '分类标签归档':
+ tag = get_object_or_404(Tag, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'tag_name': tag.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'tag_name': tag.slug})
+ if page_type == '作者文章归档':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'author_name': tag_name})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'author_name': tag_name})
+
+ if page_type == '分类目录归档':
+ category = get_object_or_404(Category, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'category_name': category.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'category_name': category.slug})
+
+ return {
+ 'previous_url': previous_url,
+ 'next_url': next_url,
+ 'page_obj': page_obj
+ }
+
+
+@register.inclusion_tag('blog/tags/article_info.html')
+def load_article_detail(article, isindex, user):
+ """
+ 加载文章详情
+ :param article:
+ :param isindex:是否列表页,若是列表页只显示摘要
+ :return:
+ """
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+
+ return {
+ 'article': article,
+ 'isindex': isindex,
+ 'user': user,
+ 'open_site_comment': blogsetting.open_site_comment,
+ }
+
+
+# return only the URL of the gravatar
+# TEMPLATE USE: {{ email|gravatar_url:150 }}
+@register.filter
+def gravatar_url(email, size=40):
+ """获得gravatar头像"""
+ cachekey = 'gravatat/' + email
+ url = cache.get(cachekey)
+ if url:
+ return url
+ else:
+ usermodels = OAuthUser.objects.filter(email=email)
+ if usermodels:
+ o = list(filter(lambda x: x.picture is not None, usermodels))
+ if o:
+ return o[0].picture
+ email = email.encode('utf-8')
+
+ default = static('blog/img/avatar.png')
+
+ url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
+ email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
+ cache.set(cachekey, url, 60 * 60 * 10)
+ logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
+ return url
+
+
+@register.filter
+def gravatar(email, size=40):
+ """获得gravatar头像"""
+ url = gravatar_url(email, size)
+ return mark_safe(
+ ' ' %
+ (url, size, size))
+
+
+@register.simple_tag
+def query(qs, **kwargs):
+ """ template tag which allows queryset filtering. Usage:
+ {% query books author=author as mybooks %}
+ {% for book in mybooks %}
+ ...
+ {% endfor %}
+ """
+ return qs.filter(**kwargs)
+
+
+@register.filter
+def addstr(arg1, arg2):
+ """concatenate arg1 & arg2"""
+ return str(arg1) + str(arg2)
diff --git a/src/DjangoBlog-master/blog/tests.py b/src/DjangoBlog-master/blog/tests.py
new file mode 100644
index 0000000..ee13505
--- /dev/null
+++ b/src/DjangoBlog-master/blog/tests.py
@@ -0,0 +1,232 @@
+import os
+
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.core.management import call_command
+from django.core.paginator import Paginator
+from django.templatetags.static import static
+from django.test import Client, RequestFactory, TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from accounts.models import BlogUser
+from blog.forms import BlogSearchForm
+from blog.models import Article, Category, Tag, SideBar, Links
+from blog.templatetags.blog_tags import load_pagination_info, load_articletags
+from djangoblog.utils import get_current_site, get_sha256
+from oauth.models import OAuthUser, OAuthConfig
+
+
+# Create your tests here.
+
+class ArticleTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_validate_article(self):
+ site = get_current_site().domain
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ user.set_password("liangliangyy")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+ response = self.client.get(user.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+ response = self.client.get('/admin/servermanager/emailsendlog/')
+ response = self.client.get('admin/admin/logentry/')
+ s = SideBar()
+ s.sequence = 1
+ s.name = 'test'
+ s.content = 'test content'
+ s.is_enable = True
+ s.save()
+
+ category = Category()
+ category.name = "category"
+ category.creation_time = timezone.now()
+ category.last_mod_time = timezone.now()
+ category.save()
+
+ tag = Tag()
+ tag.name = "nicetag"
+ tag.save()
+
+ article = Article()
+ article.title = "nicetitle"
+ article.body = "nicecontent"
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+
+ article.save()
+ self.assertEqual(0, article.tags.count())
+ article.tags.add(tag)
+ article.save()
+ self.assertEqual(1, article.tags.count())
+
+ for i in range(20):
+ article = Article()
+ article.title = "nicetitle" + str(i)
+ article.body = "nicetitle" + str(i)
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+ article.tags.add(tag)
+ article.save()
+ from blog.documents import ELASTICSEARCH_ENABLED
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index")
+ response = self.client.get('/search', {'q': 'nicetitle'})
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(article.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.notify(article.get_absolute_url())
+ response = self.client.get(tag.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(category.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/search', {'q': 'django'})
+ self.assertEqual(response.status_code, 200)
+ s = load_articletags(article)
+ self.assertIsNotNone(s)
+
+ self.client.login(username='liangliangyy', password='liangliangyy')
+
+ response = self.client.get(reverse('blog:archives'))
+ self.assertEqual(response.status_code, 200)
+
+ p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
+ self.check_pagination(p, '', '')
+
+ p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类标签归档', tag.slug)
+
+ p = Paginator(
+ Article.objects.filter(
+ author__username='liangliangyy'), settings.PAGINATE_BY)
+ self.check_pagination(p, '作者文章归档', 'liangliangyy')
+
+ p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类目录归档', category.slug)
+
+ f = BlogSearchForm()
+ f.search()
+ # self.client.login(username='liangliangyy', password='liangliangyy')
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.baidu_notify([article.get_full_url()])
+
+ from blog.templatetags.blog_tags import gravatar_url, gravatar
+ u = gravatar_url('liangliangyy@gmail.com')
+ u = gravatar('liangliangyy@gmail.com')
+
+ link = Links(
+ sequence=1,
+ name="lylinux",
+ link='https://wwww.lylinux.net')
+ link.save()
+ response = self.client.get('/links.html')
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/feed/')
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get('/sitemap.xml')
+ self.assertEqual(response.status_code, 200)
+
+ self.client.get("/admin/blog/article/1/delete/")
+ self.client.get('/admin/servermanager/emailsendlog/')
+ self.client.get('/admin/admin/logentry/')
+ self.client.get('/admin/admin/logentry/1/change/')
+
+ def check_pagination(self, p, type, value):
+ for page in range(1, p.num_pages + 1):
+ s = load_pagination_info(p.page(page), type, value)
+ self.assertIsNotNone(s)
+ if s['previous_url']:
+ response = self.client.get(s['previous_url'])
+ self.assertEqual(response.status_code, 200)
+ if s['next_url']:
+ response = self.client.get(s['next_url'])
+ self.assertEqual(response.status_code, 200)
+
+ def test_image(self):
+ import requests
+ rsp = requests.get(
+ 'https://www.python.org/static/img/python-logo.png')
+ imagepath = os.path.join(settings.BASE_DIR, 'python.png')
+ with open(imagepath, 'wb') as file:
+ file.write(rsp.content)
+ rsp = self.client.post('/upload')
+ self.assertEqual(rsp.status_code, 403)
+ sign = get_sha256(get_sha256(settings.SECRET_KEY))
+ with open(imagepath, 'rb') as file:
+ imgfile = SimpleUploadedFile(
+ 'python.png', file.read(), content_type='image/jpg')
+ form_data = {'python.png': imgfile}
+ rsp = self.client.post(
+ '/upload?sign=' + sign, form_data, follow=True)
+ self.assertEqual(rsp.status_code, 200)
+ os.remove(imagepath)
+ from djangoblog.utils import save_user_avatar, send_email
+ send_email(['qq@qq.com'], 'testTitle', 'testContent')
+ save_user_avatar(
+ 'https://www.python.org/static/img/python-logo.png')
+
+ def test_errorpage(self):
+ rsp = self.client.get('/eee')
+ self.assertEqual(rsp.status_code, 404)
+
+ def test_commands(self):
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ user.set_password("liangliangyy")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+
+ c = OAuthConfig()
+ c.type = 'qq'
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+
+ u = OAuthUser()
+ u.type = 'qq'
+ u.openid = 'openid'
+ u.user = user
+ u.picture = static("/blog/img/avatar.png")
+ u.metadata = '''
+{
+"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+}'''
+ u.save()
+
+ u = OAuthUser()
+ u.type = 'qq'
+ u.openid = 'openid1'
+ u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
+ u.metadata = '''
+ {
+ "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+ }'''
+ u.save()
+
+ from blog.documents import ELASTICSEARCH_ENABLED
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index")
+ call_command("ping_baidu", "all")
+ call_command("create_testdata")
+ call_command("clear_cache")
+ call_command("sync_user_avatar")
+ call_command("build_search_words")
diff --git a/src/DjangoBlog-master/blog/urls.py b/src/DjangoBlog-master/blog/urls.py
new file mode 100644
index 0000000..adf2703
--- /dev/null
+++ b/src/DjangoBlog-master/blog/urls.py
@@ -0,0 +1,62 @@
+from django.urls import path
+from django.views.decorators.cache import cache_page
+
+from . import views
+
+app_name = "blog"
+urlpatterns = [
+ path(
+ r'',
+ views.IndexView.as_view(),
+ name='index'),
+ path(
+ r'page//',
+ views.IndexView.as_view(),
+ name='index_page'),
+ path(
+ r'article////.html',
+ views.ArticleDetailView.as_view(),
+ name='detailbyid'),
+ path(
+ r'category/.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail'),
+ path(
+ r'category//.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail_page'),
+ path(
+ r'author/.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail'),
+ path(
+ r'author//.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail_page'),
+ path(
+ r'tag/.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail'),
+ path(
+ r'tag//.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail_page'),
+ path(
+ 'archives.html',
+ cache_page(
+ 60 * 60)(
+ views.ArchivesView.as_view()),
+ name='archives'),
+ path(
+ 'links.html',
+ views.LinkListView.as_view(),
+ name='links'),
+ path(
+ r'upload',
+ views.fileupload,
+ name='upload'),
+ path(
+ r'clean',
+ views.clean_cache_view,
+ name='clean'),
+]
diff --git a/src/DjangoBlog-master/blog/views.py b/src/DjangoBlog-master/blog/views.py
new file mode 100644
index 0000000..d5dc7ec
--- /dev/null
+++ b/src/DjangoBlog-master/blog/views.py
@@ -0,0 +1,379 @@
+import logging
+import os
+import uuid
+
+from django.conf import settings
+from django.core.paginator import Paginator
+from django.http import HttpResponse, HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.templatetags.static import static
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic.detail import DetailView
+from django.views.generic.list import ListView
+from haystack.views import SearchView
+
+from blog.models import Article, Category, LinkShowType, Links, Tag
+from comments.forms import CommentForm
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+from djangoblog.utils import cache, get_blog_setting, get_sha256
+
+logger = logging.getLogger(__name__)
+
+
+class ArticleListView(ListView):
+ # template_name属性用于指定使用哪个模板进行渲染
+ template_name = 'blog/article_index.html'
+
+ # context_object_name属性用于给上下文变量取名(在模板中使用该名字)
+ context_object_name = 'article_list'
+
+ # 页面类型,分类目录或标签列表等
+ page_type = ''
+ paginate_by = settings.PAGINATE_BY
+ page_kwarg = 'page'
+ link_type = LinkShowType.L
+
+ def get_view_cache_key(self):
+ return self.request.get['pages']
+
+ @property
+ def page_number(self):
+ page_kwarg = self.page_kwarg
+ page = self.kwargs.get(
+ page_kwarg) or self.request.GET.get(page_kwarg) or 1
+ return page
+
+ def get_queryset_cache_key(self):
+ """
+ 子类重写.获得queryset的缓存key
+ """
+ raise NotImplementedError()
+
+ def get_queryset_data(self):
+ """
+ 子类重写.获取queryset的数据
+ """
+ raise NotImplementedError()
+
+ def get_queryset_from_cache(self, cache_key):
+ '''
+ 缓存页面数据
+ :param cache_key: 缓存key
+ :return:
+ '''
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get view cache.key:{key}'.format(key=cache_key))
+ return value
+ else:
+ article_list = self.get_queryset_data()
+ cache.set(cache_key, article_list)
+ logger.info('set view cache.key:{key}'.format(key=cache_key))
+ return article_list
+
+ def get_queryset(self):
+ '''
+ 重写默认,从缓存获取数据
+ :return:
+ '''
+ key = self.get_queryset_cache_key()
+ value = self.get_queryset_from_cache(key)
+ return value
+
+ def get_context_data(self, **kwargs):
+ kwargs['linktype'] = self.link_type
+ return super(ArticleListView, self).get_context_data(**kwargs)
+
+
+class IndexView(ArticleListView):
+ '''
+ 首页
+ '''
+ # 友情链接类型
+ link_type = LinkShowType.I
+
+ def get_queryset_data(self):
+ article_list = Article.objects.filter(type='a', status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ cache_key = 'index_{page}'.format(page=self.page_number)
+ return cache_key
+
+
+class ArticleDetailView(DetailView):
+ '''
+ 文章详情页面
+ '''
+ template_name = 'blog/article_detail.html'
+ model = Article
+ pk_url_kwarg = 'article_id'
+ context_object_name = "article"
+
+ def get_context_data(self, **kwargs):
+ comment_form = CommentForm()
+
+ article_comments = self.object.comment_list()
+ parent_comments = article_comments.filter(parent_comment=None)
+ blog_setting = get_blog_setting()
+ paginator = Paginator(parent_comments, blog_setting.article_comment_count)
+ page = self.request.GET.get('comment_page', '1')
+ if not page.isnumeric():
+ page = 1
+ else:
+ page = int(page)
+ if page < 1:
+ page = 1
+ if page > paginator.num_pages:
+ page = paginator.num_pages
+
+ p_comments = paginator.page(page)
+ next_page = p_comments.next_page_number() if p_comments.has_next() else None
+ prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
+
+ if next_page:
+ kwargs[
+ 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
+ if prev_page:
+ kwargs[
+ 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
+ kwargs['form'] = comment_form
+ kwargs['article_comments'] = article_comments
+ kwargs['p_comments'] = p_comments
+ kwargs['comment_count'] = len(
+ article_comments) if article_comments else 0
+
+ kwargs['next_article'] = self.object.next_article
+ kwargs['prev_article'] = self.object.prev_article
+
+ context = super(ArticleDetailView, self).get_context_data(**kwargs)
+ article = self.object
+ # Action Hook, 通知插件"文章详情已获取"
+ hooks.run_action('after_article_body_get', article=article, request=self.request)
+ # # Filter Hook, 允许插件修改文章正文
+ article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
+ request=self.request)
+
+ return context
+
+
+class CategoryDetailView(ArticleListView):
+ '''
+ 分类目录列表
+ '''
+ page_type = "分类目录归档"
+
+ def get_queryset_data(self):
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
+
+ categoryname = category.name
+ self.categoryname = categoryname
+ categorynames = list(
+ map(lambda c: c.name, category.get_sub_categorys()))
+ article_list = Article.objects.filter(
+ category__name__in=categorynames, status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
+ categoryname = category.name
+ self.categoryname = categoryname
+ cache_key = 'category_list_{categoryname}_{page}'.format(
+ categoryname=categoryname, page=self.page_number)
+ return cache_key
+
+ def get_context_data(self, **kwargs):
+
+ categoryname = self.categoryname
+ try:
+ categoryname = categoryname.split('/')[-1]
+ except BaseException:
+ pass
+ kwargs['page_type'] = CategoryDetailView.page_type
+ kwargs['tag_name'] = categoryname
+ return super(CategoryDetailView, self).get_context_data(**kwargs)
+
+
+class AuthorDetailView(ArticleListView):
+ '''
+ 作者详情页
+ '''
+ page_type = '作者文章归档'
+
+ def get_queryset_cache_key(self):
+ from uuslug import slugify
+ author_name = slugify(self.kwargs['author_name'])
+ cache_key = 'author_{author_name}_{page}'.format(
+ author_name=author_name, page=self.page_number)
+ return cache_key
+
+ def get_queryset_data(self):
+ author_name = self.kwargs['author_name']
+ article_list = Article.objects.filter(
+ author__username=author_name, type='a', status='p')
+ return article_list
+
+ def get_context_data(self, **kwargs):
+ author_name = self.kwargs['author_name']
+ kwargs['page_type'] = AuthorDetailView.page_type
+ kwargs['tag_name'] = author_name
+ return super(AuthorDetailView, self).get_context_data(**kwargs)
+
+
+class TagDetailView(ArticleListView):
+ '''
+ 标签列表页面
+ '''
+ page_type = '分类标签归档'
+
+ def get_queryset_data(self):
+ slug = self.kwargs['tag_name']
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name
+ self.name = tag_name
+ article_list = Article.objects.filter(
+ tags__name=tag_name, type='a', status='p')
+ return article_list
+
+ def get_queryset_cache_key(self):
+ slug = self.kwargs['tag_name']
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name
+ self.name = tag_name
+ cache_key = 'tag_{tag_name}_{page}'.format(
+ tag_name=tag_name, page=self.page_number)
+ return cache_key
+
+ def get_context_data(self, **kwargs):
+ # tag_name = self.kwargs['tag_name']
+ tag_name = self.name
+ kwargs['page_type'] = TagDetailView.page_type
+ kwargs['tag_name'] = tag_name
+ return super(TagDetailView, self).get_context_data(**kwargs)
+
+
+class ArchivesView(ArticleListView):
+ '''
+ 文章归档页面
+ '''
+ page_type = '文章归档'
+ paginate_by = None
+ page_kwarg = None
+ template_name = 'blog/article_archives.html'
+
+ def get_queryset_data(self):
+ return Article.objects.filter(status='p').all()
+
+ def get_queryset_cache_key(self):
+ cache_key = 'archives'
+ return cache_key
+
+
+class LinkListView(ListView):
+ model = Links
+ template_name = 'blog/links_list.html'
+
+ def get_queryset(self):
+ return Links.objects.filter(is_enable=True)
+
+
+class EsSearchView(SearchView):
+ def get_context(self):
+ paginator, page = self.build_page()
+ context = {
+ "query": self.query,
+ "form": self.form,
+ "page": page,
+ "paginator": paginator,
+ "suggestion": None,
+ }
+ if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
+ context["suggestion"] = self.results.query.get_spelling_suggestion()
+ context.update(self.extra_context())
+
+ return context
+
+
+@csrf_exempt
+def fileupload(request):
+ """
+ 该方法需自己写调用端来上传图片,该方法仅提供图床功能
+ :param request:
+ :return:
+ """
+ if request.method == 'POST':
+ sign = request.GET.get('sign', None)
+ if not sign:
+ return HttpResponseForbidden()
+ if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
+ return HttpResponseForbidden()
+ response = []
+ for filename in request.FILES:
+ timestr = timezone.now().strftime('%Y/%m/%d')
+ imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
+ fname = u''.join(str(filename))
+ isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
+ base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
+ if not os.path.exists(base_dir):
+ os.makedirs(base_dir)
+ savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
+ if not savepath.startswith(base_dir):
+ return HttpResponse("only for post")
+ with open(savepath, 'wb+') as wfile:
+ for chunk in request.FILES[filename].chunks():
+ wfile.write(chunk)
+ if isimage:
+ from PIL import Image
+ image = Image.open(savepath)
+ image.save(savepath, quality=20, optimize=True)
+ url = static(savepath)
+ response.append(url)
+ return HttpResponse(response)
+
+ else:
+ return HttpResponse("only for post")
+
+
+def page_not_found_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ if exception:
+ logger.error(exception)
+ url = request.get_full_path()
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
+ 'statuscode': '404'},
+ status=404)
+
+
+def server_error_view(request, template_name='blog/error_page.html'):
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the server is busy, please click the home page to see other?'),
+ 'statuscode': '500'},
+ status=500)
+
+
+def permission_denied_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ if exception:
+ logger.error(exception)
+ return render(
+ request, template_name, {
+ 'message': _('Sorry, you do not have permission to access this page?'),
+ 'statuscode': '403'}, status=403)
+
+
+def clean_cache_view(request):
+ cache.clear()
+ return HttpResponse('ok')
diff --git a/src/DjangoBlog-master/comments/__init__.py b/src/DjangoBlog-master/comments/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/comments/admin.py b/src/DjangoBlog-master/comments/admin.py
new file mode 100644
index 0000000..a814f3f
--- /dev/null
+++ b/src/DjangoBlog-master/comments/admin.py
@@ -0,0 +1,47 @@
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+
+def disable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=False)
+
+
+def enable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=True)
+
+
+disable_commentstatus.short_description = _('Disable comments')
+enable_commentstatus.short_description = _('Enable comments')
+
+
+class CommentAdmin(admin.ModelAdmin):
+ list_per_page = 20
+ list_display = (
+ 'id',
+ 'body',
+ 'link_to_userinfo',
+ 'link_to_article',
+ 'is_enable',
+ 'creation_time')
+ list_display_links = ('id', 'body', 'is_enable')
+ list_filter = ('is_enable',)
+ exclude = ('creation_time', 'last_modify_time')
+ actions = [disable_commentstatus, enable_commentstatus]
+
+ def link_to_userinfo(self, obj):
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ return format_html(
+ u'%s ' %
+ (link, obj.author.nickname if obj.author.nickname else obj.author.email))
+
+ def link_to_article(self, obj):
+ info = (obj.article._meta.app_label, obj.article._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
+ return format_html(
+ u'%s ' % (link, obj.article.title))
+
+ link_to_userinfo.short_description = _('User')
+ link_to_article.short_description = _('Article')
diff --git a/src/DjangoBlog-master/comments/apps.py b/src/DjangoBlog-master/comments/apps.py
new file mode 100644
index 0000000..ff01b77
--- /dev/null
+++ b/src/DjangoBlog-master/comments/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CommentsConfig(AppConfig):
+ name = 'comments'
diff --git a/src/DjangoBlog-master/comments/forms.py b/src/DjangoBlog-master/comments/forms.py
new file mode 100644
index 0000000..e83737d
--- /dev/null
+++ b/src/DjangoBlog-master/comments/forms.py
@@ -0,0 +1,13 @@
+from django import forms
+from django.forms import ModelForm
+
+from .models import Comment
+
+
+class CommentForm(ModelForm):
+ parent_comment_id = forms.IntegerField(
+ widget=forms.HiddenInput, required=False)
+
+ class Meta:
+ model = Comment
+ fields = ['body']
diff --git a/src/DjangoBlog-master/comments/migrations/0001_initial.py b/src/DjangoBlog-master/comments/migrations/0001_initial.py
new file mode 100644
index 0000000..61d1e53
--- /dev/null
+++ b/src/DjangoBlog-master/comments/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('blog', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('body', models.TextField(max_length=300, verbose_name='正文')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
+ ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
+ ],
+ options={
+ 'verbose_name': '评论',
+ 'verbose_name_plural': '评论',
+ 'ordering': ['-id'],
+ 'get_latest_by': 'id',
+ },
+ ),
+ ]
diff --git a/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py b/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py
new file mode 100644
index 0000000..17c44db
--- /dev/null
+++ b/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.7 on 2023-04-24 13:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('comments', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='是否显示'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
new file mode 100644
index 0000000..a1ca970
--- /dev/null
+++ b/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('blog', '0005_alter_article_options_alter_category_options_and_more'),
+ ('comments', '0002_alter_comment_is_enable'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='comment',
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='article',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='enable'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='parent_comment',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/comments/migrations/__init__.py b/src/DjangoBlog-master/comments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/comments/models.py b/src/DjangoBlog-master/comments/models.py
new file mode 100644
index 0000000..7c3bbc8
--- /dev/null
+++ b/src/DjangoBlog-master/comments/models.py
@@ -0,0 +1,39 @@
+from django.conf import settings
+from django.db import models
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+
+from blog.models import Article
+
+
+# Create your models here.
+
+class Comment(models.Model):
+ body = models.TextField('正文', max_length=300)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE)
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE)
+ parent_comment = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent comment'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ is_enable = models.BooleanField(_('enable'),
+ default=False, blank=False, null=False)
+
+ class Meta:
+ ordering = ['-id']
+ verbose_name = _('comment')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def __str__(self):
+ return self.body
diff --git a/src/DjangoBlog-master/comments/templatetags/__init__.py b/src/DjangoBlog-master/comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/comments/templatetags/comments_tags.py b/src/DjangoBlog-master/comments/templatetags/comments_tags.py
new file mode 100644
index 0000000..fde02b4
--- /dev/null
+++ b/src/DjangoBlog-master/comments/templatetags/comments_tags.py
@@ -0,0 +1,30 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def parse_commenttree(commentlist, comment):
+ """获得当前评论子评论的列表
+ 用法: {% parse_commenttree article_comments comment as childcomments %}
+ """
+ datas = []
+
+ def parse(c):
+ childs = commentlist.filter(parent_comment=c, is_enable=True)
+ for child in childs:
+ datas.append(child)
+ parse(child)
+
+ parse(comment)
+ return datas
+
+
+@register.inclusion_tag('comments/tags/comment_item.html')
+def show_comment_item(comment, ischild):
+ """评论"""
+ depth = 1 if ischild else 2
+ return {
+ 'comment_item': comment,
+ 'depth': depth
+ }
diff --git a/src/DjangoBlog-master/comments/tests.py b/src/DjangoBlog-master/comments/tests.py
new file mode 100644
index 0000000..2a7f55f
--- /dev/null
+++ b/src/DjangoBlog-master/comments/tests.py
@@ -0,0 +1,109 @@
+from django.test import Client, RequestFactory, TransactionTestCase
+from django.urls import reverse
+
+from accounts.models import BlogUser
+from blog.models import Category, Article
+from comments.models import Comment
+from comments.templatetags.comments_tags import *
+from djangoblog.utils import get_max_articleid_commentid
+
+
+# Create your tests here.
+
+class CommentsTest(TransactionTestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+ from blog.models import BlogSettings
+ value = BlogSettings()
+ value.comment_need_review = True
+ value.save()
+
+ self.user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ def update_article_comment_status(self, article):
+ comments = article.comment_set.all()
+ for comment in comments:
+ comment.is_enable = True
+ comment.save()
+
+ def test_validate_comment(self):
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ category = Category()
+ category.name = "categoryccc"
+ category.save()
+
+ article = Article()
+ article.title = "nicetitleccc"
+ article.body = "nicecontentccc"
+ article.author = self.user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ comment_url = reverse(
+ 'comments:postcomment', kwargs={
+ 'article_id': article.id})
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff'
+ })
+
+ self.assertEqual(response.status_code, 302)
+
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 0)
+ self.update_article_comment_status(article)
+
+ self.assertEqual(len(article.comment_list()), 1)
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff',
+ })
+
+ self.assertEqual(response.status_code, 302)
+
+ article = Article.objects.get(pk=article.pk)
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 2)
+ parent_comment_id = article.comment_list()[0].id
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''',
+ 'parent_comment_id': parent_comment_id
+ })
+
+ self.assertEqual(response.status_code, 302)
+ self.update_article_comment_status(article)
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 3)
+ comment = Comment.objects.get(id=parent_comment_id)
+ tree = parse_commenttree(article.comment_list(), comment)
+ self.assertEqual(len(tree), 1)
+ data = show_comment_item(comment, True)
+ self.assertIsNotNone(data)
+ s = get_max_articleid_commentid()
+ self.assertIsNotNone(s)
+
+ from comments.utils import send_comment_email
+ send_comment_email(comment)
diff --git a/src/DjangoBlog-master/comments/urls.py b/src/DjangoBlog-master/comments/urls.py
new file mode 100644
index 0000000..7df3fab
--- /dev/null
+++ b/src/DjangoBlog-master/comments/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from . import views
+
+app_name = "comments"
+urlpatterns = [
+ path(
+ 'article//postcomment',
+ views.CommentPostView.as_view(),
+ name='postcomment'),
+]
diff --git a/src/DjangoBlog-master/comments/utils.py b/src/DjangoBlog-master/comments/utils.py
new file mode 100644
index 0000000..f01dba7
--- /dev/null
+++ b/src/DjangoBlog-master/comments/utils.py
@@ -0,0 +1,38 @@
+import logging
+
+from django.utils.translation import gettext_lazy as _
+
+from djangoblog.utils import get_current_site
+from djangoblog.utils import send_email
+
+logger = logging.getLogger(__name__)
+
+
+def send_comment_email(comment):
+ site = get_current_site().domain
+ subject = _('Thanks for your comment')
+ article_url = f"https://{site}{comment.article.get_absolute_url()}"
+ html_content = _("""Thank you very much for your comments on this site
+ You can visit %(article_title)s
+ to review your comments,
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
+ tomail = comment.author.email
+ send_email([tomail], subject, html_content)
+ try:
+ if comment.parent_comment:
+ html_content = _("""Your comment on %(article_title)s has
+ received a reply. %(comment_body)s
+
+ go check it out!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s
+ """) % {'article_url': article_url, 'article_title': comment.article.title,
+ 'comment_body': comment.parent_comment.body}
+ tomail = comment.parent_comment.author.email
+ send_email([tomail], subject, html_content)
+ except Exception as e:
+ logger.error(e)
diff --git a/src/DjangoBlog-master/comments/views.py b/src/DjangoBlog-master/comments/views.py
new file mode 100644
index 0000000..ad9b2b9
--- /dev/null
+++ b/src/DjangoBlog-master/comments/views.py
@@ -0,0 +1,63 @@
+# Create your views here.
+from django.core.exceptions import ValidationError
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_protect
+from django.views.generic.edit import FormView
+
+from accounts.models import BlogUser
+from blog.models import Article
+from .forms import CommentForm
+from .models import Comment
+
+
+class CommentPostView(FormView):
+ form_class = CommentForm
+ template_name = 'blog/article_detail.html'
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ return super(CommentPostView, self).dispatch(*args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+ url = article.get_absolute_url()
+ return HttpResponseRedirect(url + "#comments")
+
+ def form_invalid(self, form):
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ return self.render_to_response({
+ 'form': form,
+ 'article': article
+ })
+
+ def form_valid(self, form):
+ """提交的数据验证合法后的逻辑"""
+ user = self.request.user
+ author = BlogUser.objects.get(pk=user.pk)
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.")
+ comment = form.save(False)
+ comment.article = article
+ from djangoblog.utils import get_blog_setting
+ settings = get_blog_setting()
+ if not settings.comment_need_review:
+ comment.is_enable = True
+ comment.author = author
+
+ if form.cleaned_data['parent_comment_id']:
+ parent_comment = Comment.objects.get(
+ pk=form.cleaned_data['parent_comment_id'])
+ comment.parent_comment = parent_comment
+
+ comment.save(True)
+ return HttpResponseRedirect(
+ "%s#div-comment-%d" %
+ (article.get_absolute_url(), comment.pk))
diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml
new file mode 100644
index 0000000..83e35ff
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml
@@ -0,0 +1,48 @@
+version: '3'
+
+services:
+ es:
+ image: liangliangyy/elasticsearch-analysis-ik:8.6.1
+ container_name: es
+ restart: always
+ environment:
+ - discovery.type=single-node
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+ ports:
+ - 9200:9200
+ volumes:
+ - ./bin/datas/es/:/usr/share/elasticsearch/data/
+
+ kibana:
+ image: kibana:8.6.1
+ restart: always
+ container_name: kibana
+ ports:
+ - 5601:5601
+ environment:
+ - ELASTICSEARCH_HOSTS=http://es:9200
+
+ djangoblog:
+ build: .
+ restart: always
+ command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ - ./uploads:/code/djangoblog/uploads
+ environment:
+ - DJANGO_MYSQL_DATABASE=djangoblog
+ - DJANGO_MYSQL_USER=root
+ - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ - DJANGO_MYSQL_HOST=db
+ - DJANGO_MYSQL_PORT=3306
+ - DJANGO_MEMCACHED_LOCATION=memcached:11211
+ - DJANGO_ELASTICSEARCH_HOST=es:9200
+ links:
+ - db
+ - memcached
+ depends_on:
+ - db
+ container_name: djangoblog
+
diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml
new file mode 100644
index 0000000..9609af3
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml
@@ -0,0 +1,60 @@
+version: '3'
+
+services:
+ db:
+ image: mysql:latest
+ restart: always
+ environment:
+ - MYSQL_DATABASE=djangoblog
+ - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ ports:
+ - 3306:3306
+ volumes:
+ - ./bin/datas/mysql/:/var/lib/mysql
+ depends_on:
+ - redis
+ container_name: db
+
+ djangoblog:
+ build:
+ context: ../../
+ restart: always
+ command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ - ./logs:/code/djangoblog/logs
+ - ./uploads:/code/djangoblog/uploads
+ environment:
+ - DJANGO_MYSQL_DATABASE=djangoblog
+ - DJANGO_MYSQL_USER=root
+ - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
+ - DJANGO_MYSQL_HOST=db
+ - DJANGO_MYSQL_PORT=3306
+ - DJANGO_REDIS_URL=redis:6379
+ links:
+ - db
+ - redis
+ depends_on:
+ - db
+ container_name: djangoblog
+ nginx:
+ restart: always
+ image: nginx:latest
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./bin/nginx.conf:/etc/nginx/nginx.conf
+ - ./collectedstatic:/code/djangoblog/collectedstatic
+ links:
+ - djangoblog:djangoblog
+ container_name: nginx
+
+ redis:
+ restart: always
+ image: redis:latest
+ container_name: redis
+ ports:
+ - "6379:6379"
diff --git a/src/DjangoBlog-master/deploy/entrypoint.sh b/src/DjangoBlog-master/deploy/entrypoint.sh
new file mode 100644
index 0000000..2fb6491
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/entrypoint.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+NAME="djangoblog"
+DJANGODIR=/code/djangoblog
+USER=root
+GROUP=root
+NUM_WORKERS=1
+DJANGO_WSGI_MODULE=djangoblog.wsgi
+
+
+echo "Starting $NAME as `whoami`"
+
+cd $DJANGODIR
+
+export PYTHONPATH=$DJANGODIR:$PYTHONPATH
+
+python manage.py makemigrations && \
+ python manage.py migrate && \
+ python manage.py collectstatic --noinput && \
+ python manage.py compress --force && \
+ python manage.py build_index && \
+ python manage.py compilemessages || exit 1
+
+exec gunicorn ${DJANGO_WSGI_MODULE}:application \
+--name $NAME \
+--workers $NUM_WORKERS \
+--user=$USER --group=$GROUP \
+--bind 0.0.0.0:8000 \
+--log-level=debug \
+--log-file=- \
+--worker-class gevent \
+--threads 4
diff --git a/src/DjangoBlog-master/deploy/k8s/configmap.yaml b/src/DjangoBlog-master/deploy/k8s/configmap.yaml
new file mode 100644
index 0000000..835d4ad
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/configmap.yaml
@@ -0,0 +1,119 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: web-nginx-config
+ namespace: djangoblog
+data:
+ nginx.conf: |
+ user nginx;
+ worker_processes auto;
+ error_log /var/log/nginx/error.log notice;
+ pid /var/run/nginx.pid;
+
+ events {
+ worker_connections 1024;
+ multi_accept on;
+ use epoll;
+ }
+
+ http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ keepalive_timeout 65;
+ gzip on;
+ gzip_disable "msie6";
+
+ gzip_vary on;
+ gzip_proxied any;
+ gzip_comp_level 8;
+ gzip_buffers 16 8k;
+ gzip_http_version 1.1;
+ gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+
+ # Include server configurations
+ include /etc/nginx/conf.d/*.conf;
+ }
+ djangoblog.conf: |
+ server {
+ server_name lylinux.net;
+ root /code/djangoblog/collectedstatic/;
+ listen 80;
+ keepalive_timeout 70;
+ location /static/ {
+ expires max;
+ alias /code/djangoblog/collectedstatic/;
+ }
+
+ location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
+ root /resource/djangopub;
+ expires 1d;
+ access_log off;
+ error_log off;
+ }
+
+ location / {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_redirect off;
+ if (!-f $request_filename) {
+ proxy_pass http://djangoblog:8000;
+ break;
+ }
+ }
+ }
+ server {
+ server_name www.lylinux.net;
+ listen 80;
+ return 301 https://lylinux.net$request_uri;
+ }
+ resource.lylinux.net.conf: |
+ server {
+ index index.html index.htm;
+ server_name resource.lylinux.net;
+ root /resource/;
+
+ location /djangoblog/ {
+ alias /code/djangoblog/collectedstatic/;
+ }
+
+ access_log off;
+ error_log off;
+ include lylinux/resource.conf;
+ }
+ lylinux.resource.conf: |
+ expires max;
+ access_log off;
+ log_not_found off;
+ add_header Pragma public;
+ add_header Cache-Control "public";
+ add_header "Access-Control-Allow-Origin" "*";
+
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: djangoblog-env
+ namespace: djangoblog
+data:
+ DJANGO_MYSQL_DATABASE: djangoblog
+ DJANGO_MYSQL_USER: db_user
+ DJANGO_MYSQL_PASSWORD: db_password
+ DJANGO_MYSQL_HOST: db_host
+ DJANGO_MYSQL_PORT: db_port
+ DJANGO_REDIS_URL: "redis:6379"
+ DJANGO_DEBUG: "False"
+ MYSQL_ROOT_PASSWORD: db_password
+ MYSQL_DATABASE: djangoblog
+ MYSQL_PASSWORD: db_password
+ DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
diff --git a/src/DjangoBlog-master/deploy/k8s/deployment.yaml b/src/DjangoBlog-master/deploy/k8s/deployment.yaml
new file mode 100644
index 0000000..414fdcc
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/deployment.yaml
@@ -0,0 +1,274 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: djangoblog
+ namespace: djangoblog
+ labels:
+ app: djangoblog
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: djangoblog
+ template:
+ metadata:
+ labels:
+ app: djangoblog
+ spec:
+ containers:
+ - name: djangoblog
+ image: liangliangyy/djangoblog:latest
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 8000
+ envFrom:
+ - configMapRef:
+ name: djangoblog-env
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: djangoblog
+ mountPath: /code/djangoblog/collectedstatic
+ - name: resource
+ mountPath: /resource
+ volumes:
+ - name: djangoblog
+ persistentVolumeClaim:
+ claimName: djangoblog-pvc
+ - name: resource
+ persistentVolumeClaim:
+ claimName: resource-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+ namespace: djangoblog
+ labels:
+ app: redis
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ containers:
+ - name: redis
+ image: redis:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 6379
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: 200m
+ memory: 2Gi
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: db
+ namespace: djangoblog
+ labels:
+ app: db
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: db
+ template:
+ metadata:
+ labels:
+ app: db
+ spec:
+ containers:
+ - name: db
+ image: mysql:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 3306
+ envFrom:
+ - configMapRef:
+ name: djangoblog-env
+ readinessProbe:
+ exec:
+ command:
+ - mysqladmin
+ - ping
+ - "-h"
+ - "127.0.0.1"
+ - "-u"
+ - "root"
+ - "-p$MYSQL_ROOT_PASSWORD"
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ livenessProbe:
+ exec:
+ command:
+ - mysqladmin
+ - ping
+ - "-h"
+ - "127.0.0.1"
+ - "-u"
+ - "root"
+ - "-p$MYSQL_ROOT_PASSWORD"
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: db-data
+ mountPath: /var/lib/mysql
+ volumes:
+ - name: db-data
+ persistentVolumeClaim:
+ claimName: db-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx
+ namespace: djangoblog
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ volumeMounts:
+ - name: nginx-config
+ mountPath: /etc/nginx/nginx.conf
+ subPath: nginx.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/conf.d/default.conf
+ subPath: djangoblog.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
+ subPath: resource.lylinux.net.conf
+ - name: nginx-config
+ mountPath: /etc/nginx/lylinux/resource.conf
+ subPath: lylinux.resource.conf
+ - name: djangoblog-pvc
+ mountPath: /code/djangoblog/collectedstatic
+ - name: resource-pvc
+ mountPath: /resource
+ volumes:
+ - name: nginx-config
+ configMap:
+ name: web-nginx-config
+ - name: djangoblog-pvc
+ persistentVolumeClaim:
+ claimName: djangoblog-pvc
+ - name: resource-pvc
+ persistentVolumeClaim:
+ claimName: resource-pvc
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: elasticsearch
+ namespace: djangoblog
+ labels:
+ app: elasticsearch
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: elasticsearch
+ template:
+ metadata:
+ labels:
+ app: elasticsearch
+ spec:
+ containers:
+ - name: elasticsearch
+ image: liangliangyy/elasticsearch-analysis-ik:8.6.1
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: discovery.type
+ value: single-node
+ - name: ES_JAVA_OPTS
+ value: "-Xms256m -Xmx256m"
+ - name: xpack.security.enabled
+ value: "false"
+ - name: xpack.monitoring.templates.enabled
+ value: "false"
+ ports:
+ - containerPort: 9200
+ resources:
+ requests:
+ cpu: 10m
+ memory: 100Mi
+ limits:
+ cpu: "2"
+ memory: 2Gi
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 9200
+ initialDelaySeconds: 15
+ periodSeconds: 30
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 9200
+ initialDelaySeconds: 15
+ periodSeconds: 30
+ volumeMounts:
+ - name: elasticsearch-data
+ mountPath: /usr/share/elasticsearch/data/
+ volumes:
+ - name: elasticsearch-data
+ persistentVolumeClaim:
+ claimName: elasticsearch-pvc
diff --git a/src/DjangoBlog-master/deploy/k8s/gateway.yaml b/src/DjangoBlog-master/deploy/k8s/gateway.yaml
new file mode 100644
index 0000000..a8de073
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/gateway.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: nginx
+ namespace: djangoblog
+spec:
+ ingressClassName: nginx
+ rules:
+ - http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: nginx
+ port:
+ number: 80
\ No newline at end of file
diff --git a/src/DjangoBlog-master/deploy/k8s/pv.yaml b/src/DjangoBlog-master/deploy/k8s/pv.yaml
new file mode 100644
index 0000000..874b72f
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/pv.yaml
@@ -0,0 +1,94 @@
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-db
+spec:
+ capacity:
+ storage: 10Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-db
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-djangoblog
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-djangoblog
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+
+
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-resource
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/resource/
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
+
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-pv-elasticsearch
+spec:
+ capacity:
+ storage: 5Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ local:
+ path: /mnt/local-storage-elasticsearch
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - master
\ No newline at end of file
diff --git a/src/DjangoBlog-master/deploy/k8s/pvc.yaml b/src/DjangoBlog-master/deploy/k8s/pvc.yaml
new file mode 100644
index 0000000..ef238c5
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/pvc.yaml
@@ -0,0 +1,60 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: db-pvc
+ namespace: djangoblog
+spec:
+ storageClassName: local-storage
+ volumeName: local-pv-db
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: djangoblog-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-djangoblog
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: resource-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-resource
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: elasticsearch-pvc
+ namespace: djangoblog
+spec:
+ volumeName: local-pv-elasticsearch
+ storageClassName: local-storage
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/deploy/k8s/service.yaml b/src/DjangoBlog-master/deploy/k8s/service.yaml
new file mode 100644
index 0000000..4ef2931
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/service.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: djangoblog
+ namespace: djangoblog
+ labels:
+ app: djangoblog
+spec:
+ selector:
+ app: djangoblog
+ ports:
+ - protocol: TCP
+ port: 8000
+ targetPort: 8000
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+ namespace: djangoblog
+ labels:
+ app: nginx
+spec:
+ selector:
+ app: nginx
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 80
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+ namespace: djangoblog
+ labels:
+ app: redis
+spec:
+ selector:
+ app: redis
+ ports:
+ - protocol: TCP
+ port: 6379
+ targetPort: 6379
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: db
+ namespace: djangoblog
+ labels:
+ app: db
+spec:
+ selector:
+ app: db
+ ports:
+ - protocol: TCP
+ port: 3306
+ targetPort: 3306
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: elasticsearch
+ namespace: djangoblog
+ labels:
+ app: elasticsearch
+spec:
+ selector:
+ app: elasticsearch
+ ports:
+ - protocol: TCP
+ port: 9200
+ targetPort: 9200
+ type: ClusterIP
+
diff --git a/src/DjangoBlog-master/deploy/k8s/storageclass.yaml b/src/DjangoBlog-master/deploy/k8s/storageclass.yaml
new file mode 100644
index 0000000..5d5a14c
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/k8s/storageclass.yaml
@@ -0,0 +1,10 @@
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+ name: local-storage
+ annotations:
+ storageclass.kubernetes.io/is-default-class: "true"
+provisioner: kubernetes.io/no-provisioner
+volumeBindingMode: Immediate
+
+
diff --git a/src/DjangoBlog-master/deploy/nginx.conf b/src/DjangoBlog-master/deploy/nginx.conf
new file mode 100644
index 0000000..32161d8
--- /dev/null
+++ b/src/DjangoBlog-master/deploy/nginx.conf
@@ -0,0 +1,50 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log notice;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ server {
+ root /code/djangoblog/collectedstatic/;
+ listen 80;
+ keepalive_timeout 70;
+ location /static/ {
+ expires max;
+ alias /code/djangoblog/collectedstatic/;
+ }
+ location / {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_redirect off;
+ if (!-f $request_filename) {
+ proxy_pass http://djangoblog:8000;
+ break;
+ }
+ }
+ }
+}
diff --git a/src/DjangoBlog-master/djangoblog/__init__.py b/src/DjangoBlog-master/djangoblog/__init__.py
new file mode 100644
index 0000000..1e205f4
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
diff --git a/src/DjangoBlog-master/djangoblog/admin_site.py b/src/DjangoBlog-master/djangoblog/admin_site.py
new file mode 100644
index 0000000..f120405
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/admin_site.py
@@ -0,0 +1,64 @@
+from django.contrib.admin import AdminSite
+from django.contrib.admin.models import LogEntry
+from django.contrib.sites.admin import SiteAdmin
+from django.contrib.sites.models import Site
+
+from accounts.admin import *
+from blog.admin import *
+from blog.models import *
+from comments.admin import *
+from comments.models import *
+from djangoblog.logentryadmin import LogEntryAdmin
+from oauth.admin import *
+from oauth.models import *
+from owntracks.admin import *
+from owntracks.models import *
+from servermanager.admin import *
+from servermanager.models import *
+
+
+class DjangoBlogAdminSite(AdminSite):
+ site_header = 'djangoblog administration'
+ site_title = 'djangoblog site admin'
+
+ def __init__(self, name='admin'):
+ super().__init__(name)
+
+ def has_permission(self, request):
+ return request.user.is_superuser
+
+ # def get_urls(self):
+ # urls = super().get_urls()
+ # from django.urls import path
+ # from blog.views import refresh_memcache
+ #
+ # my_urls = [
+ # path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
+ # ]
+ # return urls + my_urls
+
+
+admin_site = DjangoBlogAdminSite(name='admin')
+
+admin_site.register(Article, ArticlelAdmin)
+admin_site.register(Category, CategoryAdmin)
+admin_site.register(Tag, TagAdmin)
+admin_site.register(Links, LinksAdmin)
+admin_site.register(SideBar, SideBarAdmin)
+admin_site.register(BlogSettings, BlogSettingsAdmin)
+
+admin_site.register(commands, CommandsAdmin)
+admin_site.register(EmailSendLog, EmailSendLogAdmin)
+
+admin_site.register(BlogUser, BlogUserAdmin)
+
+admin_site.register(Comment, CommentAdmin)
+
+admin_site.register(OAuthUser, OAuthUserAdmin)
+admin_site.register(OAuthConfig, OAuthConfigAdmin)
+
+admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
+
+admin_site.register(Site, SiteAdmin)
+
+admin_site.register(LogEntry, LogEntryAdmin)
diff --git a/src/DjangoBlog-master/djangoblog/apps.py b/src/DjangoBlog-master/djangoblog/apps.py
new file mode 100644
index 0000000..d29e318
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+class DjangoblogAppConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'djangoblog'
+
+ def ready(self):
+ super().ready()
+ # Import and load plugins here
+ from .plugin_manage.loader import load_plugins
+ load_plugins()
\ No newline at end of file
diff --git a/src/DjangoBlog-master/djangoblog/blog_signals.py b/src/DjangoBlog-master/djangoblog/blog_signals.py
new file mode 100644
index 0000000..393f441
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/blog_signals.py
@@ -0,0 +1,122 @@
+import _thread
+import logging
+
+import django.dispatch
+from django.conf import settings
+from django.contrib.admin.models import LogEntry
+from django.contrib.auth.signals import user_logged_in, user_logged_out
+from django.core.mail import EmailMultiAlternatives
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from comments.models import Comment
+from comments.utils import send_comment_email
+from djangoblog.spider_notify import SpiderNotify
+from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+
+logger = logging.getLogger(__name__)
+
+oauth_user_login_signal = django.dispatch.Signal(['id'])
+send_email_signal = django.dispatch.Signal(
+ ['emailto', 'title', 'content'])
+
+
+@receiver(send_email_signal)
+def send_email_signal_handler(sender, **kwargs):
+ emailto = kwargs['emailto']
+ title = kwargs['title']
+ content = kwargs['content']
+
+ msg = EmailMultiAlternatives(
+ title,
+ content,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=emailto)
+ msg.content_subtype = "html"
+
+ from servermanager.models import EmailSendLog
+ log = EmailSendLog()
+ log.title = title
+ log.content = content
+ log.emailto = ','.join(emailto)
+
+ try:
+ result = msg.send()
+ log.send_result = result > 0
+ except Exception as e:
+ logger.error(f"失败邮箱号: {emailto}, {e}")
+ log.send_result = False
+ log.save()
+
+
+@receiver(oauth_user_login_signal)
+def oauth_user_login_signal_handler(sender, **kwargs):
+ id = kwargs['id']
+ oauthuser = OAuthUser.objects.get(id=id)
+ site = get_current_site().domain
+ if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
+ from djangoblog.utils import save_user_avatar
+ oauthuser.picture = save_user_avatar(oauthuser.picture)
+ oauthuser.save()
+
+ delete_sidebar_cache()
+
+
+@receiver(post_save)
+def model_post_save_callback(
+ sender,
+ instance,
+ created,
+ raw,
+ using,
+ update_fields,
+ **kwargs):
+ clearcache = False
+ if isinstance(instance, LogEntry):
+ return
+ if 'get_full_url' in dir(instance):
+ is_update_views = update_fields == {'views'}
+ if not settings.TESTING and not is_update_views:
+ try:
+ notify_url = instance.get_full_url()
+ SpiderNotify.baidu_notify([notify_url])
+ except Exception as ex:
+ logger.error("notify sipder", ex)
+ if not is_update_views:
+ clearcache = True
+
+ if isinstance(instance, Comment):
+ if instance.is_enable:
+ path = instance.article.get_absolute_url()
+ site = get_current_site().domain
+ if site.find(':') > 0:
+ site = site[0:site.find(':')]
+
+ expire_view_cache(
+ path,
+ servername=site,
+ serverport=80,
+ key_prefix='blogdetail')
+ if cache.get('seo_processor'):
+ cache.delete('seo_processor')
+ comment_cache_key = 'article_comments_{id}'.format(
+ id=instance.article.id)
+ cache.delete(comment_cache_key)
+ delete_sidebar_cache()
+ delete_view_cache('article_comments', [str(instance.article.pk)])
+
+ _thread.start_new_thread(send_comment_email, (instance,))
+
+ if clearcache:
+ cache.clear()
+
+
+@receiver(user_logged_in)
+@receiver(user_logged_out)
+def user_auth_callback(sender, request, user, **kwargs):
+ if user and user.username:
+ logger.info(user)
+ delete_sidebar_cache()
+ # cache.clear()
diff --git a/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py b/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py
new file mode 100644
index 0000000..4afe498
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py
@@ -0,0 +1,183 @@
+from django.utils.encoding import force_str
+from elasticsearch_dsl import Q
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
+from haystack.forms import ModelSearchForm
+from haystack.models import SearchResult
+from haystack.utils import log as logging
+
+from blog.documents import ArticleDocument, ArticleDocumentManager
+from blog.models import Article
+
+logger = logging.getLogger(__name__)
+
+
+class ElasticSearchBackend(BaseSearchBackend):
+ def __init__(self, connection_alias, **connection_options):
+ super(
+ ElasticSearchBackend,
+ self).__init__(
+ connection_alias,
+ **connection_options)
+ self.manager = ArticleDocumentManager()
+ self.include_spelling = True
+
+ def _get_models(self, iterable):
+ models = iterable if iterable and iterable[0] else Article.objects.all()
+ docs = self.manager.convert_to_doc(models)
+ return docs
+
+ def _create(self, models):
+ self.manager.create_index()
+ docs = self._get_models(models)
+ self.manager.rebuild(docs)
+
+ def _delete(self, models):
+ for m in models:
+ m.delete()
+ return True
+
+ def _rebuild(self, models):
+ models = models if models else Article.objects.all()
+ docs = self.manager.convert_to_doc(models)
+ self.manager.update_docs(docs)
+
+ def update(self, index, iterable, commit=True):
+
+ models = self._get_models(iterable)
+ self.manager.update_docs(models)
+
+ def remove(self, obj_or_string):
+ models = self._get_models([obj_or_string])
+ self._delete(models)
+
+ def clear(self, models=None, commit=True):
+ self.remove(None)
+
+ @staticmethod
+ def get_suggestion(query: str) -> str:
+ """获取推荐词, 如果没有找到添加原搜索词"""
+
+ search = ArticleDocument.search() \
+ .query("match", body=query) \
+ .suggest('suggest_search', query, term={'field': 'body'}) \
+ .execute()
+
+ keywords = []
+ for suggest in search.suggest.suggest_search:
+ if suggest["options"]:
+ keywords.append(suggest["options"][0]["text"])
+ else:
+ keywords.append(suggest["text"])
+
+ return ' '.join(keywords)
+
+ @log_query
+ def search(self, query_string, **kwargs):
+ logger.info('search query_string:' + query_string)
+
+ start_offset = kwargs.get('start_offset')
+ end_offset = kwargs.get('end_offset')
+
+ # 推荐词搜索
+ if getattr(self, "is_suggest", None):
+ suggestion = self.get_suggestion(query_string)
+ else:
+ suggestion = query_string
+
+ q = Q('bool',
+ should=[Q('match', body=suggestion), Q('match', title=suggestion)],
+ minimum_should_match="70%")
+
+ search = ArticleDocument.search() \
+ .query('bool', filter=[q]) \
+ .filter('term', status='p') \
+ .filter('term', type='a') \
+ .source(False)[start_offset: end_offset]
+
+ results = search.execute()
+ hits = results['hits'].total
+ raw_results = []
+ for raw_result in results['hits']['hits']:
+ app_label = 'blog'
+ model_name = 'Article'
+ additional_fields = {}
+
+ result_class = SearchResult
+
+ result = result_class(
+ app_label,
+ model_name,
+ raw_result['_id'],
+ raw_result['_score'],
+ **additional_fields)
+ raw_results.append(result)
+ facets = {}
+ spelling_suggestion = None if query_string == suggestion else suggestion
+
+ return {
+ 'results': raw_results,
+ 'hits': hits,
+ 'facets': facets,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+
+class ElasticSearchQuery(BaseSearchQuery):
+ def _convert_datetime(self, date):
+ if hasattr(date, 'hour'):
+ return force_str(date.strftime('%Y%m%d%H%M%S'))
+ else:
+ return force_str(date.strftime('%Y%m%d000000'))
+
+ def clean(self, query_fragment):
+ """
+ Provides a mechanism for sanitizing user input before presenting the
+ value to the backend.
+
+ Whoosh 1.X differs here in that you can no longer use a backslash
+ to escape reserved characters. Instead, the whole word should be
+ quoted.
+ """
+ words = query_fragment.split()
+ cleaned_words = []
+
+ for word in words:
+ if word in self.backend.RESERVED_WORDS:
+ word = word.replace(word, word.lower())
+
+ for char in self.backend.RESERVED_CHARACTERS:
+ if char in word:
+ word = "'%s'" % word
+ break
+
+ cleaned_words.append(word)
+
+ return ' '.join(cleaned_words)
+
+ def build_query_fragment(self, field, filter_type, value):
+ return value.query_string
+
+ def get_count(self):
+ results = self.get_results()
+ return len(results) if results else 0
+
+ def get_spelling_suggestion(self, preferred_query=None):
+ return self._spelling_suggestion
+
+ def build_params(self, spelling_query=None):
+ kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
+ return kwargs
+
+
+class ElasticSearchModelSearchForm(ModelSearchForm):
+
+ def search(self):
+ # 是否建议搜索
+ self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
+ sqs = super().search()
+ return sqs
+
+
+class ElasticSearchEngine(BaseEngine):
+ backend = ElasticSearchBackend
+ query = ElasticSearchQuery
diff --git a/src/DjangoBlog-master/djangoblog/feeds.py b/src/DjangoBlog-master/djangoblog/feeds.py
new file mode 100644
index 0000000..8c4e851
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/feeds.py
@@ -0,0 +1,40 @@
+from django.contrib.auth import get_user_model
+from django.contrib.syndication.views import Feed
+from django.utils import timezone
+from django.utils.feedgenerator import Rss201rev2Feed
+
+from blog.models import Article
+from djangoblog.utils import CommonMarkdown
+
+
+class DjangoBlogFeed(Feed):
+ feed_type = Rss201rev2Feed
+
+ description = '大巧无工,重剑无锋.'
+ title = "且听风吟 大巧无工,重剑无锋. "
+ link = "/feed/"
+
+ def author_name(self):
+ return get_user_model().objects.first().nickname
+
+ def author_link(self):
+ return get_user_model().objects.first().get_absolute_url()
+
+ def items(self):
+ return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
+
+ def item_title(self, item):
+ return item.title
+
+ def item_description(self, item):
+ return CommonMarkdown.get_markdown(item.body)
+
+ def feed_copyright(self):
+ now = timezone.now()
+ return "Copyright© {year} 且听风吟".format(year=now.year)
+
+ def item_link(self, item):
+ return item.get_absolute_url()
+
+ def item_guid(self, item):
+ return
diff --git a/src/DjangoBlog-master/djangoblog/logentryadmin.py b/src/DjangoBlog-master/djangoblog/logentryadmin.py
new file mode 100644
index 0000000..2f6a535
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/logentryadmin.py
@@ -0,0 +1,91 @@
+from django.contrib import admin
+from django.contrib.admin.models import DELETION
+from django.contrib.contenttypes.models import ContentType
+from django.urls import reverse, NoReverseMatch
+from django.utils.encoding import force_str
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+
+class LogEntryAdmin(admin.ModelAdmin):
+ list_filter = [
+ 'content_type'
+ ]
+
+ search_fields = [
+ 'object_repr',
+ 'change_message'
+ ]
+
+ list_display_links = [
+ 'action_time',
+ 'get_change_message',
+ ]
+ list_display = [
+ 'action_time',
+ 'user_link',
+ 'content_type',
+ 'object_link',
+ 'get_change_message',
+ ]
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ return (
+ request.user.is_superuser or
+ request.user.has_perm('admin.change_logentry')
+ ) and request.method != 'POST'
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ def object_link(self, obj):
+ object_link = escape(obj.object_repr)
+ content_type = obj.content_type
+
+ if obj.action_flag != DELETION and content_type is not None:
+ # try returning an actual link instead of object repr string
+ try:
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.object_id]
+ )
+ object_link = '{} '.format(url, object_link)
+ except NoReverseMatch:
+ pass
+ return mark_safe(object_link)
+
+ object_link.admin_order_field = 'object_repr'
+ object_link.short_description = _('object')
+
+ def user_link(self, obj):
+ content_type = ContentType.objects.get_for_model(type(obj.user))
+ user_link = escape(force_str(obj.user))
+ try:
+ # try returning an actual link instead of object repr string
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.user.pk]
+ )
+ user_link = '{} '.format(url, user_link)
+ except NoReverseMatch:
+ pass
+ return mark_safe(user_link)
+
+ user_link.admin_order_field = 'user'
+ user_link.short_description = _('user')
+
+ def get_queryset(self, request):
+ queryset = super(LogEntryAdmin, self).get_queryset(request)
+ return queryset.prefetch_related('content_type')
+
+ def get_actions(self, request):
+ actions = super(LogEntryAdmin, self).get_actions(request)
+ if 'delete_selected' in actions:
+ del actions['delete_selected']
+ return actions
diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py b/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py
new file mode 100644
index 0000000..2b4be5c
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py
@@ -0,0 +1,41 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class BasePlugin:
+ # 插件元数据
+ PLUGIN_NAME = None
+ PLUGIN_DESCRIPTION = None
+ PLUGIN_VERSION = None
+
+ def __init__(self):
+ if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
+ raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
+ self.init_plugin()
+ self.register_hooks()
+
+ def init_plugin(self):
+ """
+ 插件初始化逻辑
+ 子类可以重写此方法来实现特定的初始化操作
+ """
+ logger.info(f'{self.PLUGIN_NAME} initialized.')
+
+ def register_hooks(self):
+ """
+ 注册插件钩子
+ 子类可以重写此方法来注册特定的钩子
+ """
+ pass
+
+ def get_plugin_info(self):
+ """
+ 获取插件信息
+ :return: 包含插件元数据的字典
+ """
+ return {
+ 'name': self.PLUGIN_NAME,
+ 'description': self.PLUGIN_DESCRIPTION,
+ 'version': self.PLUGIN_VERSION
+ }
diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py b/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py
new file mode 100644
index 0000000..6685b7c
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py
@@ -0,0 +1,7 @@
+ARTICLE_DETAIL_LOAD = 'article_detail_load'
+ARTICLE_CREATE = 'article_create'
+ARTICLE_UPDATE = 'article_update'
+ARTICLE_DELETE = 'article_delete'
+
+ARTICLE_CONTENT_HOOK_NAME = "the_content"
+
diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py b/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py
new file mode 100644
index 0000000..d712540
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py
@@ -0,0 +1,44 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+_hooks = {}
+
+
+def register(hook_name: str, callback: callable):
+ """
+ 注册一个钩子回调。
+ """
+ if hook_name not in _hooks:
+ _hooks[hook_name] = []
+ _hooks[hook_name].append(callback)
+ logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
+
+
+def run_action(hook_name: str, *args, **kwargs):
+ """
+ 执行一个 Action Hook。
+ 它会按顺序执行所有注册到该钩子上的回调函数。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Running action hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ try:
+ callback(*args, **kwargs)
+ except Exception as e:
+ logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
+
+
+def apply_filters(hook_name: str, value, *args, **kwargs):
+ """
+ 执行一个 Filter Hook。
+ 它会把 value 依次传递给所有注册的回调函数进行处理。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Applying filter hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ try:
+ value = callback(value, *args, **kwargs)
+ except Exception as e:
+ logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
+ return value
diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py b/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py
new file mode 100644
index 0000000..12e824b
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py
@@ -0,0 +1,19 @@
+import os
+import logging
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+def load_plugins():
+ """
+ Dynamically loads and initializes plugins from the 'plugins' directory.
+ This function is intended to be called when the Django app registry is ready.
+ """
+ for plugin_name in settings.ACTIVE_PLUGINS:
+ plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
+ if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
+ try:
+ __import__(f'plugins.{plugin_name}.plugin')
+ logger.info(f"Successfully loaded plugin: {plugin_name}")
+ except ImportError as e:
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
\ No newline at end of file
diff --git a/src/DjangoBlog-master/djangoblog/settings.py b/src/DjangoBlog-master/djangoblog/settings.py
new file mode 100644
index 0000000..d076bb6
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/settings.py
@@ -0,0 +1,343 @@
+"""
+Django settings for djangoblog project.
+
+Generated by 'django-admin startproject' using Django 1.10.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+import os
+import sys
+from pathlib import Path
+
+from django.utils.translation import gettext_lazy as _
+
+
+def env_to_bool(env, default):
+ str_val = os.environ.get(env)
+ return default if str_val is None else str_val == 'True'
+
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ.get(
+ 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = env_to_bool('DJANGO_DEBUG', True)
+# DEBUG = False
+TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
+
+# ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
+# django 4.0新增配置
+CSRF_TRUSTED_ORIGINS = ['http://example.com']
+# Application definition
+
+
+INSTALLED_APPS = [
+ # 'django.contrib.admin',
+ 'django.contrib.admin.apps.SimpleAdminConfig',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.sites',
+ 'django.contrib.sitemaps',
+ 'mdeditor',
+ 'haystack',
+ 'blog',
+ 'accounts',
+ 'comments',
+ 'oauth',
+ 'servermanager',
+ 'owntracks',
+ 'compressor',
+ 'djangoblog'
+]
+
+MIDDLEWARE = [
+
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.gzip.GZipMiddleware',
+ # 'django.middleware.cache.UpdateCacheMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ # 'django.middleware.cache.FetchFromCacheMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.http.ConditionalGetMiddleware',
+ 'blog.middleware.OnlineMiddleware'
+]
+
+ROOT_URLCONF = 'djangoblog.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ 'blog.context_processors.seo_processor'
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'djangoblog.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
+ 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
+ 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
+ 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
+ 'PORT': int(
+ os.environ.get('DJANGO_MYSQL_PORT') or 3306),
+ 'OPTIONS': {
+ 'charset': 'utf8mb4'},
+ }}
+
+# Password validation
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+LANGUAGES = (
+ ('en', _('English')),
+ ('zh-hans', _('Simplified Chinese')),
+ ('zh-hant', _('Traditional Chinese')),
+)
+LOCALE_PATHS = (
+ os.path.join(BASE_DIR, 'locale'),
+)
+
+LANGUAGE_CODE = 'zh-hans'
+
+TIME_ZONE = 'Asia/Shanghai'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = False
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+
+
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
+ 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
+ },
+}
+# Automatically update searching index
+HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+# Allow user login with username and password
+AUTHENTICATION_BACKENDS = [
+ 'accounts.user_login_backend.EmailOrUsernameModelBackend']
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
+
+STATIC_URL = '/static/'
+STATICFILES = os.path.join(BASE_DIR, 'static')
+
+AUTH_USER_MODEL = 'accounts.BlogUser'
+LOGIN_URL = '/login/'
+
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
+DATE_TIME_FORMAT = '%Y-%m-%d'
+
+# bootstrap color styles
+BOOTSTRAP_COLOR_TYPES = [
+ 'default', 'primary', 'success', 'info', 'warning', 'danger'
+]
+
+# paginate
+PAGINATE_BY = 10
+# http cache timeout
+CACHE_CONTROL_MAX_AGE = 2592000
+# cache setting
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'TIMEOUT': 10800,
+ 'LOCATION': 'unique-snowflake',
+ }
+}
+# 使用redis作为缓存
+if os.environ.get("DJANGO_REDIS_URL"):
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+ 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
+ }
+ }
+
+SITE_ID = 1
+BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
+ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
+
+# Email:
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
+EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
+EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
+EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = EMAIL_HOST_USER
+# Setting debug=false did NOT handle except email notifications
+ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
+# WX ADMIN password(Two times md5)
+WXADMIN = os.environ.get(
+ 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
+
+LOG_PATH = os.path.join(BASE_DIR, 'logs')
+if not os.path.exists(LOG_PATH):
+ os.makedirs(LOG_PATH, exist_ok=True)
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'root': {
+ 'level': 'INFO',
+ 'handlers': ['console', 'log_file'],
+ },
+ 'formatters': {
+ 'verbose': {
+ 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
+ }
+ },
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse',
+ },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue',
+ },
+ },
+ 'handlers': {
+ 'log_file': {
+ 'level': 'INFO',
+ 'class': 'logging.handlers.TimedRotatingFileHandler',
+ 'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
+ 'when': 'D',
+ 'formatter': 'verbose',
+ 'interval': 1,
+ 'delay': True,
+ 'backupCount': 5,
+ 'encoding': 'utf-8'
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'filters': ['require_debug_true'],
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose'
+ },
+ 'null': {
+ 'class': 'logging.NullHandler',
+ },
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'djangoblog': {
+ 'handlers': ['log_file', 'console'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': False,
+ }
+ }
+}
+
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # other
+ 'compressor.finders.CompressorFinder',
+)
+COMPRESS_ENABLED = True
+# COMPRESS_OFFLINE = True
+
+
+COMPRESS_CSS_FILTERS = [
+ # creates absolute urls from relative ones
+ 'compressor.filters.css_default.CssAbsoluteFilter',
+ # css minimizer
+ 'compressor.filters.cssmin.CSSMinFilter'
+]
+COMPRESS_JS_FILTERS = [
+ 'compressor.filters.jsmin.JSMinFilter'
+]
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
+MEDIA_URL = '/media/'
+X_FRAME_OPTIONS = 'SAMEORIGIN'
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
+ ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
+ },
+ }
+ HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
+ },
+ }
+
+# Plugin System
+PLUGINS_DIR = BASE_DIR / 'plugins'
+ACTIVE_PLUGINS = [
+ 'article_copyright',
+ 'reading_time',
+ 'external_links',
+ 'view_count',
+ 'seo_optimizer'
+]
\ No newline at end of file
diff --git a/src/DjangoBlog-master/djangoblog/sitemap.py b/src/DjangoBlog-master/djangoblog/sitemap.py
new file mode 100644
index 0000000..8b7d446
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/sitemap.py
@@ -0,0 +1,59 @@
+from django.contrib.sitemaps import Sitemap
+from django.urls import reverse
+
+from blog.models import Article, Category, Tag
+
+
+class StaticViewSitemap(Sitemap):
+ priority = 0.5
+ changefreq = 'daily'
+
+ def items(self):
+ return ['blog:index', ]
+
+ def location(self, item):
+ return reverse(item)
+
+
+class ArticleSiteMap(Sitemap):
+ changefreq = "monthly"
+ priority = "0.6"
+
+ def items(self):
+ return Article.objects.filter(status='p')
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class CategorySiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.6"
+
+ def items(self):
+ return Category.objects.all()
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class TagSiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.3"
+
+ def items(self):
+ return Tag.objects.all()
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class UserSiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.3"
+
+ def items(self):
+ return list(set(map(lambda x: x.author, Article.objects.all())))
+
+ def lastmod(self, obj):
+ return obj.date_joined
diff --git a/src/DjangoBlog-master/djangoblog/spider_notify.py b/src/DjangoBlog-master/djangoblog/spider_notify.py
new file mode 100644
index 0000000..7b909e9
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/spider_notify.py
@@ -0,0 +1,21 @@
+import logging
+
+import requests
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+class SpiderNotify():
+ @staticmethod
+ def baidu_notify(urls):
+ try:
+ data = '\n'.join(urls)
+ result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
+ logger.info(result.text)
+ except Exception as e:
+ logger.error(e)
+
+ @staticmethod
+ def notify(url):
+ SpiderNotify.baidu_notify(url)
diff --git a/src/DjangoBlog-master/djangoblog/tests.py b/src/DjangoBlog-master/djangoblog/tests.py
new file mode 100644
index 0000000..01237d9
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/tests.py
@@ -0,0 +1,32 @@
+from django.test import TestCase
+
+from djangoblog.utils import *
+
+
+class DjangoBlogTest(TestCase):
+ def setUp(self):
+ pass
+
+ def test_utils(self):
+ md5 = get_sha256('test')
+ self.assertIsNotNone(md5)
+ c = CommonMarkdown.get_markdown('''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''')
+ self.assertIsNotNone(c)
+ d = {
+ 'd': 'key1',
+ 'd2': 'key2'
+ }
+ data = parse_dict_to_url(d)
+ self.assertIsNotNone(data)
diff --git a/src/DjangoBlog-master/djangoblog/urls.py b/src/DjangoBlog-master/djangoblog/urls.py
new file mode 100644
index 0000000..4aae58a
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/urls.py
@@ -0,0 +1,64 @@
+"""djangoblog URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.conf.urls.i18n import i18n_patterns
+from django.conf.urls.static import static
+from django.contrib.sitemaps.views import sitemap
+from django.urls import path, include
+from django.urls import re_path
+from haystack.views import search_view_factory
+
+from blog.views import EsSearchView
+from djangoblog.admin_site import admin_site
+from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
+from djangoblog.feeds import DjangoBlogFeed
+from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
+
+sitemaps = {
+
+ 'blog': ArticleSiteMap,
+ 'Category': CategorySiteMap,
+ 'Tag': TagSiteMap,
+ 'User': UserSiteMap,
+ 'static': StaticViewSitemap
+}
+
+handler404 = 'blog.views.page_not_found_view'
+handler500 = 'blog.views.server_error_view'
+handle403 = 'blog.views.permission_denied_view'
+
+urlpatterns = [
+ path('i18n/', include('django.conf.urls.i18n')),
+]
+urlpatterns += i18n_patterns(
+ re_path(r'^admin/', admin_site.urls),
+ re_path(r'', include('blog.urls', namespace='blog')),
+ re_path(r'mdeditor/', include('mdeditor.urls')),
+ re_path(r'', include('comments.urls', namespace='comment')),
+ re_path(r'', include('accounts.urls', namespace='account')),
+ re_path(r'', include('oauth.urls', namespace='oauth')),
+ re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
+ name='django.contrib.sitemaps.views.sitemap'),
+ re_path(r'^feed/$', DjangoBlogFeed()),
+ re_path(r'^rss/$', DjangoBlogFeed()),
+ re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
+ name='search'),
+ re_path(r'', include('servermanager.urls', namespace='servermanager')),
+ re_path(r'', include('owntracks.urls', namespace='owntracks'))
+ , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
diff --git a/src/DjangoBlog-master/djangoblog/utils.py b/src/DjangoBlog-master/djangoblog/utils.py
new file mode 100644
index 0000000..57f63dc
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/utils.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+
+import logging
+import os
+import random
+import string
+import uuid
+from hashlib import sha256
+
+import bleach
+import markdown
+import requests
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.core.cache import cache
+from django.templatetags.static import static
+
+logger = logging.getLogger(__name__)
+
+
+def get_max_articleid_commentid():
+ from blog.models import Article
+ from comments.models import Comment
+ return (Article.objects.latest().pk, Comment.objects.latest().pk)
+
+
+def get_sha256(str):
+ m = sha256(str.encode('utf-8'))
+ return m.hexdigest()
+
+
+def cache_decorator(expiration=3 * 60):
+ def wrapper(func):
+ def news(*args, **kwargs):
+ try:
+ view = args[0]
+ key = view.get_cache_key()
+ except:
+ key = None
+ if not key:
+ unique_str = repr((func, args, kwargs))
+
+ m = sha256(unique_str.encode('utf-8'))
+ key = m.hexdigest()
+ value = cache.get(key)
+ if value is not None:
+ # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
+ if str(value) == '__default_cache_value__':
+ return None
+ else:
+ return value
+ else:
+ logger.debug(
+ 'cache_decorator set cache:%s key:%s' %
+ (func.__name__, key))
+ value = func(*args, **kwargs)
+ if value is None:
+ cache.set(key, '__default_cache_value__', expiration)
+ else:
+ cache.set(key, value, expiration)
+ return value
+
+ return news
+
+ return wrapper
+
+
+def expire_view_cache(path, servername, serverport, key_prefix=None):
+ '''
+ 刷新视图缓存
+ :param path:url路径
+ :param servername:host
+ :param serverport:端口
+ :param key_prefix:前缀
+ :return:是否成功
+ '''
+ from django.http import HttpRequest
+ from django.utils.cache import get_cache_key
+
+ request = HttpRequest()
+ request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
+ request.path = path
+
+ key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
+ if key:
+ logger.info('expire_view_cache:get key:{path}'.format(path=path))
+ if cache.get(key):
+ cache.delete(key)
+ return True
+ return False
+
+
+@cache_decorator()
+def get_current_site():
+ site = Site.objects.get_current()
+ return site
+
+
+class CommonMarkdown:
+ @staticmethod
+ def _convert_markdown(value):
+ md = markdown.Markdown(
+ extensions=[
+ 'extra',
+ 'codehilite',
+ 'toc',
+ 'tables',
+ ]
+ )
+ body = md.convert(value)
+ toc = md.toc
+ return body, toc
+
+ @staticmethod
+ def get_markdown_with_toc(value):
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body, toc
+
+ @staticmethod
+ def get_markdown(value):
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body
+
+
+def send_email(emailto, title, content):
+ from djangoblog.blog_signals import send_email_signal
+ send_email_signal.send(
+ send_email.__class__,
+ emailto=emailto,
+ title=title,
+ content=content)
+
+
+def generate_code() -> str:
+ """生成随机数验证码"""
+ return ''.join(random.sample(string.digits, 6))
+
+
+def parse_dict_to_url(dict):
+ from urllib.parse import quote
+ url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
+ for k, v in dict.items()])
+ return url
+
+
+def get_blog_setting():
+ value = cache.get('get_blog_setting')
+ if value:
+ return value
+ else:
+ from blog.models import BlogSettings
+ if not BlogSettings.objects.count():
+ setting = BlogSettings()
+ setting.site_name = 'djangoblog'
+ setting.site_description = '基于Django的博客系统'
+ setting.site_seo_description = '基于Django的博客系统'
+ setting.site_keywords = 'Django,Python'
+ setting.article_sub_length = 300
+ setting.sidebar_article_count = 10
+ setting.sidebar_comment_count = 5
+ setting.show_google_adsense = False
+ setting.open_site_comment = True
+ setting.analytics_code = ''
+ setting.beian_code = ''
+ setting.show_gongan_code = False
+ setting.comment_need_review = False
+ setting.save()
+ value = BlogSettings.objects.first()
+ logger.info('set cache get_blog_setting')
+ cache.set('get_blog_setting', value)
+ return value
+
+
+def save_user_avatar(url):
+ '''
+ 保存用户头像
+ :param url:头像url
+ :return: 本地路径
+ '''
+ logger.info(url)
+
+ try:
+ basedir = os.path.join(settings.STATICFILES, 'avatar')
+ rsp = requests.get(url, timeout=2)
+ if rsp.status_code == 200:
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+
+ image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
+ isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
+ ext = os.path.splitext(url)[1] if isimage else '.jpg'
+ save_filename = str(uuid.uuid4().hex) + ext
+ logger.info('保存用户头像:' + basedir + save_filename)
+ with open(os.path.join(basedir, save_filename), 'wb+') as file:
+ file.write(rsp.content)
+ return static('avatar/' + save_filename)
+ except Exception as e:
+ logger.error(e)
+ return static('blog/img/avatar.png')
+
+
+def delete_sidebar_cache():
+ from blog.models import LinkShowType
+ keys = ["sidebar" + x for x in LinkShowType.values]
+ for k in keys:
+ logger.info('delete sidebar key:' + k)
+ cache.delete(k)
+
+
+def delete_view_cache(prefix, keys):
+ from django.core.cache.utils import make_template_fragment_key
+ key = make_template_fragment_key(prefix, keys)
+ cache.delete(key)
+
+
+def get_resource_url():
+ if settings.STATIC_URL:
+ return settings.STATIC_URL
+ else:
+ site = get_current_site()
+ return 'http://' + site.domain + '/static/'
+
+
+ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
+ 'h2', 'p']
+ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
+
+
+def sanitize_html(html):
+ return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
diff --git a/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py b/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py
new file mode 100644
index 0000000..04e3f7f
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py
@@ -0,0 +1,1044 @@
+# encoding: utf-8
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import json
+import os
+import re
+import shutil
+import threading
+import warnings
+
+import six
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from datetime import datetime
+from django.utils.encoding import force_str
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
+from haystack.constants import DJANGO_CT, DJANGO_ID, ID
+from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
+from haystack.inputs import Clean, Exact, PythonData, Raw
+from haystack.models import SearchResult
+from haystack.utils import get_identifier, get_model_ct
+from haystack.utils import log as logging
+from haystack.utils.app_loading import haystack_get_model
+from jieba.analyse import ChineseAnalyzer
+from whoosh import index
+from whoosh.analysis import StemmingAnalyzer
+from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
+from whoosh.fields import ID as WHOOSH_ID
+from whoosh.filedb.filestore import FileStorage, RamStorage
+from whoosh.highlight import ContextFragmenter, HtmlFormatter
+from whoosh.highlight import highlight as whoosh_highlight
+from whoosh.qparser import QueryParser
+from whoosh.searching import ResultsPage
+from whoosh.writing import AsyncWriter
+
+try:
+ import whoosh
+except ImportError:
+ raise MissingDependency(
+ "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
+
+# Handle minimum requirement.
+if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
+ raise MissingDependency(
+ "The 'whoosh' backend requires version 2.5.0 or greater.")
+
+# Bubble up the correct error.
+
+DATETIME_REGEX = re.compile(
+ '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$')
+LOCALS = threading.local()
+LOCALS.RAM_STORE = None
+
+
+class WhooshHtmlFormatter(HtmlFormatter):
+ """
+ This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
+ We use it to have consistent results across backends. Specifically,
+ Solr, Xapian and Elasticsearch are using this formatting.
+ """
+ template = '<%(tag)s>%(t)s%(tag)s>'
+
+
+class WhooshSearchBackend(BaseSearchBackend):
+ # Word reserved by Whoosh for special use.
+ RESERVED_WORDS = (
+ 'AND',
+ 'NOT',
+ 'OR',
+ 'TO',
+ )
+
+ # Characters reserved by Whoosh for special use.
+ # The '\\' must come first, so as not to overwrite the other slash
+ # replacements.
+ RESERVED_CHARACTERS = (
+ '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
+ '[', ']', '^', '"', '~', '*', '?', ':', '.',
+ )
+
+ def __init__(self, connection_alias, **connection_options):
+ super(
+ WhooshSearchBackend,
+ self).__init__(
+ connection_alias,
+ **connection_options)
+ self.setup_complete = False
+ self.use_file_storage = True
+ self.post_limit = getattr(
+ connection_options,
+ 'POST_LIMIT',
+ 128 * 1024 * 1024)
+ self.path = connection_options.get('PATH')
+
+ if connection_options.get('STORAGE', 'file') != 'file':
+ self.use_file_storage = False
+
+ if self.use_file_storage and not self.path:
+ raise ImproperlyConfigured(
+ "You must specify a 'PATH' in your settings for connection '%s'." %
+ connection_alias)
+
+ self.log = logging.getLogger('haystack')
+
+ def setup(self):
+ """
+ Defers loading until needed.
+ """
+ from haystack import connections
+ new_index = False
+
+ # Make sure the index is there.
+ if self.use_file_storage and not os.path.exists(self.path):
+ os.makedirs(self.path)
+ new_index = True
+
+ if self.use_file_storage and not os.access(self.path, os.W_OK):
+ raise IOError(
+ "The path to your Whoosh index '%s' is not writable for the current user/group." %
+ self.path)
+
+ if self.use_file_storage:
+ self.storage = FileStorage(self.path)
+ else:
+ global LOCALS
+
+ if getattr(LOCALS, 'RAM_STORE', None) is None:
+ LOCALS.RAM_STORE = RamStorage()
+
+ self.storage = LOCALS.RAM_STORE
+
+ self.content_field_name, self.schema = self.build_schema(
+ connections[self.connection_alias].get_unified_index().all_searchfields())
+ self.parser = QueryParser(self.content_field_name, schema=self.schema)
+
+ if new_index is True:
+ self.index = self.storage.create_index(self.schema)
+ else:
+ try:
+ self.index = self.storage.open_index(schema=self.schema)
+ except index.EmptyIndexError:
+ self.index = self.storage.create_index(self.schema)
+
+ self.setup_complete = True
+
+ def build_schema(self, fields):
+ schema_fields = {
+ ID: WHOOSH_ID(stored=True, unique=True),
+ DJANGO_CT: WHOOSH_ID(stored=True),
+ DJANGO_ID: WHOOSH_ID(stored=True),
+ }
+ # Grab the number of keys that are hard-coded into Haystack.
+ # We'll use this to (possibly) fail slightly more gracefully later.
+ initial_key_count = len(schema_fields)
+ content_field_name = ''
+
+ for field_name, field_class in fields.items():
+ if field_class.is_multivalued:
+ if field_class.indexed is False:
+ schema_fields[field_class.index_fieldname] = IDLIST(
+ stored=True, field_boost=field_class.boost)
+ else:
+ schema_fields[field_class.index_fieldname] = KEYWORD(
+ stored=True, commas=True, scorable=True, field_boost=field_class.boost)
+ elif field_class.field_type in ['date', 'datetime']:
+ schema_fields[field_class.index_fieldname] = DATETIME(
+ stored=field_class.stored, sortable=True)
+ elif field_class.field_type == 'integer':
+ schema_fields[field_class.index_fieldname] = NUMERIC(
+ stored=field_class.stored, numtype=int, field_boost=field_class.boost)
+ elif field_class.field_type == 'float':
+ schema_fields[field_class.index_fieldname] = NUMERIC(
+ stored=field_class.stored, numtype=float, field_boost=field_class.boost)
+ elif field_class.field_type == 'boolean':
+ # Field boost isn't supported on BOOLEAN as of 1.8.2.
+ schema_fields[field_class.index_fieldname] = BOOLEAN(
+ stored=field_class.stored)
+ elif field_class.field_type == 'ngram':
+ schema_fields[field_class.index_fieldname] = NGRAM(
+ minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
+ elif field_class.field_type == 'edge_ngram':
+ schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
+ stored=field_class.stored,
+ field_boost=field_class.boost)
+ else:
+ # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
+ schema_fields[field_class.index_fieldname] = TEXT(
+ stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
+ if field_class.document is True:
+ content_field_name = field_class.index_fieldname
+ schema_fields[field_class.index_fieldname].spelling = True
+
+ # Fail more gracefully than relying on the backend to die if no fields
+ # are found.
+ if len(schema_fields) <= initial_key_count:
+ raise SearchBackendError(
+ "No fields were found in any search_indexes. Please correct this before attempting to search.")
+
+ return (content_field_name, Schema(**schema_fields))
+
+ def update(self, index, iterable, commit=True):
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+ writer = AsyncWriter(self.index)
+
+ for obj in iterable:
+ try:
+ doc = index.full_prepare(obj)
+ except SkipDocument:
+ self.log.debug(u"Indexing for object `%s` skipped", obj)
+ else:
+ # Really make sure it's unicode, because Whoosh won't have it any
+ # other way.
+ for key in doc:
+ doc[key] = self._from_python(doc[key])
+
+ # Document boosts aren't supported in Whoosh 2.5.0+.
+ if 'boost' in doc:
+ del doc['boost']
+
+ try:
+ writer.update_document(**doc)
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ # We'll log the object identifier but won't include the actual object
+ # to avoid the possibility of that generating encoding errors while
+ # processing the log message:
+ self.log.error(
+ u"%s while preparing object for update" %
+ e.__class__.__name__,
+ exc_info=True,
+ extra={
+ "data": {
+ "index": index,
+ "object": get_identifier(obj)}})
+
+ if len(iterable) > 0:
+ # For now, commit no matter what, as we run into locking issues
+ # otherwise.
+ writer.commit()
+
+ def remove(self, obj_or_string, commit=True):
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+ whoosh_id = get_identifier(obj_or_string)
+
+ try:
+ self.index.delete_by_query(
+ q=self.parser.parse(
+ u'%s:"%s"' %
+ (ID, whoosh_id)))
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ self.log.error(
+ "Failed to remove document '%s' from Whoosh: %s",
+ whoosh_id,
+ e,
+ exc_info=True)
+
+ def clear(self, models=None, commit=True):
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+
+ if models is not None:
+ assert isinstance(models, (list, tuple))
+
+ try:
+ if models is None:
+ self.delete_index()
+ else:
+ models_to_delete = []
+
+ for model in models:
+ models_to_delete.append(
+ u"%s:%s" %
+ (DJANGO_CT, get_model_ct(model)))
+
+ self.index.delete_by_query(
+ q=self.parser.parse(
+ u" OR ".join(models_to_delete)))
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ if models is not None:
+ self.log.error(
+ "Failed to clear Whoosh index of models '%s': %s",
+ ','.join(models_to_delete),
+ e,
+ exc_info=True)
+ else:
+ self.log.error(
+ "Failed to clear Whoosh index: %s", e, exc_info=True)
+
+ def delete_index(self):
+ # Per the Whoosh mailing list, if wiping out everything from the index,
+ # it's much more efficient to simply delete the index files.
+ if self.use_file_storage and os.path.exists(self.path):
+ shutil.rmtree(self.path)
+ elif not self.use_file_storage:
+ self.storage.clean()
+
+ # Recreate everything.
+ self.setup()
+
+ def optimize(self):
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+ self.index.optimize()
+
+ def calculate_page(self, start_offset=0, end_offset=None):
+ # Prevent against Whoosh throwing an error. Requires an end_offset
+ # greater than 0.
+ if end_offset is not None and end_offset <= 0:
+ end_offset = 1
+
+ # Determine the page.
+ page_num = 0
+
+ if end_offset is None:
+ end_offset = 1000000
+
+ if start_offset is None:
+ start_offset = 0
+
+ page_length = end_offset - start_offset
+
+ if page_length and page_length > 0:
+ page_num = int(start_offset / page_length)
+
+ # Increment because Whoosh uses 1-based page numbers.
+ page_num += 1
+ return page_num, page_length
+
+ @log_query
+ def search(
+ self,
+ query_string,
+ sort_by=None,
+ start_offset=0,
+ end_offset=None,
+ fields='',
+ highlight=False,
+ facets=None,
+ date_facets=None,
+ query_facets=None,
+ narrow_queries=None,
+ spelling_query=None,
+ within=None,
+ dwithin=None,
+ distance_point=None,
+ models=None,
+ limit_to_registered_models=None,
+ result_class=None,
+ **kwargs):
+ if not self.setup_complete:
+ self.setup()
+
+ # A zero length query should return no results.
+ if len(query_string) == 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ query_string = force_str(query_string)
+
+ # A one-character query (non-wildcard) gets nabbed by a stopwords
+ # filter and should yield zero results.
+ if len(query_string) <= 1 and query_string != u'*':
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ reverse = False
+
+ if sort_by is not None:
+ # Determine if we need to reverse the results and if Whoosh can
+ # handle what it's being asked to sort by. Reversing is an
+ # all-or-nothing action, unfortunately.
+ sort_by_list = []
+ reverse_counter = 0
+
+ for order_by in sort_by:
+ if order_by.startswith('-'):
+ reverse_counter += 1
+
+ if reverse_counter and reverse_counter != len(sort_by):
+ raise SearchBackendError("Whoosh requires all order_by fields"
+ " to use the same sort direction")
+
+ for order_by in sort_by:
+ if order_by.startswith('-'):
+ sort_by_list.append(order_by[1:])
+
+ if len(sort_by_list) == 1:
+ reverse = True
+ else:
+ sort_by_list.append(order_by)
+
+ if len(sort_by_list) == 1:
+ reverse = False
+
+ sort_by = sort_by_list[0]
+
+ if facets is not None:
+ warnings.warn(
+ "Whoosh does not handle faceting.",
+ Warning,
+ stacklevel=2)
+
+ if date_facets is not None:
+ warnings.warn(
+ "Whoosh does not handle date faceting.",
+ Warning,
+ stacklevel=2)
+
+ if query_facets is not None:
+ warnings.warn(
+ "Whoosh does not handle query faceting.",
+ Warning,
+ stacklevel=2)
+
+ narrowed_results = None
+ self.index = self.index.refresh()
+
+ if limit_to_registered_models is None:
+ limit_to_registered_models = getattr(
+ settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
+
+ if models and len(models):
+ model_choices = sorted(get_model_ct(model) for model in models)
+ elif limit_to_registered_models:
+ # Using narrow queries, limit the results to only models handled
+ # with the current routers.
+ model_choices = self.build_models_list()
+ else:
+ model_choices = []
+
+ if len(model_choices) > 0:
+ if narrow_queries is None:
+ narrow_queries = set()
+
+ narrow_queries.add(' OR '.join(
+ ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
+
+ narrow_searcher = None
+
+ if narrow_queries is not None:
+ # Potentially expensive? I don't see another way to do it in
+ # Whoosh...
+ narrow_searcher = self.index.searcher()
+
+ for nq in narrow_queries:
+ recent_narrowed_results = narrow_searcher.search(
+ self.parser.parse(force_str(nq)), limit=None)
+
+ if len(recent_narrowed_results) <= 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ if narrowed_results:
+ narrowed_results.filter(recent_narrowed_results)
+ else:
+ narrowed_results = recent_narrowed_results
+
+ self.index = self.index.refresh()
+
+ if self.index.doc_count():
+ searcher = self.index.searcher()
+ parsed_query = self.parser.parse(query_string)
+
+ # In the event of an invalid/stopworded query, recover gracefully.
+ if parsed_query is None:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ page_num, page_length = self.calculate_page(
+ start_offset, end_offset)
+
+ search_kwargs = {
+ 'pagelen': page_length,
+ 'sortedby': sort_by,
+ 'reverse': reverse,
+ }
+
+ # Handle the case where the results have been narrowed.
+ if narrowed_results is not None:
+ search_kwargs['filter'] = narrowed_results
+
+ try:
+ raw_page = searcher.search_page(
+ parsed_query,
+ page_num,
+ **search_kwargs
+ )
+ except ValueError:
+ if not self.silently_fail:
+ raise
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ # Because as of Whoosh 2.5.1, it will return the wrong page of
+ # results if you request something too high. :(
+ if raw_page.pagenum < page_num:
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ results = self._process_results(
+ raw_page,
+ highlight=highlight,
+ query_string=query_string,
+ spelling_query=spelling_query,
+ result_class=result_class)
+ searcher.close()
+
+ if hasattr(narrow_searcher, 'close'):
+ narrow_searcher.close()
+
+ return results
+ else:
+ if self.include_spelling:
+ if spelling_query:
+ spelling_suggestion = self.create_spelling_suggestion(
+ spelling_query)
+ else:
+ spelling_suggestion = self.create_spelling_suggestion(
+ query_string)
+ else:
+ spelling_suggestion = None
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+ def more_like_this(
+ self,
+ model_instance,
+ additional_query_string=None,
+ start_offset=0,
+ end_offset=None,
+ models=None,
+ limit_to_registered_models=None,
+ result_class=None,
+ **kwargs):
+ if not self.setup_complete:
+ self.setup()
+
+ # Deferred models will have a different class ("RealClass_Deferred_fieldname")
+ # which won't be in our registry:
+ model_klass = model_instance._meta.concrete_model
+
+ field_name = self.content_field_name
+ narrow_queries = set()
+ narrowed_results = None
+ self.index = self.index.refresh()
+
+ if limit_to_registered_models is None:
+ limit_to_registered_models = getattr(
+ settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
+
+ if models and len(models):
+ model_choices = sorted(get_model_ct(model) for model in models)
+ elif limit_to_registered_models:
+ # Using narrow queries, limit the results to only models handled
+ # with the current routers.
+ model_choices = self.build_models_list()
+ else:
+ model_choices = []
+
+ if len(model_choices) > 0:
+ if narrow_queries is None:
+ narrow_queries = set()
+
+ narrow_queries.add(' OR '.join(
+ ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
+
+ if additional_query_string and additional_query_string != '*':
+ narrow_queries.add(additional_query_string)
+
+ narrow_searcher = None
+
+ if narrow_queries is not None:
+ # Potentially expensive? I don't see another way to do it in
+ # Whoosh...
+ narrow_searcher = self.index.searcher()
+
+ for nq in narrow_queries:
+ recent_narrowed_results = narrow_searcher.search(
+ self.parser.parse(force_str(nq)), limit=None)
+
+ if len(recent_narrowed_results) <= 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ if narrowed_results:
+ narrowed_results.filter(recent_narrowed_results)
+ else:
+ narrowed_results = recent_narrowed_results
+
+ page_num, page_length = self.calculate_page(start_offset, end_offset)
+
+ self.index = self.index.refresh()
+ raw_results = EmptyResults()
+
+ if self.index.doc_count():
+ query = "%s:%s" % (ID, get_identifier(model_instance))
+ searcher = self.index.searcher()
+ parsed_query = self.parser.parse(query)
+ results = searcher.search(parsed_query)
+
+ if len(results):
+ raw_results = results[0].more_like_this(
+ field_name, top=end_offset)
+
+ # Handle the case where the results have been narrowed.
+ if narrowed_results is not None and hasattr(raw_results, 'filter'):
+ raw_results.filter(narrowed_results)
+
+ try:
+ raw_page = ResultsPage(raw_results, page_num, page_length)
+ except ValueError:
+ if not self.silently_fail:
+ raise
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ # Because as of Whoosh 2.5.1, it will return the wrong page of
+ # results if you request something too high. :(
+ if raw_page.pagenum < page_num:
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ results = self._process_results(raw_page, result_class=result_class)
+ searcher.close()
+
+ if hasattr(narrow_searcher, 'close'):
+ narrow_searcher.close()
+
+ return results
+
+ def _process_results(
+ self,
+ raw_page,
+ highlight=False,
+ query_string='',
+ spelling_query=None,
+ result_class=None):
+ from haystack import connections
+ results = []
+
+ # It's important to grab the hits first before slicing. Otherwise, this
+ # can cause pagination failures.
+ hits = len(raw_page)
+
+ if result_class is None:
+ result_class = SearchResult
+
+ facets = {}
+ spelling_suggestion = None
+ unified_index = connections[self.connection_alias].get_unified_index()
+ indexed_models = unified_index.get_indexed_models()
+
+ for doc_offset, raw_result in enumerate(raw_page):
+ score = raw_page.score(doc_offset) or 0
+ app_label, model_name = raw_result[DJANGO_CT].split('.')
+ additional_fields = {}
+ model = haystack_get_model(app_label, model_name)
+
+ if model and model in indexed_models:
+ for key, value in raw_result.items():
+ index = unified_index.get_index(model)
+ string_key = str(key)
+
+ if string_key in index.fields and hasattr(
+ index.fields[string_key], 'convert'):
+ # Special-cased due to the nature of KEYWORD fields.
+ if index.fields[string_key].is_multivalued:
+ if value is None or len(value) == 0:
+ additional_fields[string_key] = []
+ else:
+ additional_fields[string_key] = value.split(
+ ',')
+ else:
+ additional_fields[string_key] = index.fields[string_key].convert(
+ value)
+ else:
+ additional_fields[string_key] = self._to_python(value)
+
+ del (additional_fields[DJANGO_CT])
+ del (additional_fields[DJANGO_ID])
+
+ if highlight:
+ sa = StemmingAnalyzer()
+ formatter = WhooshHtmlFormatter('em')
+ terms = [token.text for token in sa(query_string)]
+
+ whoosh_result = whoosh_highlight(
+ additional_fields.get(self.content_field_name),
+ terms,
+ sa,
+ ContextFragmenter(),
+ formatter
+ )
+ additional_fields['highlighted'] = {
+ self.content_field_name: [whoosh_result],
+ }
+
+ result = result_class(
+ app_label,
+ model_name,
+ raw_result[DJANGO_ID],
+ score,
+ **additional_fields)
+ results.append(result)
+ else:
+ hits -= 1
+
+ if self.include_spelling:
+ if spelling_query:
+ spelling_suggestion = self.create_spelling_suggestion(
+ spelling_query)
+ else:
+ spelling_suggestion = self.create_spelling_suggestion(
+ query_string)
+
+ return {
+ 'results': results,
+ 'hits': hits,
+ 'facets': facets,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+ def create_spelling_suggestion(self, query_string):
+ spelling_suggestion = None
+ reader = self.index.reader()
+ corrector = reader.corrector(self.content_field_name)
+ cleaned_query = force_str(query_string)
+
+ if not query_string:
+ return spelling_suggestion
+
+ # Clean the string.
+ for rev_word in self.RESERVED_WORDS:
+ cleaned_query = cleaned_query.replace(rev_word, '')
+
+ for rev_char in self.RESERVED_CHARACTERS:
+ cleaned_query = cleaned_query.replace(rev_char, '')
+
+ # Break it down.
+ query_words = cleaned_query.split()
+ suggested_words = []
+
+ for word in query_words:
+ suggestions = corrector.suggest(word, limit=1)
+
+ if len(suggestions) > 0:
+ suggested_words.append(suggestions[0])
+
+ spelling_suggestion = ' '.join(suggested_words)
+ return spelling_suggestion
+
+ def _from_python(self, value):
+ """
+ Converts Python values to a string for Whoosh.
+
+ Code courtesy of pysolr.
+ """
+ if hasattr(value, 'strftime'):
+ if not hasattr(value, 'hour'):
+ value = datetime(value.year, value.month, value.day, 0, 0, 0)
+ elif isinstance(value, bool):
+ if value:
+ value = 'true'
+ else:
+ value = 'false'
+ elif isinstance(value, (list, tuple)):
+ value = u','.join([force_str(v) for v in value])
+ elif isinstance(value, (six.integer_types, float)):
+ # Leave it alone.
+ pass
+ else:
+ value = force_str(value)
+ return value
+
+ def _to_python(self, value):
+ """
+ Converts values from Whoosh to native Python values.
+
+ A port of the same method in pysolr, as they deal with data the same way.
+ """
+ if value == 'true':
+ return True
+ elif value == 'false':
+ return False
+
+ if value and isinstance(value, six.string_types):
+ possible_datetime = DATETIME_REGEX.search(value)
+
+ if possible_datetime:
+ date_values = possible_datetime.groupdict()
+
+ for dk, dv in date_values.items():
+ date_values[dk] = int(dv)
+
+ return datetime(
+ date_values['year'],
+ date_values['month'],
+ date_values['day'],
+ date_values['hour'],
+ date_values['minute'],
+ date_values['second'])
+
+ try:
+ # Attempt to use json to load the values.
+ converted_value = json.loads(value)
+
+ # Try to handle most built-in types.
+ if isinstance(
+ converted_value,
+ (list,
+ tuple,
+ set,
+ dict,
+ six.integer_types,
+ float,
+ complex)):
+ return converted_value
+ except BaseException:
+ # If it fails (SyntaxError or its ilk) or we don't trust it,
+ # continue on.
+ pass
+
+ return value
+
+
+class WhooshSearchQuery(BaseSearchQuery):
+ def _convert_datetime(self, date):
+ if hasattr(date, 'hour'):
+ return force_str(date.strftime('%Y%m%d%H%M%S'))
+ else:
+ return force_str(date.strftime('%Y%m%d000000'))
+
+ def clean(self, query_fragment):
+ """
+ Provides a mechanism for sanitizing user input before presenting the
+ value to the backend.
+
+ Whoosh 1.X differs here in that you can no longer use a backslash
+ to escape reserved characters. Instead, the whole word should be
+ quoted.
+ """
+ words = query_fragment.split()
+ cleaned_words = []
+
+ for word in words:
+ if word in self.backend.RESERVED_WORDS:
+ word = word.replace(word, word.lower())
+
+ for char in self.backend.RESERVED_CHARACTERS:
+ if char in word:
+ word = "'%s'" % word
+ break
+
+ cleaned_words.append(word)
+
+ return ' '.join(cleaned_words)
+
+ def build_query_fragment(self, field, filter_type, value):
+ from haystack import connections
+ query_frag = ''
+ is_datetime = False
+
+ if not hasattr(value, 'input_type_name'):
+ # Handle when we've got a ``ValuesListQuerySet``...
+ if hasattr(value, 'values_list'):
+ value = list(value)
+
+ if hasattr(value, 'strftime'):
+ is_datetime = True
+
+ if isinstance(value, six.string_types) and value != ' ':
+ # It's not an ``InputType``. Assume ``Clean``.
+ value = Clean(value)
+ else:
+ value = PythonData(value)
+
+ # Prepare the query using the InputType.
+ prepared_value = value.prepare(self)
+
+ if not isinstance(prepared_value, (set, list, tuple)):
+ # Then convert whatever we get back to what pysolr wants if needed.
+ prepared_value = self.backend._from_python(prepared_value)
+
+ # 'content' is a special reserved word, much like 'pk' in
+ # Django's ORM layer. It indicates 'no special field'.
+ if field == 'content':
+ index_fieldname = ''
+ else:
+ index_fieldname = u'%s:' % connections[self._using].get_unified_index(
+ ).get_index_fieldname(field)
+
+ filter_types = {
+ 'content': '%s',
+ 'contains': '*%s*',
+ 'endswith': "*%s",
+ 'startswith': "%s*",
+ 'exact': '%s',
+ 'gt': "{%s to}",
+ 'gte': "[%s to]",
+ 'lt': "{to %s}",
+ 'lte': "[to %s]",
+ 'fuzzy': u'%s~',
+ }
+
+ if value.post_process is False:
+ query_frag = prepared_value
+ else:
+ if filter_type in [
+ 'content',
+ 'contains',
+ 'startswith',
+ 'endswith',
+ 'fuzzy']:
+ if value.input_type_name == 'exact':
+ query_frag = prepared_value
+ else:
+ # Iterate over terms & incorportate the converted form of
+ # each into the query.
+ terms = []
+
+ if isinstance(prepared_value, six.string_types):
+ possible_values = prepared_value.split(' ')
+ else:
+ if is_datetime is True:
+ prepared_value = self._convert_datetime(
+ prepared_value)
+
+ possible_values = [prepared_value]
+
+ for possible_value in possible_values:
+ terms.append(
+ filter_types[filter_type] %
+ self.backend._from_python(possible_value))
+
+ if len(terms) == 1:
+ query_frag = terms[0]
+ else:
+ query_frag = u"(%s)" % " AND ".join(terms)
+ elif filter_type == 'in':
+ in_options = []
+
+ for possible_value in prepared_value:
+ is_datetime = False
+
+ if hasattr(possible_value, 'strftime'):
+ is_datetime = True
+
+ pv = self.backend._from_python(possible_value)
+
+ if is_datetime is True:
+ pv = self._convert_datetime(pv)
+
+ if isinstance(pv, six.string_types) and not is_datetime:
+ in_options.append('"%s"' % pv)
+ else:
+ in_options.append('%s' % pv)
+
+ query_frag = "(%s)" % " OR ".join(in_options)
+ elif filter_type == 'range':
+ start = self.backend._from_python(prepared_value[0])
+ end = self.backend._from_python(prepared_value[1])
+
+ if hasattr(prepared_value[0], 'strftime'):
+ start = self._convert_datetime(start)
+
+ if hasattr(prepared_value[1], 'strftime'):
+ end = self._convert_datetime(end)
+
+ query_frag = u"[%s to %s]" % (start, end)
+ elif filter_type == 'exact':
+ if value.input_type_name == 'exact':
+ query_frag = prepared_value
+ else:
+ prepared_value = Exact(prepared_value).prepare(self)
+ query_frag = filter_types[filter_type] % prepared_value
+ else:
+ if is_datetime is True:
+ prepared_value = self._convert_datetime(prepared_value)
+
+ query_frag = filter_types[filter_type] % prepared_value
+
+ if len(query_frag) and not isinstance(value, Raw):
+ if not query_frag.startswith('(') and not query_frag.endswith(')'):
+ query_frag = "(%s)" % query_frag
+
+ return u"%s%s" % (index_fieldname, query_frag)
+
+ # if not filter_type in ('in', 'range'):
+ # # 'in' is a bit of a special case, as we don't want to
+ # # convert a valid list/tuple to string. Defer handling it
+ # # until later...
+ # value = self.backend._from_python(value)
+
+
+class WhooshEngine(BaseEngine):
+ backend = WhooshSearchBackend
+ query = WhooshSearchQuery
diff --git a/src/DjangoBlog-master/djangoblog/wsgi.py b/src/DjangoBlog-master/djangoblog/wsgi.py
new file mode 100644
index 0000000..2295efd
--- /dev/null
+++ b/src/DjangoBlog-master/djangoblog/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for djangoblog project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
+
+application = get_wsgi_application()
diff --git a/src/DjangoBlog-master/docs/README-en.md b/src/DjangoBlog-master/docs/README-en.md
new file mode 100644
index 0000000..37ea069
--- /dev/null
+++ b/src/DjangoBlog-master/docs/README-en.md
@@ -0,0 +1,158 @@
+# DjangoBlog
+
+
+
+
+
+
+
+
+
+ A powerful, elegant, and modern blog system.
+
+ English • 简体中文
+
+
+---
+
+DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
+
+## ✨ Features
+
+- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
+- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
+- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
+- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
+- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
+- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
+- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
+- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
+- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
+- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
+- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
+
+## 🛠️ Tech Stack
+
+- **Backend**: Python 3.10, Django 4.0
+- **Database**: MySQL, SQLite (configurable)
+- **Cache**: Redis
+- **Frontend**: HTML5, CSS3, JavaScript
+- **Search**: Whoosh, Elasticsearch (configurable)
+- **Editor**: Markdown (mdeditor)
+
+## 🚀 Getting Started
+
+### 1. Prerequisites
+
+Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
+
+### 2. Clone & Installation
+
+```bash
+# Clone the project to your local machine
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
+
+# Install dependencies
+pip install -r requirements.txt
+```
+
+### 3. Project Configuration
+
+- **Database**:
+ Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ Create the database in MySQL:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **More Configurations**:
+ For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
+
+### 4. Database Initialization
+
+```bash
+python manage.py makemigrations
+python manage.py migrate
+
+# Create a superuser account
+python manage.py createsuperuser
+```
+
+### 5. Running the Project
+
+```bash
+# (Optional) Generate some test data
+python manage.py create_testdata
+
+# (Optional) Collect and compress static files
+python manage.py collectstatic --noinput
+python manage.py compress --force
+
+# Start the development server
+python manage.py runserver
+```
+
+Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
+
+## Deployment
+
+- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
+- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
+- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
+
+## 🧩 Plugin System
+
+The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
+
+- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
+- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
+- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
+
+## 🤝 Contributing
+
+We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
+
+## 📄 License
+
+This project is open-sourced under the [MIT License](LICENSE).
+
+---
+
+## ❤️ Support & Sponsorship
+
+If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
+
+
+
+
+
+
+ (Left) Alipay / (Right) WeChat
+
+
+## 🙏 Acknowledgements
+
+A special thanks to **JetBrains** for providing a free open-source license for this project.
+
+
+
+
+
+
+
+---
+> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.
diff --git a/src/DjangoBlog-master/docs/config-en.md b/src/DjangoBlog-master/docs/config-en.md
new file mode 100644
index 0000000..b877efb
--- /dev/null
+++ b/src/DjangoBlog-master/docs/config-en.md
@@ -0,0 +1,64 @@
+# Introduction to main features settings
+
+## Cache:
+Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`.
+```python
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'LOCATION': '127.0.0.1:11211',
+ 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog',
+ 'TIMEOUT': 60 * 60 * 10
+ },
+ 'locmemcache': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'TIMEOUT': 10800,
+ 'LOCATION': 'unique-snowflake',
+ }
+}
+```
+
+## OAuth Login:
+QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration.
+
+### Callback address examples:
+QQ: http://your-domain-name/oauth/authorize?type=qq
+Weibo: http://your-domain-name/oauth/authorize?type=weibo
+type is in the type field of `oauthmanager`.
+
+## owntracks:
+owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap.
+
+## Email feature:
+Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify:
+```python
+EMAIL_HOST = 'smtp.zoho.com'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER')
+```
+with your email account information.
+
+## WeChat Official Account
+Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account.
+
+## Introduction to website configuration
+You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc.
+OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default.
+
+## Source code highlighting
+If the code block in your article didn't show hightlight, please write the code blocks as following:
+
+
+
+That is, you should add the corresponding language name before the code block.
+
+## Update
+If you get errors as following while executing database migrations:
+```python
+django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
+```
+This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed.
+
diff --git a/src/DjangoBlog-master/docs/config.md b/src/DjangoBlog-master/docs/config.md
new file mode 100644
index 0000000..24673a3
--- /dev/null
+++ b/src/DjangoBlog-master/docs/config.md
@@ -0,0 +1,58 @@
+# 主要功能配置介绍:
+
+## 缓存:
+缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。
+https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199
+
+
+## oauth登录:
+
+现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在
+**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。
+### 回调地址示例:
+qq:http://你的域名/oauth/authorize?type=qq
+微博:http://你的域名/oauth/authorize?type=weibo
+type对应在`oauthmanager`中的type字段。
+
+## owntracks:
+owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为:
+`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。
+
+## 邮件功能:
+同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改:
+```python
+EMAIL_HOST = 'smtp.zoho.com'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER')
+```
+为你自己的邮箱配置。
+
+## 微信公众号
+集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。
+然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。
+## 网站配置介绍
+在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。
+其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。
+## 代码高亮
+如果你发现你文章的代码没有高亮,请这样书写代码块:
+
+
+
+
+也就是说,需要在代码块开始位置加入这段代码对应的语言。
+
+## update
+如果你发现执行数据库迁移的时候出现如下报错:
+```python
+django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
+```
+可能是因为你的mysql版本低于5.6,需要升级mysql版本>=5.6即可。
+
+
+django 4.0登录可能会报错CSRF,需要配置下`settings.py`中的`CSRF_TRUSTED_ORIGINS`
+
+https://github.com/liangliangyy/DjangoBlog/blob/master/djangoblog/settings.py#L39
+
diff --git a/src/DjangoBlog-master/docs/docker-en.md b/src/DjangoBlog-master/docs/docker-en.md
new file mode 100644
index 0000000..8d5d59e
--- /dev/null
+++ b/src/DjangoBlog-master/docs/docker-en.md
@@ -0,0 +1,114 @@
+# Deploying DjangoBlog with Docker
+
+
+
+
+
+This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
+
+## 1. Prerequisites
+
+Before you begin, please ensure you have the following software installed on your system:
+- [Docker Engine](https://docs.docker.com/engine/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
+
+## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
+
+This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
+
+### Step 1: Start the Basic Services
+
+From the project's root directory, run the following command:
+
+```bash
+# Build and start the containers in detached mode (includes Django app and MySQL)
+docker-compose up -d --build
+```
+
+`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
+
+- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
+- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
+
+### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
+
+If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
+
+```bash
+# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
+docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
+```
+- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
+
+### Step 3: First-Time Initialization
+
+After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
+
+```bash
+# Get a shell inside the djangoblog application container (named 'web')
+docker-compose exec web bash
+
+# Inside the container, run the following commands:
+# Create a superuser account (follow the prompts to set username, email, and password)
+python manage.py createsuperuser
+
+# (Optional) Create some test data
+python manage.py create_testdata
+
+# (Optional, if ES is enabled) Create the search index
+python manage.py rebuild_index
+
+# Exit the container
+exit
+```
+
+## 3. Alternative Method: Using the Standalone Docker Image
+
+If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
+
+```bash
+# Pull the latest image from Docker Hub
+docker pull liangliangyy/djangoblog:latest
+
+# Run the container and connect it to your external database
+docker run -d \
+ -p 8000:8000 \
+ -e DJANGO_SECRET_KEY='your-strong-secret-key' \
+ -e DJANGO_MYSQL_HOST='your-mysql-host' \
+ -e DJANGO_MYSQL_USER='your-mysql-user' \
+ -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
+ -e DJANGO_MYSQL_DATABASE='djangoblog' \
+ --name djangoblog \
+ liangliangyy/djangoblog:latest
+```
+
+- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
+- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
+
+## 4. Configuration (Environment Variables)
+
+Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
+
+| Environment Variable | Default/Example Value | Notes |
+|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
+| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
+| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
+| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
+| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
+| `DJANGO_MYSQL_USER` | `root` | Database username. |
+| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
+| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
+| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
+| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
+| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
+| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
+| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
+| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
+| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
+| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
+| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
+
+---
+
+After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.
\ No newline at end of file
diff --git a/src/DjangoBlog-master/docs/docker.md b/src/DjangoBlog-master/docs/docker.md
new file mode 100644
index 0000000..e7c255a
--- /dev/null
+++ b/src/DjangoBlog-master/docs/docker.md
@@ -0,0 +1,114 @@
+# 使用 Docker 部署 DjangoBlog
+
+
+
+
+
+本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
+
+## 1. 环境准备
+
+在开始之前,请确保您的系统中已经安装了以下软件:
+- [Docker Engine](https://docs.docker.com/engine/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
+
+## 2. 推荐方式:使用 `docker-compose` (一键部署)
+
+这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
+
+### 步骤 1: 启动基础服务
+
+在项目根目录下,执行以下命令:
+
+```bash
+# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
+docker-compose up -d --build
+```
+
+`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
+
+- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
+- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
+
+### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
+
+如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
+
+```bash
+# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
+docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
+```
+- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
+
+### 步骤 3: 首次运行的初始化操作
+
+当容器首次启动后,您需要进入容器来执行一些初始化命令。
+
+```bash
+# 进入 djangoblog 应用容器
+docker-compose exec web bash
+
+# 在容器内执行以下命令:
+# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
+python manage.py createsuperuser
+
+# (可选) 创建一些测试数据
+python manage.py create_testdata
+
+# (可选,如果启用了 ES) 创建索引
+python manage.py rebuild_index
+
+# 退出容器
+exit
+```
+
+## 3. 备选方式:使用独立的 Docker 镜像
+
+如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
+
+```bash
+# 从 Docker Hub 拉取最新镜像
+docker pull liangliangyy/djangoblog:latest
+
+# 运行容器,并链接到您的外部数据库
+docker run -d \
+ -p 8000:8000 \
+ -e DJANGO_SECRET_KEY='your-strong-secret-key' \
+ -e DJANGO_MYSQL_HOST='your-mysql-host' \
+ -e DJANGO_MYSQL_USER='your-mysql-user' \
+ -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
+ -e DJANGO_MYSQL_DATABASE='djangoblog' \
+ --name djangoblog \
+ liangliangyy/djangoblog:latest
+```
+
+- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。
+- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
+
+## 4. 配置说明 (环境变量)
+
+本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
+
+| 环境变量名称 | 默认值/示例 | 备注 |
+|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
+| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
+| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
+| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
+| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
+| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
+| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
+| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
+| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
+| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
+| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
+| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
+| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
+| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
+| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
+| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
+| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
+
+---
+
+部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。
diff --git a/src/DjangoBlog-master/docs/es.md b/src/DjangoBlog-master/docs/es.md
new file mode 100644
index 0000000..97226c5
--- /dev/null
+++ b/src/DjangoBlog-master/docs/es.md
@@ -0,0 +1,28 @@
+# 集成Elasticsearch
+如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单,
+首先需要注意如下几点:
+1. 你的`Elasticsearch`支持`ik`中文分词
+2. 你的`Elasticsearch`版本>=7.3.0
+
+接下来在`settings.py`做如下改动即可:
+- 增加es链接,如下所示:
+```python
+ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': '127.0.0.1:9200'
+ },
+}
+```
+- 修改`HAYSTACK`配置:
+```python
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
+ },
+}
+```
+然后终端执行:
+```shell script
+./manage.py build_index
+```
+这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。
\ No newline at end of file
diff --git a/src/DjangoBlog-master/docs/imgs/alipay.jpg b/src/DjangoBlog-master/docs/imgs/alipay.jpg
new file mode 100644
index 0000000..424d70a
Binary files /dev/null and b/src/DjangoBlog-master/docs/imgs/alipay.jpg differ
diff --git a/src/DjangoBlog-master/docs/imgs/pycharm_logo.png b/src/DjangoBlog-master/docs/imgs/pycharm_logo.png
new file mode 100644
index 0000000..7f2a4b0
Binary files /dev/null and b/src/DjangoBlog-master/docs/imgs/pycharm_logo.png differ
diff --git a/src/DjangoBlog-master/docs/imgs/wechat.jpg b/src/DjangoBlog-master/docs/imgs/wechat.jpg
new file mode 100644
index 0000000..7edf525
Binary files /dev/null and b/src/DjangoBlog-master/docs/imgs/wechat.jpg differ
diff --git a/src/DjangoBlog-master/docs/k8s-en.md b/src/DjangoBlog-master/docs/k8s-en.md
new file mode 100644
index 0000000..20e9527
--- /dev/null
+++ b/src/DjangoBlog-master/docs/k8s-en.md
@@ -0,0 +1,141 @@
+# Deploying DjangoBlog with Kubernetes
+
+This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
+
+## Architecture Overview
+
+This deployment utilizes a microservices-based, cloud-native architecture:
+
+- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
+- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
+- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
+- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
+- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
+
+## 1. Prerequisites
+
+Before you begin, please ensure you have the following:
+
+- A running Kubernetes cluster.
+- The `kubectl` command-line tool configured to connect to your cluster.
+- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
+- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
+
+## 2. Deployment Steps
+
+### Step 1: Create a Namespace
+
+We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
+
+```bash
+# Create a namespace named 'djangoblog'
+kubectl create namespace djangoblog
+```
+
+### Step 2: Configure Persistent Storage
+
+This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
+
+```bash
+# Log in to your master node
+ssh user@master-node
+
+# Create the required storage directories
+sudo mkdir -p /mnt/local-storage-db
+sudo mkdir -p /mnt/local-storage-djangoblog
+sudo mkdir -p /mnt/resource/
+sudo mkdir -p /mnt/local-storage-elasticsearch
+
+# Log out from the node
+exit
+```
+**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
+
+After creating the directories, apply the storage-related configurations:
+
+```bash
+# Apply the StorageClass
+kubectl apply -f deploy/k8s/storageclass.yaml
+
+# Apply the PersistentVolumes (PVs)
+kubectl apply -f deploy/k8s/pv.yaml
+
+# Apply the PersistentVolumeClaims (PVCs)
+kubectl apply -f deploy/k8s/pvc.yaml
+```
+
+### Step 3: Configure the Application
+
+Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
+
+**It is strongly recommended to change the following fields:**
+- `DJANGO_SECRET_KEY`: Change to a random, complex string.
+- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
+
+```bash
+# Edit the ConfigMap file
+vim deploy/k8s/configmap.yaml
+
+# Apply the configuration
+kubectl apply -f deploy/k8s/configmap.yaml
+```
+
+### Step 4: Deploy the Application Stack
+
+Now, we can deploy all the core services.
+
+```bash
+# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
+kubectl apply -f deploy/k8s/deployment.yaml
+
+# Deploy the Services (to create internal endpoints for the Deployments)
+kubectl apply -f deploy/k8s/service.yaml
+```
+
+The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
+
+```bash
+kubectl get pods -n djangoblog -w
+```
+
+### Step 5: Expose the Application Externally
+
+Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
+
+```bash
+# Apply the Ingress rule
+kubectl apply -f deploy/k8s/gateway.yaml
+```
+
+Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
+
+```bash
+kubectl get ingress -n djangoblog
+```
+
+### Step 6: First-Time Initialization
+
+Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
+
+```bash
+# First, get the name of a djangoblog pod
+kubectl get pods -n djangoblog | grep djangoblog
+
+# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
+kubectl exec -it [pod-name] -n djangoblog -- bash
+
+# Inside the Pod, run the following commands:
+# Create a superuser account (follow the prompts)
+python manage.py createsuperuser
+
+# (Optional) Create some test data
+python manage.py create_testdata
+
+# (Optional, if ES is enabled) Create the search index
+python manage.py rebuild_index
+
+# Exit the Pod
+exit
+```
+
+Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.
\ No newline at end of file
diff --git a/src/DjangoBlog-master/docs/k8s.md b/src/DjangoBlog-master/docs/k8s.md
new file mode 100644
index 0000000..9da3c28
--- /dev/null
+++ b/src/DjangoBlog-master/docs/k8s.md
@@ -0,0 +1,141 @@
+# 使用 Kubernetes 部署 DjangoBlog
+
+本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。
+
+## 架构概览
+
+本次部署采用的是微服务化的云原生架构:
+
+- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。
+- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。**
+- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。
+- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。
+- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。
+
+## 1. 环境准备
+
+在开始之前,请确保您已具备以下环境:
+
+- 一个正在运行的 Kubernetes 集群。
+- `kubectl` 命令行工具已配置并能够连接到您的集群。
+- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。
+- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。
+
+## 2. 部署步骤
+
+### 步骤 1: 创建命名空间
+
+我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。
+
+```bash
+# 创建一个名为 djangoblog 的命名空间
+kubectl create namespace djangoblog
+```
+
+### 步骤 2: 配置持久化存储
+
+此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。
+
+```bash
+# 登录到您的 master 节点
+ssh user@master-node
+
+# 创建所需的存储目录
+sudo mkdir -p /mnt/local-storage-db
+sudo mkdir -p /mnt/local-storage-djangoblog
+sudo mkdir -p /mnt/resource/
+sudo mkdir -p /mnt/local-storage-elasticsearch
+
+# 退出节点
+exit
+```
+**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。
+
+创建目录后,应用存储相关的配置文件:
+
+```bash
+# 应用 StorageClass
+kubectl apply -f deploy/k8s/storageclass.yaml
+
+# 应用 PersistentVolume (PV)
+kubectl apply -f deploy/k8s/pv.yaml
+
+# 应用 PersistentVolumeClaim (PVC)
+kubectl apply -f deploy/k8s/pvc.yaml
+```
+
+### 步骤 3: 配置应用
+
+在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。
+
+**强烈建议修改以下字段:**
+- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。
+- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。
+
+```bash
+# 编辑 ConfigMap 文件
+vim deploy/k8s/configmap.yaml
+
+# 应用配置
+kubectl apply -f deploy/k8s/configmap.yaml
+```
+
+### 步骤 4: 部署应用服务栈
+
+现在,我们可以部署所有的核心服务了。
+
+```bash
+# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
+kubectl apply -f deploy/k8s/deployment.yaml
+
+# 部署 Services (为 Deployments 创建内部访问端点)
+kubectl apply -f deploy/k8s/service.yaml
+```
+
+部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`):
+
+```bash
+kubectl get pods -n djangoblog -w
+```
+
+### 步骤 5: 暴露应用到外部
+
+最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。
+
+```bash
+# 应用 Ingress 规则
+kubectl apply -f deploy/k8s/gateway.yaml
+```
+
+部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址:
+
+```bash
+kubectl get ingress -n djangoblog
+```
+
+### 步骤 6: 首次运行的初始化操作
+
+与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。
+
+```bash
+# 首先,获取 djangoblog pod 的名称
+kubectl get pods -n djangoblog | grep djangoblog
+
+# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称)
+kubectl exec -it [pod-name] -n djangoblog -- bash
+
+# 在 Pod 内部执行以下命令:
+# 创建超级管理员账户 (请按照提示操作)
+python manage.py createsuperuser
+
+# (可选) 创建测试数据
+python manage.py create_testdata
+
+# (可选,如果启用了 ES) 创建索引
+python manage.py rebuild_index
+
+# 退出 Pod
+exit
+```
+
+至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署!
\ No newline at end of file
diff --git a/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..f63669f
Binary files /dev/null and b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo differ
diff --git a/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c80b30a
--- /dev/null
+++ b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,685 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-09-13 16:02+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: .\accounts\admin.py:12
+msgid "password"
+msgstr "password"
+
+#: .\accounts\admin.py:13
+msgid "Enter password again"
+msgstr "Enter password again"
+
+#: .\accounts\admin.py:24 .\accounts\forms.py:89
+msgid "passwords do not match"
+msgstr "passwords do not match"
+
+#: .\accounts\forms.py:36
+msgid "email already exists"
+msgstr "email already exists"
+
+#: .\accounts\forms.py:46 .\accounts\forms.py:50
+msgid "New password"
+msgstr "New password"
+
+#: .\accounts\forms.py:60
+msgid "Confirm password"
+msgstr "Confirm password"
+
+#: .\accounts\forms.py:70 .\accounts\forms.py:116
+msgid "Email"
+msgstr "Email"
+
+#: .\accounts\forms.py:76 .\accounts\forms.py:80
+msgid "Code"
+msgstr "Code"
+
+#: .\accounts\forms.py:100 .\accounts\tests.py:194
+msgid "email does not exist"
+msgstr "email does not exist"
+
+#: .\accounts\models.py:12 .\oauth\models.py:17
+msgid "nick name"
+msgstr "nick name"
+
+#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
+#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
+#: .\oauth\models.py:53
+msgid "creation time"
+msgstr "creation time"
+
+#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
+#: .\oauth\models.py:54
+msgid "last modify time"
+msgstr "last modify time"
+
+#: .\accounts\models.py:15
+msgid "create source"
+msgstr "create source"
+
+#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
+msgid "user"
+msgstr "user"
+
+#: .\accounts\tests.py:216 .\accounts\utils.py:39
+msgid "Verification code error"
+msgstr "Verification code error"
+
+#: .\accounts\utils.py:13
+msgid "Verify Email"
+msgstr "Verify Email"
+
+#: .\accounts\utils.py:21
+#, python-format
+msgid ""
+"You are resetting the password, the verification code is:%(code)s, valid "
+"within 5 minutes, please keep it properly"
+msgstr ""
+"You are resetting the password, the verification code is:%(code)s, valid "
+"within 5 minutes, please keep it properly"
+
+#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
+#: .\oauth\models.py:12
+msgid "author"
+msgstr "author"
+
+#: .\blog\admin.py:53
+msgid "Publish selected articles"
+msgstr "Publish selected articles"
+
+#: .\blog\admin.py:54
+msgid "Draft selected articles"
+msgstr "Draft selected articles"
+
+#: .\blog\admin.py:55
+msgid "Close article comments"
+msgstr "Close article comments"
+
+#: .\blog\admin.py:56
+msgid "Open article comments"
+msgstr "Open article comments"
+
+#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
+#: .\templates\blog\tags\sidebar.html:40
+msgid "category"
+msgstr "category"
+
+#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
+msgid "index"
+msgstr "index"
+
+#: .\blog\models.py:21
+msgid "list"
+msgstr "list"
+
+#: .\blog\models.py:22
+msgid "post"
+msgstr "post"
+
+#: .\blog\models.py:23
+msgid "all"
+msgstr "all"
+
+#: .\blog\models.py:24
+msgid "slide"
+msgstr "slide"
+
+#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
+msgid "modify time"
+msgstr "modify time"
+
+#: .\blog\models.py:63
+msgid "Draft"
+msgstr "Draft"
+
+#: .\blog\models.py:64
+msgid "Published"
+msgstr "Published"
+
+#: .\blog\models.py:67
+msgid "Open"
+msgstr "Open"
+
+#: .\blog\models.py:68
+msgid "Close"
+msgstr "Close"
+
+#: .\blog\models.py:71 .\comments\admin.py:47
+msgid "Article"
+msgstr "Article"
+
+#: .\blog\models.py:72
+msgid "Page"
+msgstr "Page"
+
+#: .\blog\models.py:74 .\blog\models.py:280
+msgid "title"
+msgstr "title"
+
+#: .\blog\models.py:75
+msgid "body"
+msgstr "body"
+
+#: .\blog\models.py:77
+msgid "publish time"
+msgstr "publish time"
+
+#: .\blog\models.py:79
+msgid "status"
+msgstr "status"
+
+#: .\blog\models.py:84
+msgid "comment status"
+msgstr "comment status"
+
+#: .\blog\models.py:88 .\oauth\models.py:43
+msgid "type"
+msgstr "type"
+
+#: .\blog\models.py:89
+msgid "views"
+msgstr "views"
+
+#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
+msgid "order"
+msgstr "order"
+
+#: .\blog\models.py:98
+msgid "show toc"
+msgstr "show toc"
+
+#: .\blog\models.py:105 .\blog\models.py:249
+msgid "tag"
+msgstr "tag"
+
+#: .\blog\models.py:115 .\comments\models.py:21
+msgid "article"
+msgstr "article"
+
+#: .\blog\models.py:171
+msgid "category name"
+msgstr "category name"
+
+#: .\blog\models.py:174
+msgid "parent category"
+msgstr "parent category"
+
+#: .\blog\models.py:234
+msgid "tag name"
+msgstr "tag name"
+
+#: .\blog\models.py:256
+msgid "link name"
+msgstr "link name"
+
+#: .\blog\models.py:257 .\blog\models.py:271
+msgid "link"
+msgstr "link"
+
+#: .\blog\models.py:260
+msgid "is show"
+msgstr "is show"
+
+#: .\blog\models.py:262
+msgid "show type"
+msgstr "show type"
+
+#: .\blog\models.py:281
+msgid "content"
+msgstr "content"
+
+#: .\blog\models.py:283 .\oauth\models.py:52
+msgid "is enable"
+msgstr "is enable"
+
+#: .\blog\models.py:289
+msgid "sidebar"
+msgstr "sidebar"
+
+#: .\blog\models.py:299
+msgid "site name"
+msgstr "site name"
+
+#: .\blog\models.py:305
+msgid "site description"
+msgstr "site description"
+
+#: .\blog\models.py:311
+msgid "site seo description"
+msgstr "site seo description"
+
+#: .\blog\models.py:313
+msgid "site keywords"
+msgstr "site keywords"
+
+#: .\blog\models.py:318
+msgid "article sub length"
+msgstr "article sub length"
+
+#: .\blog\models.py:319
+msgid "sidebar article count"
+msgstr "sidebar article count"
+
+#: .\blog\models.py:320
+msgid "sidebar comment count"
+msgstr "sidebar comment count"
+
+#: .\blog\models.py:321
+msgid "article comment count"
+msgstr "article comment count"
+
+#: .\blog\models.py:322
+msgid "show adsense"
+msgstr "show adsense"
+
+#: .\blog\models.py:324
+msgid "adsense code"
+msgstr "adsense code"
+
+#: .\blog\models.py:325
+msgid "open site comment"
+msgstr "open site comment"
+
+#: .\blog\models.py:352
+msgid "Website configuration"
+msgstr "Website configuration"
+
+#: .\blog\models.py:360
+msgid "There can only be one configuration"
+msgstr "There can only be one configuration"
+
+#: .\blog\views.py:348
+msgid ""
+"Sorry, the page you requested is not found, please click the home page to "
+"see other?"
+msgstr ""
+"Sorry, the page you requested is not found, please click the home page to "
+"see other?"
+
+#: .\blog\views.py:356
+msgid "Sorry, the server is busy, please click the home page to see other?"
+msgstr "Sorry, the server is busy, please click the home page to see other?"
+
+#: .\blog\views.py:369
+msgid "Sorry, you do not have permission to access this page?"
+msgstr "Sorry, you do not have permission to access this page?"
+
+#: .\comments\admin.py:15
+msgid "Disable comments"
+msgstr "Disable comments"
+
+#: .\comments\admin.py:16
+msgid "Enable comments"
+msgstr "Enable comments"
+
+#: .\comments\admin.py:46
+msgid "User"
+msgstr "User"
+
+#: .\comments\models.py:25
+msgid "parent comment"
+msgstr "parent comment"
+
+#: .\comments\models.py:29
+msgid "enable"
+msgstr "enable"
+
+#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
+msgid "comment"
+msgstr "comment"
+
+#: .\comments\utils.py:13
+msgid "Thanks for your comment"
+msgstr "Thanks for your comment"
+
+#: .\comments\utils.py:15
+#, python-format
+msgid ""
+"Thank you very much for your comments on this site
\n"
+" You can visit %(article_title)s \n"
+" to review your comments,\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s"
+msgstr ""
+"Thank you very much for your comments on this site
\n"
+" You can visit %(article_title)s \n"
+" to review your comments,\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s"
+
+#: .\comments\utils.py:26
+#, python-format
+msgid ""
+"Your comment on "
+"%(article_title)s has \n"
+" received a reply. %(comment_body)s\n"
+" \n"
+" go check it out!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s\n"
+" "
+msgstr ""
+"Your comment on "
+"%(article_title)s has \n"
+" received a reply. %(comment_body)s\n"
+" \n"
+" go check it out!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s\n"
+" "
+
+#: .\djangoblog\logentryadmin.py:63
+msgid "object"
+msgstr "object"
+
+#: .\djangoblog\settings.py:140
+msgid "English"
+msgstr "English"
+
+#: .\djangoblog\settings.py:141
+msgid "Simplified Chinese"
+msgstr "Simplified Chinese"
+
+#: .\djangoblog\settings.py:142
+msgid "Traditional Chinese"
+msgstr "Traditional Chinese"
+
+#: .\oauth\models.py:30
+msgid "oauth user"
+msgstr "oauth user"
+
+#: .\oauth\models.py:37
+msgid "weibo"
+msgstr "weibo"
+
+#: .\oauth\models.py:38
+msgid "google"
+msgstr "google"
+
+#: .\oauth\models.py:48
+msgid "callback url"
+msgstr "callback url"
+
+#: .\oauth\models.py:59
+msgid "already exists"
+msgstr "already exists"
+
+#: .\oauth\views.py:154
+#, python-format
+msgid ""
+"\n"
+" Congratulations, you have successfully bound your email address. You "
+"can use\n"
+" %(oauthuser_type)s to directly log in to this website without a "
+"password.
\n"
+" You are welcome to continue to follow this site, the address is\n"
+" %(site)s \n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link to your "
+"browser.\n"
+" %(site)s\n"
+" "
+msgstr ""
+"\n"
+" Congratulations, you have successfully bound your email address. You "
+"can use\n"
+" %(oauthuser_type)s to directly log in to this website without a "
+"password.
\n"
+" You are welcome to continue to follow this site, the address is\n"
+" %(site)s \n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link to your "
+"browser.\n"
+" %(site)s\n"
+" "
+
+#: .\oauth\views.py:165
+msgid "Congratulations on your successful binding!"
+msgstr "Congratulations on your successful binding!"
+
+#: .\oauth\views.py:217
+#, python-format
+msgid ""
+"\n"
+" Please click the link below to bind your email
\n"
+"\n"
+" %(url)s \n"
+"\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link "
+"to your browser.\n"
+" \n"
+" %(url)s\n"
+" "
+msgstr ""
+"\n"
+" Please click the link below to bind your email
\n"
+"\n"
+" %(url)s \n"
+"\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link "
+"to your browser.\n"
+" \n"
+" %(url)s\n"
+" "
+
+#: .\oauth\views.py:228 .\oauth\views.py:240
+msgid "Bind your email"
+msgstr "Bind your email"
+
+#: .\oauth\views.py:242
+msgid ""
+"Congratulations, the binding is just one step away. Please log in to your "
+"email to check the email to complete the binding. Thank you."
+msgstr ""
+"Congratulations, the binding is just one step away. Please log in to your "
+"email to check the email to complete the binding. Thank you."
+
+#: .\oauth\views.py:245
+msgid "Binding successful"
+msgstr "Binding successful"
+
+#: .\oauth\views.py:247
+#, python-format
+msgid ""
+"Congratulations, you have successfully bound your email address. You can use "
+"%(oauthuser_type)s to directly log in to this website without a password. "
+"You are welcome to continue to follow this site."
+msgstr ""
+"Congratulations, you have successfully bound your email address. You can use "
+"%(oauthuser_type)s to directly log in to this website without a password. "
+"You are welcome to continue to follow this site."
+
+#: .\templates\account\forget_password.html:7
+msgid "forget the password"
+msgstr "forget the password"
+
+#: .\templates\account\forget_password.html:18
+msgid "get verification code"
+msgstr "get verification code"
+
+#: .\templates\account\forget_password.html:19
+msgid "submit"
+msgstr "submit"
+
+#: .\templates\account\login.html:36
+msgid "Create Account"
+msgstr "Create Account"
+
+#: .\templates\account\login.html:42
+#, fuzzy
+#| msgid "forget the password"
+msgid "Forget Password"
+msgstr "forget the password"
+
+#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
+msgid "login"
+msgstr "login"
+
+#: .\templates\account\result.html:22
+msgid "back to the homepage"
+msgstr "back to the homepage"
+
+#: .\templates\blog\article_archives.html:7
+#: .\templates\blog\article_archives.html:24
+msgid "article archive"
+msgstr "article archive"
+
+#: .\templates\blog\article_archives.html:32
+msgid "year"
+msgstr "year"
+
+#: .\templates\blog\article_archives.html:36
+msgid "month"
+msgstr "month"
+
+#: .\templates\blog\tags\article_info.html:12
+msgid "pin to top"
+msgstr "pin to top"
+
+#: .\templates\blog\tags\article_info.html:28
+msgid "comments"
+msgstr "comments"
+
+#: .\templates\blog\tags\article_info.html:58
+msgid "toc"
+msgstr "toc"
+
+#: .\templates\blog\tags\article_meta_info.html:6
+msgid "posted in"
+msgstr "posted in"
+
+#: .\templates\blog\tags\article_meta_info.html:14
+msgid "and tagged"
+msgstr "and tagged"
+
+#: .\templates\blog\tags\article_meta_info.html:25
+msgid "by "
+msgstr "by"
+
+#: .\templates\blog\tags\article_meta_info.html:29
+#, python-format
+msgid ""
+"\n"
+" title=\"View all articles published by "
+"%(article.author.username)s\"\n"
+" "
+msgstr ""
+"\n"
+" title=\"View all articles published by "
+"%(article.author.username)s\"\n"
+" "
+
+#: .\templates\blog\tags\article_meta_info.html:44
+msgid "on"
+msgstr "on"
+
+#: .\templates\blog\tags\article_meta_info.html:54
+msgid "edit"
+msgstr "edit"
+
+#: .\templates\blog\tags\article_pagination.html:4
+msgid "article navigation"
+msgstr "article navigation"
+
+#: .\templates\blog\tags\article_pagination.html:9
+msgid "earlier articles"
+msgstr "earlier articles"
+
+#: .\templates\blog\tags\article_pagination.html:12
+msgid "newer articles"
+msgstr "newer articles"
+
+#: .\templates\blog\tags\article_tag_list.html:5
+msgid "tags"
+msgstr "tags"
+
+#: .\templates\blog\tags\sidebar.html:7
+msgid "search"
+msgstr "search"
+
+#: .\templates\blog\tags\sidebar.html:50
+msgid "recent comments"
+msgstr "recent comments"
+
+#: .\templates\blog\tags\sidebar.html:57
+msgid "published on"
+msgstr "published on"
+
+#: .\templates\blog\tags\sidebar.html:65
+msgid "recent articles"
+msgstr "recent articles"
+
+#: .\templates\blog\tags\sidebar.html:77
+msgid "bookmark"
+msgstr "bookmark"
+
+#: .\templates\blog\tags\sidebar.html:96
+msgid "Tag Cloud"
+msgstr "Tag Cloud"
+
+#: .\templates\blog\tags\sidebar.html:107
+msgid "Welcome to star or fork the source code of this site"
+msgstr "Welcome to star or fork the source code of this site"
+
+#: .\templates\blog\tags\sidebar.html:118
+msgid "Function"
+msgstr "Function"
+
+#: .\templates\blog\tags\sidebar.html:120
+msgid "management site"
+msgstr "management site"
+
+#: .\templates\blog\tags\sidebar.html:122
+msgid "logout"
+msgstr "logout"
+
+#: .\templates\blog\tags\sidebar.html:129
+msgid "Track record"
+msgstr "Track record"
+
+#: .\templates\blog\tags\sidebar.html:135
+msgid "Click me to return to the top"
+msgstr "Click me to return to the top"
+
+#: .\templates\oauth\oauth_applications.html:5
+#| msgid "login"
+msgid "quick login"
+msgstr "quick login"
+
+#: .\templates\share_layout\nav.html:26
+msgid "Article archive"
+msgstr "Article archive"
diff --git a/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo b/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a2d36e9
Binary files /dev/null and b/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po b/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po
new file mode 100644
index 0000000..200b7e6
--- /dev/null
+++ b/src/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po
@@ -0,0 +1,667 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-09-13 16:02+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: .\accounts\admin.py:12
+msgid "password"
+msgstr "密码"
+
+#: .\accounts\admin.py:13
+msgid "Enter password again"
+msgstr "再次输入密码"
+
+#: .\accounts\admin.py:24 .\accounts\forms.py:89
+msgid "passwords do not match"
+msgstr "密码不匹配"
+
+#: .\accounts\forms.py:36
+msgid "email already exists"
+msgstr "邮箱已存在"
+
+#: .\accounts\forms.py:46 .\accounts\forms.py:50
+msgid "New password"
+msgstr "新密码"
+
+#: .\accounts\forms.py:60
+msgid "Confirm password"
+msgstr "确认密码"
+
+#: .\accounts\forms.py:70 .\accounts\forms.py:116
+msgid "Email"
+msgstr "邮箱"
+
+#: .\accounts\forms.py:76 .\accounts\forms.py:80
+msgid "Code"
+msgstr "验证码"
+
+#: .\accounts\forms.py:100 .\accounts\tests.py:194
+msgid "email does not exist"
+msgstr "邮箱不存在"
+
+#: .\accounts\models.py:12 .\oauth\models.py:17
+msgid "nick name"
+msgstr "昵称"
+
+#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
+#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
+#: .\oauth\models.py:53
+msgid "creation time"
+msgstr "创建时间"
+
+#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
+#: .\oauth\models.py:54
+msgid "last modify time"
+msgstr "最后修改时间"
+
+#: .\accounts\models.py:15
+msgid "create source"
+msgstr "来源"
+
+#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
+msgid "user"
+msgstr "用户"
+
+#: .\accounts\tests.py:216 .\accounts\utils.py:39
+msgid "Verification code error"
+msgstr "验证码错误"
+
+#: .\accounts\utils.py:13
+msgid "Verify Email"
+msgstr "验证邮箱"
+
+#: .\accounts\utils.py:21
+#, python-format
+msgid ""
+"You are resetting the password, the verification code is:%(code)s, valid "
+"within 5 minutes, please keep it properly"
+msgstr "您正在重置密码,验证码为:%(code)s,5分钟内有效 请妥善保管."
+
+#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
+#: .\oauth\models.py:12
+msgid "author"
+msgstr "作者"
+
+#: .\blog\admin.py:53
+msgid "Publish selected articles"
+msgstr "发布选中的文章"
+
+#: .\blog\admin.py:54
+msgid "Draft selected articles"
+msgstr "选中文章设为草稿"
+
+#: .\blog\admin.py:55
+msgid "Close article comments"
+msgstr "关闭文章评论"
+
+#: .\blog\admin.py:56
+msgid "Open article comments"
+msgstr "打开文章评论"
+
+#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
+#: .\templates\blog\tags\sidebar.html:40
+msgid "category"
+msgstr "分类目录"
+
+#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
+msgid "index"
+msgstr "首页"
+
+#: .\blog\models.py:21
+msgid "list"
+msgstr "列表"
+
+#: .\blog\models.py:22
+msgid "post"
+msgstr "文章"
+
+#: .\blog\models.py:23
+msgid "all"
+msgstr "所有"
+
+#: .\blog\models.py:24
+msgid "slide"
+msgstr "侧边栏"
+
+#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
+msgid "modify time"
+msgstr "修改时间"
+
+#: .\blog\models.py:63
+msgid "Draft"
+msgstr "草稿"
+
+#: .\blog\models.py:64
+msgid "Published"
+msgstr "发布"
+
+#: .\blog\models.py:67
+msgid "Open"
+msgstr "打开"
+
+#: .\blog\models.py:68
+msgid "Close"
+msgstr "关闭"
+
+#: .\blog\models.py:71 .\comments\admin.py:47
+msgid "Article"
+msgstr "文章"
+
+#: .\blog\models.py:72
+msgid "Page"
+msgstr "页面"
+
+#: .\blog\models.py:74 .\blog\models.py:280
+msgid "title"
+msgstr "标题"
+
+#: .\blog\models.py:75
+msgid "body"
+msgstr "内容"
+
+#: .\blog\models.py:77
+msgid "publish time"
+msgstr "发布时间"
+
+#: .\blog\models.py:79
+msgid "status"
+msgstr "状态"
+
+#: .\blog\models.py:84
+msgid "comment status"
+msgstr "评论状态"
+
+#: .\blog\models.py:88 .\oauth\models.py:43
+msgid "type"
+msgstr "类型"
+
+#: .\blog\models.py:89
+msgid "views"
+msgstr "阅读量"
+
+#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
+msgid "order"
+msgstr "排序"
+
+#: .\blog\models.py:98
+msgid "show toc"
+msgstr "显示目录"
+
+#: .\blog\models.py:105 .\blog\models.py:249
+msgid "tag"
+msgstr "标签"
+
+#: .\blog\models.py:115 .\comments\models.py:21
+msgid "article"
+msgstr "文章"
+
+#: .\blog\models.py:171
+msgid "category name"
+msgstr "分类名"
+
+#: .\blog\models.py:174
+msgid "parent category"
+msgstr "上级分类"
+
+#: .\blog\models.py:234
+msgid "tag name"
+msgstr "标签名"
+
+#: .\blog\models.py:256
+msgid "link name"
+msgstr "链接名"
+
+#: .\blog\models.py:257 .\blog\models.py:271
+msgid "link"
+msgstr "链接"
+
+#: .\blog\models.py:260
+msgid "is show"
+msgstr "是否显示"
+
+#: .\blog\models.py:262
+msgid "show type"
+msgstr "显示类型"
+
+#: .\blog\models.py:281
+msgid "content"
+msgstr "内容"
+
+#: .\blog\models.py:283 .\oauth\models.py:52
+msgid "is enable"
+msgstr "是否启用"
+
+#: .\blog\models.py:289
+msgid "sidebar"
+msgstr "侧边栏"
+
+#: .\blog\models.py:299
+msgid "site name"
+msgstr "站点名称"
+
+#: .\blog\models.py:305
+msgid "site description"
+msgstr "站点描述"
+
+#: .\blog\models.py:311
+msgid "site seo description"
+msgstr "站点SEO描述"
+
+#: .\blog\models.py:313
+msgid "site keywords"
+msgstr "关键字"
+
+#: .\blog\models.py:318
+msgid "article sub length"
+msgstr "文章摘要长度"
+
+#: .\blog\models.py:319
+msgid "sidebar article count"
+msgstr "侧边栏文章数目"
+
+#: .\blog\models.py:320
+msgid "sidebar comment count"
+msgstr "侧边栏评论数目"
+
+#: .\blog\models.py:321
+msgid "article comment count"
+msgstr "文章页面默认显示评论数目"
+
+#: .\blog\models.py:322
+msgid "show adsense"
+msgstr "是否显示广告"
+
+#: .\blog\models.py:324
+msgid "adsense code"
+msgstr "广告内容"
+
+#: .\blog\models.py:325
+msgid "open site comment"
+msgstr "公共头部"
+
+#: .\blog\models.py:352
+msgid "Website configuration"
+msgstr "网站配置"
+
+#: .\blog\models.py:360
+msgid "There can only be one configuration"
+msgstr "只能有一个配置"
+
+#: .\blog\views.py:348
+msgid ""
+"Sorry, the page you requested is not found, please click the home page to "
+"see other?"
+msgstr "抱歉,你所访问的页面找不到,请点击首页看看别的?"
+
+#: .\blog\views.py:356
+msgid "Sorry, the server is busy, please click the home page to see other?"
+msgstr "抱歉,服务出错了,请点击首页看看别的?"
+
+#: .\blog\views.py:369
+msgid "Sorry, you do not have permission to access this page?"
+msgstr "抱歉,你没用权限访问此页面。"
+
+#: .\comments\admin.py:15
+msgid "Disable comments"
+msgstr "禁用评论"
+
+#: .\comments\admin.py:16
+msgid "Enable comments"
+msgstr "启用评论"
+
+#: .\comments\admin.py:46
+msgid "User"
+msgstr "用户"
+
+#: .\comments\models.py:25
+msgid "parent comment"
+msgstr "上级评论"
+
+#: .\comments\models.py:29
+msgid "enable"
+msgstr "启用"
+
+#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
+msgid "comment"
+msgstr "评论"
+
+#: .\comments\utils.py:13
+msgid "Thanks for your comment"
+msgstr "感谢你的评论"
+
+#: .\comments\utils.py:15
+#, python-format
+msgid ""
+"Thank you very much for your comments on this site
\n"
+" You can visit %(article_title)s \n"
+" to review your comments,\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s"
+msgstr ""
+"非常感谢您对此网站的评论
\n"
+" 您可以访问%(article_title)s \n"
+"查看您的评论,\n"
+"再次感谢您!\n"
+" \n"
+" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n"
+"%(article_url)s"
+
+#: .\comments\utils.py:26
+#, python-format
+msgid ""
+"Your comment on "
+"%(article_title)s has \n"
+" received a reply. %(comment_body)s\n"
+" \n"
+" go check it out!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s\n"
+" "
+msgstr ""
+"您对 %(article_title)s "
+"的评论有\n"
+" 收到回复。 %(comment_body)s\n"
+" \n"
+"快去看看吧!\n"
+" \n"
+" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n"
+" %(article_url)s\n"
+" "
+
+#: .\djangoblog\logentryadmin.py:63
+msgid "object"
+msgstr "对象"
+
+#: .\djangoblog\settings.py:140
+msgid "English"
+msgstr "英文"
+
+#: .\djangoblog\settings.py:141
+msgid "Simplified Chinese"
+msgstr "简体中文"
+
+#: .\djangoblog\settings.py:142
+msgid "Traditional Chinese"
+msgstr "繁体中文"
+
+#: .\oauth\models.py:30
+msgid "oauth user"
+msgstr "第三方用户"
+
+#: .\oauth\models.py:37
+msgid "weibo"
+msgstr "微博"
+
+#: .\oauth\models.py:38
+msgid "google"
+msgstr "谷歌"
+
+#: .\oauth\models.py:48
+msgid "callback url"
+msgstr "回调地址"
+
+#: .\oauth\models.py:59
+msgid "already exists"
+msgstr "已经存在"
+
+#: .\oauth\views.py:154
+#, python-format
+msgid ""
+"\n"
+" Congratulations, you have successfully bound your email address. You "
+"can use\n"
+" %(oauthuser_type)s to directly log in to this website without a "
+"password.
\n"
+" You are welcome to continue to follow this site, the address is\n"
+" %(site)s \n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link to your "
+"browser.\n"
+" %(site)s\n"
+" "
+msgstr ""
+"\n"
+" 恭喜你已经绑定成功 你可以使用\n"
+" %(oauthuser_type)s 来免密登录本站
\n"
+" 欢迎继续关注本站, 地址是\n"
+" %(site)s \n"
+" 再次感谢你\n"
+" \n"
+" 如果上面链接无法打开,请复制此链接到你的浏览器 \n"
+" %(site)s\n"
+" "
+
+#: .\oauth\views.py:165
+msgid "Congratulations on your successful binding!"
+msgstr "恭喜你绑定成功"
+
+#: .\oauth\views.py:217
+#, python-format
+msgid ""
+"\n"
+" Please click the link below to bind your email
\n"
+"\n"
+" %(url)s \n"
+"\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link "
+"to your browser.\n"
+" \n"
+" %(url)s\n"
+" "
+msgstr ""
+"\n"
+" 请点击下面的链接绑定您的邮箱
\n"
+"\n"
+" %(url)s \n"
+"\n"
+"再次感谢您!\n"
+" \n"
+"如果上面的链接打不开,请复制此链接到您的浏览器。\n"
+"%(url)s\n"
+" "
+
+#: .\oauth\views.py:228 .\oauth\views.py:240
+msgid "Bind your email"
+msgstr "绑定邮箱"
+
+#: .\oauth\views.py:242
+msgid ""
+"Congratulations, the binding is just one step away. Please log in to your "
+"email to check the email to complete the binding. Thank you."
+msgstr "恭喜您,还差一步就绑定成功了,请登录您的邮箱查看邮件完成绑定,谢谢。"
+
+#: .\oauth\views.py:245
+msgid "Binding successful"
+msgstr "绑定成功"
+
+#: .\oauth\views.py:247
+#, python-format
+msgid ""
+"Congratulations, you have successfully bound your email address. You can use "
+"%(oauthuser_type)s to directly log in to this website without a password. "
+"You are welcome to continue to follow this site."
+msgstr ""
+"恭喜您绑定成功,您以后可以使用%(oauthuser_type)s来直接免密码登录本站啦,感谢"
+"您对本站对关注。"
+
+#: .\templates\account\forget_password.html:7
+msgid "forget the password"
+msgstr "忘记密码"
+
+#: .\templates\account\forget_password.html:18
+msgid "get verification code"
+msgstr "获取验证码"
+
+#: .\templates\account\forget_password.html:19
+msgid "submit"
+msgstr "提交"
+
+#: .\templates\account\login.html:36
+msgid "Create Account"
+msgstr "创建账号"
+
+#: .\templates\account\login.html:42
+#| msgid "forget the password"
+msgid "Forget Password"
+msgstr "忘记密码"
+
+#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
+msgid "login"
+msgstr "登录"
+
+#: .\templates\account\result.html:22
+msgid "back to the homepage"
+msgstr "返回首页吧"
+
+#: .\templates\blog\article_archives.html:7
+#: .\templates\blog\article_archives.html:24
+msgid "article archive"
+msgstr "文章归档"
+
+#: .\templates\blog\article_archives.html:32
+msgid "year"
+msgstr "年"
+
+#: .\templates\blog\article_archives.html:36
+msgid "month"
+msgstr "月"
+
+#: .\templates\blog\tags\article_info.html:12
+msgid "pin to top"
+msgstr "置顶"
+
+#: .\templates\blog\tags\article_info.html:28
+msgid "comments"
+msgstr "评论"
+
+#: .\templates\blog\tags\article_info.html:58
+msgid "toc"
+msgstr "目录"
+
+#: .\templates\blog\tags\article_meta_info.html:6
+msgid "posted in"
+msgstr "发布于"
+
+#: .\templates\blog\tags\article_meta_info.html:14
+msgid "and tagged"
+msgstr "并标记为"
+
+#: .\templates\blog\tags\article_meta_info.html:25
+msgid "by "
+msgstr "由"
+
+#: .\templates\blog\tags\article_meta_info.html:29
+#, python-format
+msgid ""
+"\n"
+" title=\"View all articles published by "
+"%(article.author.username)s\"\n"
+" "
+msgstr ""
+"\n"
+" title=\"查看所有由 %(article.author.username)s\"发布的文章\n"
+" "
+
+#: .\templates\blog\tags\article_meta_info.html:44
+msgid "on"
+msgstr "在"
+
+#: .\templates\blog\tags\article_meta_info.html:54
+msgid "edit"
+msgstr "编辑"
+
+#: .\templates\blog\tags\article_pagination.html:4
+msgid "article navigation"
+msgstr "文章导航"
+
+#: .\templates\blog\tags\article_pagination.html:9
+msgid "earlier articles"
+msgstr "早期文章"
+
+#: .\templates\blog\tags\article_pagination.html:12
+msgid "newer articles"
+msgstr "较新文章"
+
+#: .\templates\blog\tags\article_tag_list.html:5
+msgid "tags"
+msgstr "标签"
+
+#: .\templates\blog\tags\sidebar.html:7
+msgid "search"
+msgstr "搜索"
+
+#: .\templates\blog\tags\sidebar.html:50
+msgid "recent comments"
+msgstr "近期评论"
+
+#: .\templates\blog\tags\sidebar.html:57
+msgid "published on"
+msgstr "发表于"
+
+#: .\templates\blog\tags\sidebar.html:65
+msgid "recent articles"
+msgstr "近期文章"
+
+#: .\templates\blog\tags\sidebar.html:77
+msgid "bookmark"
+msgstr "书签"
+
+#: .\templates\blog\tags\sidebar.html:96
+msgid "Tag Cloud"
+msgstr "标签云"
+
+#: .\templates\blog\tags\sidebar.html:107
+msgid "Welcome to star or fork the source code of this site"
+msgstr "欢迎您STAR或者FORK本站源代码"
+
+#: .\templates\blog\tags\sidebar.html:118
+msgid "Function"
+msgstr "功能"
+
+#: .\templates\blog\tags\sidebar.html:120
+msgid "management site"
+msgstr "管理站点"
+
+#: .\templates\blog\tags\sidebar.html:122
+msgid "logout"
+msgstr "登出"
+
+#: .\templates\blog\tags\sidebar.html:129
+msgid "Track record"
+msgstr "运动轨迹记录"
+
+#: .\templates\blog\tags\sidebar.html:135
+msgid "Click me to return to the top"
+msgstr "点我返回顶部"
+
+#: .\templates\oauth\oauth_applications.html:5
+#| msgid "login"
+msgid "quick login"
+msgstr "快捷登录"
+
+#: .\templates\share_layout\nav.html:26
+msgid "Article archive"
+msgstr "文章归档"
diff --git a/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo b/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..fe2ea17
Binary files /dev/null and b/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo differ
diff --git a/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po b/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a2920ce
--- /dev/null
+++ b/src/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po
@@ -0,0 +1,668 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-09-13 16:02+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: .\accounts\admin.py:12
+msgid "password"
+msgstr "密碼"
+
+#: .\accounts\admin.py:13
+msgid "Enter password again"
+msgstr "再次輸入密碼"
+
+#: .\accounts\admin.py:24 .\accounts\forms.py:89
+msgid "passwords do not match"
+msgstr "密碼不匹配"
+
+#: .\accounts\forms.py:36
+msgid "email already exists"
+msgstr "郵箱已存在"
+
+#: .\accounts\forms.py:46 .\accounts\forms.py:50
+msgid "New password"
+msgstr "新密碼"
+
+#: .\accounts\forms.py:60
+msgid "Confirm password"
+msgstr "確認密碼"
+
+#: .\accounts\forms.py:70 .\accounts\forms.py:116
+msgid "Email"
+msgstr "郵箱"
+
+#: .\accounts\forms.py:76 .\accounts\forms.py:80
+msgid "Code"
+msgstr "驗證碼"
+
+#: .\accounts\forms.py:100 .\accounts\tests.py:194
+msgid "email does not exist"
+msgstr "郵箱不存在"
+
+#: .\accounts\models.py:12 .\oauth\models.py:17
+msgid "nick name"
+msgstr "昵稱"
+
+#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266
+#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23
+#: .\oauth\models.py:53
+msgid "creation time"
+msgstr "創建時間"
+
+#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24
+#: .\oauth\models.py:54
+msgid "last modify time"
+msgstr "最後修改時間"
+
+#: .\accounts\models.py:15
+msgid "create source"
+msgstr "來源"
+
+#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81
+msgid "user"
+msgstr "用戶"
+
+#: .\accounts\tests.py:216 .\accounts\utils.py:39
+msgid "Verification code error"
+msgstr "驗證碼錯誤"
+
+#: .\accounts\utils.py:13
+msgid "Verify Email"
+msgstr "驗證郵箱"
+
+#: .\accounts\utils.py:21
+#, python-format
+msgid ""
+"You are resetting the password, the verification code is:%(code)s, valid "
+"within 5 minutes, please keep it properly"
+msgstr "您正在重置密碼,驗證碼為:%(code)s,5分鐘內有效 請妥善保管."
+
+#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17
+#: .\oauth\models.py:12
+msgid "author"
+msgstr "作者"
+
+#: .\blog\admin.py:53
+msgid "Publish selected articles"
+msgstr "發布選中的文章"
+
+#: .\blog\admin.py:54
+msgid "Draft selected articles"
+msgstr "選中文章設為草稿"
+
+#: .\blog\admin.py:55
+msgid "Close article comments"
+msgstr "關閉文章評論"
+
+#: .\blog\admin.py:56
+msgid "Open article comments"
+msgstr "打開文章評論"
+
+#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183
+#: .\templates\blog\tags\sidebar.html:40
+msgid "category"
+msgstr "分類目錄"
+
+#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8
+msgid "index"
+msgstr "首頁"
+
+#: .\blog\models.py:21
+msgid "list"
+msgstr "列表"
+
+#: .\blog\models.py:22
+msgid "post"
+msgstr "文章"
+
+#: .\blog\models.py:23
+msgid "all"
+msgstr "所有"
+
+#: .\blog\models.py:24
+msgid "slide"
+msgstr "側邊欄"
+
+#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285
+msgid "modify time"
+msgstr "修改時間"
+
+#: .\blog\models.py:63
+msgid "Draft"
+msgstr "草稿"
+
+#: .\blog\models.py:64
+msgid "Published"
+msgstr "發布"
+
+#: .\blog\models.py:67
+msgid "Open"
+msgstr "打開"
+
+#: .\blog\models.py:68
+msgid "Close"
+msgstr "關閉"
+
+#: .\blog\models.py:71 .\comments\admin.py:47
+msgid "Article"
+msgstr "文章"
+
+#: .\blog\models.py:72
+msgid "Page"
+msgstr "頁面"
+
+#: .\blog\models.py:74 .\blog\models.py:280
+msgid "title"
+msgstr "標題"
+
+#: .\blog\models.py:75
+msgid "body"
+msgstr "內容"
+
+#: .\blog\models.py:77
+msgid "publish time"
+msgstr "發布時間"
+
+#: .\blog\models.py:79
+msgid "status"
+msgstr "狀態"
+
+#: .\blog\models.py:84
+msgid "comment status"
+msgstr "評論狀態"
+
+#: .\blog\models.py:88 .\oauth\models.py:43
+msgid "type"
+msgstr "類型"
+
+#: .\blog\models.py:89
+msgid "views"
+msgstr "閱讀量"
+
+#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282
+msgid "order"
+msgstr "排序"
+
+#: .\blog\models.py:98
+msgid "show toc"
+msgstr "顯示目錄"
+
+#: .\blog\models.py:105 .\blog\models.py:249
+msgid "tag"
+msgstr "標簽"
+
+#: .\blog\models.py:115 .\comments\models.py:21
+msgid "article"
+msgstr "文章"
+
+#: .\blog\models.py:171
+msgid "category name"
+msgstr "分類名"
+
+#: .\blog\models.py:174
+msgid "parent category"
+msgstr "上級分類"
+
+#: .\blog\models.py:234
+msgid "tag name"
+msgstr "標簽名"
+
+#: .\blog\models.py:256
+msgid "link name"
+msgstr "鏈接名"
+
+#: .\blog\models.py:257 .\blog\models.py:271
+msgid "link"
+msgstr "鏈接"
+
+#: .\blog\models.py:260
+msgid "is show"
+msgstr "是否顯示"
+
+#: .\blog\models.py:262
+msgid "show type"
+msgstr "顯示類型"
+
+#: .\blog\models.py:281
+msgid "content"
+msgstr "內容"
+
+#: .\blog\models.py:283 .\oauth\models.py:52
+msgid "is enable"
+msgstr "是否啟用"
+
+#: .\blog\models.py:289
+msgid "sidebar"
+msgstr "側邊欄"
+
+#: .\blog\models.py:299
+msgid "site name"
+msgstr "站點名稱"
+
+#: .\blog\models.py:305
+msgid "site description"
+msgstr "站點描述"
+
+#: .\blog\models.py:311
+msgid "site seo description"
+msgstr "站點SEO描述"
+
+#: .\blog\models.py:313
+msgid "site keywords"
+msgstr "關鍵字"
+
+#: .\blog\models.py:318
+msgid "article sub length"
+msgstr "文章摘要長度"
+
+#: .\blog\models.py:319
+msgid "sidebar article count"
+msgstr "側邊欄文章數目"
+
+#: .\blog\models.py:320
+msgid "sidebar comment count"
+msgstr "側邊欄評論數目"
+
+#: .\blog\models.py:321
+msgid "article comment count"
+msgstr "文章頁面默認顯示評論數目"
+
+#: .\blog\models.py:322
+msgid "show adsense"
+msgstr "是否顯示廣告"
+
+#: .\blog\models.py:324
+msgid "adsense code"
+msgstr "廣告內容"
+
+#: .\blog\models.py:325
+msgid "open site comment"
+msgstr "公共頭部"
+
+#: .\blog\models.py:352
+msgid "Website configuration"
+msgstr "網站配置"
+
+#: .\blog\models.py:360
+msgid "There can only be one configuration"
+msgstr "只能有一個配置"
+
+#: .\blog\views.py:348
+msgid ""
+"Sorry, the page you requested is not found, please click the home page to "
+"see other?"
+msgstr "抱歉,你所訪問的頁面找不到,請點擊首頁看看別的?"
+
+#: .\blog\views.py:356
+msgid "Sorry, the server is busy, please click the home page to see other?"
+msgstr "抱歉,服務出錯了,請點擊首頁看看別的?"
+
+#: .\blog\views.py:369
+msgid "Sorry, you do not have permission to access this page?"
+msgstr "抱歉,你沒用權限訪問此頁面。"
+
+#: .\comments\admin.py:15
+msgid "Disable comments"
+msgstr "禁用評論"
+
+#: .\comments\admin.py:16
+msgid "Enable comments"
+msgstr "啟用評論"
+
+#: .\comments\admin.py:46
+msgid "User"
+msgstr "用戶"
+
+#: .\comments\models.py:25
+msgid "parent comment"
+msgstr "上級評論"
+
+#: .\comments\models.py:29
+msgid "enable"
+msgstr "啟用"
+
+#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30
+msgid "comment"
+msgstr "評論"
+
+#: .\comments\utils.py:13
+msgid "Thanks for your comment"
+msgstr "感謝你的評論"
+
+#: .\comments\utils.py:15
+#, python-format
+msgid ""
+"Thank you very much for your comments on this site
\n"
+" You can visit %(article_title)s \n"
+" to review your comments,\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s"
+msgstr ""
+"非常感謝您對此網站的評論
\n"
+" 您可以訪問%(article_title)s \n"
+"查看您的評論,\n"
+"再次感謝您!\n"
+" \n"
+" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n"
+"%(article_url)s"
+
+#: .\comments\utils.py:26
+#, python-format
+msgid ""
+"Your comment on "
+"%(article_title)s has \n"
+" received a reply. %(comment_body)s\n"
+" \n"
+" go check it out!\n"
+" \n"
+" If the link above cannot be opened, please copy this "
+"link to your browser.\n"
+" %(article_url)s\n"
+" "
+msgstr ""
+"您對 %(article_title)s "
+"的評論有\n"
+" 收到回復。 %(comment_body)s\n"
+" \n"
+"快去看看吧!\n"
+" \n"
+" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n"
+" %(article_url)s\n"
+" "
+
+#: .\djangoblog\logentryadmin.py:63
+msgid "object"
+msgstr "對象"
+
+#: .\djangoblog\settings.py:140
+msgid "English"
+msgstr "英文"
+
+#: .\djangoblog\settings.py:141
+msgid "Simplified Chinese"
+msgstr "簡體中文"
+
+#: .\djangoblog\settings.py:142
+msgid "Traditional Chinese"
+msgstr "繁體中文"
+
+#: .\oauth\models.py:30
+msgid "oauth user"
+msgstr "第三方用戶"
+
+#: .\oauth\models.py:37
+msgid "weibo"
+msgstr "微博"
+
+#: .\oauth\models.py:38
+msgid "google"
+msgstr "谷歌"
+
+#: .\oauth\models.py:48
+msgid "callback url"
+msgstr "回調地址"
+
+#: .\oauth\models.py:59
+msgid "already exists"
+msgstr "已經存在"
+
+#: .\oauth\views.py:154
+#, python-format
+msgid ""
+"\n"
+" Congratulations, you have successfully bound your email address. You "
+"can use\n"
+" %(oauthuser_type)s to directly log in to this website without a "
+"password.
\n"
+" You are welcome to continue to follow this site, the address is\n"
+" %(site)s \n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link to your "
+"browser.\n"
+" %(site)s\n"
+" "
+msgstr ""
+"\n"
+" 恭喜你已經綁定成功 你可以使用\n"
+" %(oauthuser_type)s 來免密登錄本站
\n"
+" 歡迎繼續關註本站, 地址是\n"
+" %(site)s \n"
+" 再次感謝你\n"
+" \n"
+" 如果上面鏈接無法打開,請復製此鏈接到你的瀏覽器 \n"
+" %(site)s\n"
+" "
+
+#: .\oauth\views.py:165
+msgid "Congratulations on your successful binding!"
+msgstr "恭喜你綁定成功"
+
+#: .\oauth\views.py:217
+#, python-format
+msgid ""
+"\n"
+" Please click the link below to bind your email
\n"
+"\n"
+" %(url)s \n"
+"\n"
+" Thank you again!\n"
+" \n"
+" If the link above cannot be opened, please copy this link "
+"to your browser.\n"
+" \n"
+" %(url)s\n"
+" "
+msgstr ""
+"\n"
+" 請點擊下面的鏈接綁定您的郵箱
\n"
+"\n"
+" %(url)s \n"
+"\n"
+"再次感謝您!\n"
+" \n"
+"如果上面的鏈接打不開,請復製此鏈接到您的瀏覽器。\n"
+"%(url)s\n"
+" "
+
+#: .\oauth\views.py:228 .\oauth\views.py:240
+msgid "Bind your email"
+msgstr "綁定郵箱"
+
+#: .\oauth\views.py:242
+msgid ""
+"Congratulations, the binding is just one step away. Please log in to your "
+"email to check the email to complete the binding. Thank you."
+msgstr "恭喜您,還差一步就綁定成功了,請登錄您的郵箱查看郵件完成綁定,謝謝。"
+
+#: .\oauth\views.py:245
+msgid "Binding successful"
+msgstr "綁定成功"
+
+#: .\oauth\views.py:247
+#, python-format
+msgid ""
+"Congratulations, you have successfully bound your email address. You can use "
+"%(oauthuser_type)s to directly log in to this website without a password. "
+"You are welcome to continue to follow this site."
+msgstr ""
+"恭喜您綁定成功,您以後可以使用%(oauthuser_type)s來直接免密碼登錄本站啦,感謝"
+"您對本站對關註。"
+
+#: .\templates\account\forget_password.html:7
+msgid "forget the password"
+msgstr "忘記密碼"
+
+#: .\templates\account\forget_password.html:18
+msgid "get verification code"
+msgstr "獲取驗證碼"
+
+#: .\templates\account\forget_password.html:19
+msgid "submit"
+msgstr "提交"
+
+#: .\templates\account\login.html:36
+msgid "Create Account"
+msgstr "創建賬號"
+
+#: .\templates\account\login.html:42
+#, fuzzy
+#| msgid "forget the password"
+msgid "Forget Password"
+msgstr "忘記密碼"
+
+#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126
+msgid "login"
+msgstr "登錄"
+
+#: .\templates\account\result.html:22
+msgid "back to the homepage"
+msgstr "返回首頁吧"
+
+#: .\templates\blog\article_archives.html:7
+#: .\templates\blog\article_archives.html:24
+msgid "article archive"
+msgstr "文章歸檔"
+
+#: .\templates\blog\article_archives.html:32
+msgid "year"
+msgstr "年"
+
+#: .\templates\blog\article_archives.html:36
+msgid "month"
+msgstr "月"
+
+#: .\templates\blog\tags\article_info.html:12
+msgid "pin to top"
+msgstr "置頂"
+
+#: .\templates\blog\tags\article_info.html:28
+msgid "comments"
+msgstr "評論"
+
+#: .\templates\blog\tags\article_info.html:58
+msgid "toc"
+msgstr "目錄"
+
+#: .\templates\blog\tags\article_meta_info.html:6
+msgid "posted in"
+msgstr "發布於"
+
+#: .\templates\blog\tags\article_meta_info.html:14
+msgid "and tagged"
+msgstr "並標記為"
+
+#: .\templates\blog\tags\article_meta_info.html:25
+msgid "by "
+msgstr "由"
+
+#: .\templates\blog\tags\article_meta_info.html:29
+#, python-format
+msgid ""
+"\n"
+" title=\"View all articles published by "
+"%(article.author.username)s\"\n"
+" "
+msgstr ""
+"\n"
+" title=\"查看所有由 %(article.author.username)s\"發布的文章\n"
+" "
+
+#: .\templates\blog\tags\article_meta_info.html:44
+msgid "on"
+msgstr "在"
+
+#: .\templates\blog\tags\article_meta_info.html:54
+msgid "edit"
+msgstr "編輯"
+
+#: .\templates\blog\tags\article_pagination.html:4
+msgid "article navigation"
+msgstr "文章導航"
+
+#: .\templates\blog\tags\article_pagination.html:9
+msgid "earlier articles"
+msgstr "早期文章"
+
+#: .\templates\blog\tags\article_pagination.html:12
+msgid "newer articles"
+msgstr "較新文章"
+
+#: .\templates\blog\tags\article_tag_list.html:5
+msgid "tags"
+msgstr "標簽"
+
+#: .\templates\blog\tags\sidebar.html:7
+msgid "search"
+msgstr "搜索"
+
+#: .\templates\blog\tags\sidebar.html:50
+msgid "recent comments"
+msgstr "近期評論"
+
+#: .\templates\blog\tags\sidebar.html:57
+msgid "published on"
+msgstr "發表於"
+
+#: .\templates\blog\tags\sidebar.html:65
+msgid "recent articles"
+msgstr "近期文章"
+
+#: .\templates\blog\tags\sidebar.html:77
+msgid "bookmark"
+msgstr "書簽"
+
+#: .\templates\blog\tags\sidebar.html:96
+msgid "Tag Cloud"
+msgstr "標簽雲"
+
+#: .\templates\blog\tags\sidebar.html:107
+msgid "Welcome to star or fork the source code of this site"
+msgstr "歡迎您STAR或者FORK本站源代碼"
+
+#: .\templates\blog\tags\sidebar.html:118
+msgid "Function"
+msgstr "功能"
+
+#: .\templates\blog\tags\sidebar.html:120
+msgid "management site"
+msgstr "管理站點"
+
+#: .\templates\blog\tags\sidebar.html:122
+msgid "logout"
+msgstr "登出"
+
+#: .\templates\blog\tags\sidebar.html:129
+msgid "Track record"
+msgstr "運動軌跡記錄"
+
+#: .\templates\blog\tags\sidebar.html:135
+msgid "Click me to return to the top"
+msgstr "點我返回頂部"
+
+#: .\templates\oauth\oauth_applications.html:5
+#| msgid "login"
+msgid "quick login"
+msgstr "快捷登錄"
+
+#: .\templates\share_layout\nav.html:26
+msgid "Article archive"
+msgstr "文章歸檔"
diff --git a/src/DjangoBlog-master/manage.py b/src/DjangoBlog-master/manage.py
new file mode 100644
index 0000000..919ba74
--- /dev/null
+++ b/src/DjangoBlog-master/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/src/DjangoBlog-master/oauth/__init__.py b/src/DjangoBlog-master/oauth/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/oauth/admin.py b/src/DjangoBlog-master/oauth/admin.py
new file mode 100644
index 0000000..57eab5f
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/admin.py
@@ -0,0 +1,54 @@
+import logging
+
+from django.contrib import admin
+# Register your models here.
+from django.urls import reverse
+from django.utils.html import format_html
+
+logger = logging.getLogger(__name__)
+
+
+class OAuthUserAdmin(admin.ModelAdmin):
+ search_fields = ('nickname', 'email')
+ list_per_page = 20
+ list_display = (
+ 'id',
+ 'nickname',
+ 'link_to_usermodel',
+ 'show_user_image',
+ 'type',
+ 'email',
+ )
+ list_display_links = ('id', 'nickname')
+ list_filter = ('author', 'type',)
+ readonly_fields = []
+
+ def get_readonly_fields(self, request, obj=None):
+ return list(self.readonly_fields) + \
+ [field.name for field in obj._meta.fields] + \
+ [field.name for field in obj._meta.many_to_many]
+
+ def has_add_permission(self, request):
+ return False
+
+ def link_to_usermodel(self, obj):
+ if obj.author:
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ return format_html(
+ u'%s ' %
+ (link, obj.author.nickname if obj.author.nickname else obj.author.email))
+
+ def show_user_image(self, obj):
+ img = obj.picture
+ return format_html(
+ u' ' %
+ (img))
+
+ link_to_usermodel.short_description = '用户'
+ show_user_image.short_description = '用户头像'
+
+
+class OAuthConfigAdmin(admin.ModelAdmin):
+ list_display = ('type', 'appkey', 'appsecret', 'is_enable')
+ list_filter = ('type',)
diff --git a/src/DjangoBlog-master/oauth/apps.py b/src/DjangoBlog-master/oauth/apps.py
new file mode 100644
index 0000000..17fcea2
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class OauthConfig(AppConfig):
+ name = 'oauth'
diff --git a/src/DjangoBlog-master/oauth/forms.py b/src/DjangoBlog-master/oauth/forms.py
new file mode 100644
index 0000000..0e4ede3
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/forms.py
@@ -0,0 +1,12 @@
+from django.contrib.auth.forms import forms
+from django.forms import widgets
+
+
+class RequireEmailForm(forms.Form):
+ email = forms.EmailField(label='电子邮箱', required=True)
+ oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(RequireEmailForm, self).__init__(*args, **kwargs)
+ self.fields['email'].widget = widgets.EmailInput(
+ attrs={'placeholder': "email", "class": "form-control"})
diff --git a/src/DjangoBlog-master/oauth/migrations/0001_initial.py b/src/DjangoBlog-master/oauth/migrations/0001_initial.py
new file mode 100644
index 0000000..3aa3e03
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/migrations/0001_initial.py
@@ -0,0 +1,57 @@
+# Generated by Django 4.1.7 on 2023-03-07 09:53
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OAuthConfig',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
+ ('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
+ ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
+ ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, 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': 'oauth配置',
+ 'verbose_name_plural': 'oauth配置',
+ 'ordering': ['-created_time'],
+ },
+ ),
+ migrations.CreateModel(
+ name='OAuthUser',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('openid', models.CharField(max_length=50)),
+ ('nickname', models.CharField(max_length=50, verbose_name='昵称')),
+ ('token', models.CharField(blank=True, max_length=150, null=True)),
+ ('picture', models.CharField(blank=True, max_length=350, null=True)),
+ ('type', models.CharField(max_length=50)),
+ ('email', models.CharField(blank=True, max_length=50, null=True)),
+ ('metadata', models.TextField(blank=True, null=True)),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
+ ],
+ options={
+ 'verbose_name': 'oauth用户',
+ 'verbose_name_plural': 'oauth用户',
+ 'ordering': ['-created_time'],
+ },
+ ),
+ ]
diff --git a/src/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/src/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
new file mode 100644
index 0000000..d5cc70e
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
@@ -0,0 +1,86 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('oauth', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='oauthconfig',
+ options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
+ ),
+ migrations.AlterModelOptions(
+ name='oauthuser',
+ options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
+ ),
+ migrations.RemoveField(
+ model_name='oauthconfig',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='oauthconfig',
+ name='last_mod_time',
+ ),
+ migrations.RemoveField(
+ model_name='oauthuser',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='oauthuser',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='oauthconfig',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='oauthconfig',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AddField(
+ model_name='oauthuser',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='oauthuser',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='callback_url',
+ field=models.CharField(default='', max_length=200, verbose_name='callback url'),
+ ),
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='is_enable',
+ field=models.BooleanField(default=True, verbose_name='is enable'),
+ ),
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='type',
+ field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
+ ),
+ migrations.AlterField(
+ model_name='oauthuser',
+ name='author',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='oauthuser',
+ name='nickname',
+ field=models.CharField(max_length=50, verbose_name='nickname'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py b/src/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py
new file mode 100644
index 0000000..6af08eb
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2024-01-26 02:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='oauthuser',
+ name='nickname',
+ field=models.CharField(max_length=50, verbose_name='nick name'),
+ ),
+ ]
diff --git a/src/DjangoBlog-master/oauth/migrations/__init__.py b/src/DjangoBlog-master/oauth/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/oauth/models.py b/src/DjangoBlog-master/oauth/models.py
new file mode 100644
index 0000000..be838ed
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/models.py
@@ -0,0 +1,67 @@
+# Create your models here.
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+
+
+class OAuthUser(models.Model):
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ openid = models.CharField(max_length=50)
+ nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
+ token = models.CharField(max_length=150, null=True, blank=True)
+ picture = models.CharField(max_length=350, blank=True, null=True)
+ type = models.CharField(blank=False, null=False, max_length=50)
+ email = models.CharField(max_length=50, null=True, blank=True)
+ metadata = models.TextField(null=True, blank=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ def __str__(self):
+ return self.nickname
+
+ class Meta:
+ verbose_name = _('oauth user')
+ verbose_name_plural = verbose_name
+ ordering = ['-creation_time']
+
+
+class OAuthConfig(models.Model):
+ TYPE = (
+ ('weibo', _('weibo')),
+ ('google', _('google')),
+ ('github', 'GitHub'),
+ ('facebook', 'FaceBook'),
+ ('qq', 'QQ'),
+ )
+ type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
+ appkey = models.CharField(max_length=200, verbose_name='AppKey')
+ appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
+ callback_url = models.CharField(
+ max_length=200,
+ verbose_name=_('callback url'),
+ blank=False,
+ default='')
+ is_enable = models.BooleanField(
+ _('is enable'), default=True, blank=False, null=False)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ def clean(self):
+ if OAuthConfig.objects.filter(
+ type=self.type).exclude(id=self.id).count():
+ raise ValidationError(_(self.type + _('already exists')))
+
+ def __str__(self):
+ return self.type
+
+ class Meta:
+ verbose_name = 'oauth配置'
+ verbose_name_plural = verbose_name
+ ordering = ['-creation_time']
diff --git a/src/DjangoBlog-master/oauth/oauthmanager.py b/src/DjangoBlog-master/oauth/oauthmanager.py
new file mode 100644
index 0000000..2e7ceef
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/oauthmanager.py
@@ -0,0 +1,504 @@
+import json
+import logging
+import os
+import urllib.parse
+from abc import ABCMeta, abstractmethod
+
+import requests
+
+from djangoblog.utils import cache_decorator
+from oauth.models import OAuthUser, OAuthConfig
+
+logger = logging.getLogger(__name__)
+
+
+class OAuthAccessTokenException(Exception):
+ '''
+ oauth授权失败异常
+ '''
+
+
+class BaseOauthManager(metaclass=ABCMeta):
+ """获取用户授权"""
+ AUTH_URL = None
+ """获取token"""
+ TOKEN_URL = None
+ """获取用户信息"""
+ API_URL = None
+ '''icon图标名'''
+ ICON_NAME = None
+
+ def __init__(self, access_token=None, openid=None):
+ self.access_token = access_token
+ self.openid = openid
+
+ @property
+ def is_access_token_set(self):
+ return self.access_token is not None
+
+ @property
+ def is_authorized(self):
+ return self.is_access_token_set and self.access_token is not None and self.openid is not None
+
+ @abstractmethod
+ def get_authorization_url(self, nexturl='/'):
+ pass
+
+ @abstractmethod
+ def get_access_token_by_code(self, code):
+ pass
+
+ @abstractmethod
+ def get_oauth_userinfo(self):
+ pass
+
+ @abstractmethod
+ def get_picture(self, metadata):
+ pass
+
+ def do_get(self, url, params, headers=None):
+ rsp = requests.get(url=url, params=params, headers=headers)
+ logger.info(rsp.text)
+ return rsp.text
+
+ def do_post(self, url, params, headers=None):
+ rsp = requests.post(url, params, headers=headers)
+ logger.info(rsp.text)
+ return rsp.text
+
+ def get_config(self):
+ value = OAuthConfig.objects.filter(type=self.ICON_NAME)
+ return value[0] if value else None
+
+
+class WBOauthManager(BaseOauthManager):
+ AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
+ TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
+ API_URL = 'https://api.weibo.com/2/users/show.json'
+ ICON_NAME = 'weibo'
+
+ def __init__(self, access_token=None, openid=None):
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ super(
+ WBOauthManager,
+ self).__init__(
+ access_token=access_token,
+ openid=openid)
+
+ def get_authorization_url(self, nexturl='/'):
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': self.callback_url + '&next_url=' + nexturl
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+
+ obj = json.loads(rsp)
+ if 'access_token' in obj:
+ self.access_token = str(obj['access_token'])
+ self.openid = str(obj['uid'])
+ return self.get_oauth_userinfo()
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ if not self.is_authorized:
+ return None
+ params = {
+ 'uid': self.openid,
+ 'access_token': self.access_token
+ }
+ rsp = self.do_get(self.API_URL, params)
+ try:
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.metadata = rsp
+ user.picture = datas['avatar_large']
+ user.nickname = datas['screen_name']
+ user.openid = datas['id']
+ user.type = 'weibo'
+ user.token = self.access_token
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('weibo oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ datas = json.loads(metadata)
+ return datas['avatar_large']
+
+
+class ProxyManagerMixin:
+ def __init__(self, *args, **kwargs):
+ if os.environ.get("HTTP_PROXY"):
+ self.proxies = {
+ "http": os.environ.get("HTTP_PROXY"),
+ "https": os.environ.get("HTTP_PROXY")
+ }
+ else:
+ self.proxies = None
+
+ def do_get(self, url, params, headers=None):
+ rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
+ logger.info(rsp.text)
+ return rsp.text
+
+ def do_post(self, url, params, headers=None):
+ rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
+ logger.info(rsp.text)
+ return rsp.text
+
+
+class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
+ AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
+ TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
+ API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
+ ICON_NAME = 'google'
+
+ def __init__(self, access_token=None, openid=None):
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ super(
+ GoogleOauthManager,
+ self).__init__(
+ access_token=access_token,
+ openid=openid)
+
+ def get_authorization_url(self, nexturl='/'):
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': self.callback_url,
+ 'scope': 'openid email',
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+
+ obj = json.loads(rsp)
+
+ if 'access_token' in obj:
+ self.access_token = str(obj['access_token'])
+ self.openid = str(obj['id_token'])
+ logger.info(self.ICON_NAME + ' oauth ' + rsp)
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ if not self.is_authorized:
+ return None
+ params = {
+ 'access_token': self.access_token
+ }
+ rsp = self.do_get(self.API_URL, params)
+ try:
+
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.metadata = rsp
+ user.picture = datas['picture']
+ user.nickname = datas['name']
+ user.openid = datas['sub']
+ user.token = self.access_token
+ user.type = 'google'
+ if datas['email']:
+ user.email = datas['email']
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('google oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ datas = json.loads(metadata)
+ return datas['picture']
+
+
+class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
+ AUTH_URL = 'https://github.com/login/oauth/authorize'
+ TOKEN_URL = 'https://github.com/login/oauth/access_token'
+ API_URL = 'https://api.github.com/user'
+ ICON_NAME = 'github'
+
+ def __init__(self, access_token=None, openid=None):
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ super(
+ GitHubOauthManager,
+ self).__init__(
+ access_token=access_token,
+ openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': f'{self.callback_url}&next_url={next_url}',
+ 'scope': 'user'
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+
+ from urllib import parse
+ r = parse.parse_qs(rsp)
+ if 'access_token' in r:
+ self.access_token = (r['access_token'][0])
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+
+ rsp = self.do_get(self.API_URL, params={}, headers={
+ "Authorization": "token " + self.access_token
+ })
+ try:
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.picture = datas['avatar_url']
+ user.nickname = datas['name']
+ user.openid = datas['id']
+ user.type = 'github'
+ user.token = self.access_token
+ user.metadata = rsp
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('github oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ datas = json.loads(metadata)
+ return datas['avatar_url']
+
+
+class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
+ AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
+ TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
+ API_URL = 'https://graph.facebook.com/me'
+ ICON_NAME = 'facebook'
+
+ def __init__(self, access_token=None, openid=None):
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ super(
+ FaceBookOauthManager,
+ self).__init__(
+ access_token=access_token,
+ openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': self.callback_url,
+ 'scope': 'email,public_profile'
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ # 'grant_type': 'authorization_code',
+ 'code': code,
+
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+
+ obj = json.loads(rsp)
+ if 'access_token' in obj:
+ token = str(obj['access_token'])
+ self.access_token = token
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ params = {
+ 'access_token': self.access_token,
+ 'fields': 'id,name,picture,email'
+ }
+ try:
+ rsp = self.do_get(self.API_URL, params)
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.nickname = datas['name']
+ user.openid = datas['id']
+ user.type = 'facebook'
+ user.token = self.access_token
+ user.metadata = rsp
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
+ user.picture = str(datas['picture']['data']['url'])
+ return user
+ except Exception as e:
+ logger.error(e)
+ return None
+
+ def get_picture(self, metadata):
+ datas = json.loads(metadata)
+ return str(datas['picture']['data']['url'])
+
+
+class QQOauthManager(BaseOauthManager):
+ AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
+ TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
+ API_URL = 'https://graph.qq.com/user/get_user_info'
+ OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
+ ICON_NAME = 'qq'
+
+ def __init__(self, access_token=None, openid=None):
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ super(
+ QQOauthManager,
+ self).__init__(
+ access_token=access_token,
+ openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ params = {
+ 'response_type': 'code',
+ 'client_id': self.client_id,
+ 'redirect_uri': self.callback_url + '&next_url=' + next_url,
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ params = {
+ 'grant_type': 'authorization_code',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_get(self.TOKEN_URL, params)
+ if rsp:
+ d = urllib.parse.parse_qs(rsp)
+ if 'access_token' in d:
+ token = d['access_token']
+ self.access_token = token[0]
+ return token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_open_id(self):
+ if self.is_access_token_set:
+ params = {
+ 'access_token': self.access_token
+ }
+ rsp = self.do_get(self.OPEN_ID_URL, params)
+ if rsp:
+ rsp = rsp.replace(
+ 'callback(', '').replace(
+ ')', '').replace(
+ ';', '')
+ obj = json.loads(rsp)
+ openid = str(obj['openid'])
+ self.openid = openid
+ return openid
+
+ def get_oauth_userinfo(self):
+ openid = self.get_open_id()
+ if openid:
+ params = {
+ 'access_token': self.access_token,
+ 'oauth_consumer_key': self.client_id,
+ 'openid': self.openid
+ }
+ rsp = self.do_get(self.API_URL, params)
+ logger.info(rsp)
+ obj = json.loads(rsp)
+ user = OAuthUser()
+ user.nickname = obj['nickname']
+ user.openid = openid
+ user.type = 'qq'
+ user.token = self.access_token
+ user.metadata = rsp
+ if 'email' in obj:
+ user.email = obj['email']
+ if 'figureurl' in obj:
+ user.picture = str(obj['figureurl'])
+ return user
+
+ def get_picture(self, metadata):
+ datas = json.loads(metadata)
+ return str(datas['figureurl'])
+
+
+@cache_decorator(expiration=100 * 60)
+def get_oauth_apps():
+ configs = OAuthConfig.objects.filter(is_enable=True).all()
+ if not configs:
+ return []
+ configtypes = [x.type for x in configs]
+ applications = BaseOauthManager.__subclasses__()
+ apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
+ return apps
+
+
+def get_manager_by_type(type):
+ applications = get_oauth_apps()
+ if applications:
+ finds = list(
+ filter(
+ lambda x: x.ICON_NAME.lower() == type.lower(),
+ applications))
+ if finds:
+ return finds[0]
+ return None
diff --git a/src/DjangoBlog-master/oauth/templatetags/__init__.py b/src/DjangoBlog-master/oauth/templatetags/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/templatetags/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/DjangoBlog-master/oauth/templatetags/oauth_tags.py b/src/DjangoBlog-master/oauth/templatetags/oauth_tags.py
new file mode 100644
index 0000000..7b687d5
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/templatetags/oauth_tags.py
@@ -0,0 +1,22 @@
+from django import template
+from django.urls import reverse
+
+from oauth.oauthmanager import get_oauth_apps
+
+register = template.Library()
+
+
+@register.inclusion_tag('oauth/oauth_applications.html')
+def load_oauth_applications(request):
+ applications = get_oauth_apps()
+ if applications:
+ baseurl = reverse('oauth:oauthlogin')
+ path = request.get_full_path()
+
+ apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format(
+ baseurl=baseurl, type=x.ICON_NAME, next=path)), applications))
+ else:
+ apps = []
+ return {
+ 'apps': apps
+ }
diff --git a/src/DjangoBlog-master/oauth/tests.py b/src/DjangoBlog-master/oauth/tests.py
new file mode 100644
index 0000000..bb23b9b
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/tests.py
@@ -0,0 +1,249 @@
+import json
+from unittest.mock import patch
+
+from django.conf import settings
+from django.contrib import auth
+from django.test import Client, RequestFactory, TestCase
+from django.urls import reverse
+
+from djangoblog.utils import get_sha256
+from oauth.models import OAuthConfig
+from oauth.oauthmanager import BaseOauthManager
+
+
+# Create your tests here.
+class OAuthConfigTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_oauth_login_test(self):
+ c = OAuthConfig()
+ c.type = 'weibo'
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("api.weibo.com" in response.url)
+
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/')
+
+
+class OauthLoginTest(TestCase):
+ def setUp(self) -> None:
+ self.client = Client()
+ self.factory = RequestFactory()
+ self.apps = self.init_apps()
+
+ def init_apps(self):
+ applications = [p() for p in BaseOauthManager.__subclasses__()]
+ for application in applications:
+ c = OAuthConfig()
+ c.type = application.ICON_NAME.lower()
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+ return applications
+
+ def get_app_by_type(self, type):
+ for app in self.apps:
+ if app.ICON_NAME.lower() == type:
+ return app
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_login(self, mock_do_get, mock_do_post):
+ weibo_app = self.get_app_by_type('weibo')
+ assert weibo_app
+ url = weibo_app.get_authorization_url()
+ mock_do_post.return_value = json.dumps({"access_token": "access_token",
+ "uid": "uid"
+ })
+ mock_do_get.return_value = json.dumps({
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name",
+ "id": "id",
+ "email": "email",
+ })
+ userinfo = weibo_app.get_access_token_by_code('code')
+ self.assertEqual(userinfo.token, 'access_token')
+ self.assertEqual(userinfo.openid, 'id')
+
+ @patch("oauth.oauthmanager.GoogleOauthManager.do_post")
+ @patch("oauth.oauthmanager.GoogleOauthManager.do_get")
+ def test_google_login(self, mock_do_get, mock_do_post):
+ google_app = self.get_app_by_type('google')
+ assert google_app
+ url = google_app.get_authorization_url()
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ "id_token": "id_token",
+ })
+ mock_do_get.return_value = json.dumps({
+ "picture": "picture",
+ "name": "name",
+ "sub": "sub",
+ "email": "email",
+ })
+ token = google_app.get_access_token_by_code('code')
+ userinfo = google_app.get_oauth_userinfo()
+ self.assertEqual(userinfo.token, 'access_token')
+ self.assertEqual(userinfo.openid, 'sub')
+
+ @patch("oauth.oauthmanager.GitHubOauthManager.do_post")
+ @patch("oauth.oauthmanager.GitHubOauthManager.do_get")
+ def test_github_login(self, mock_do_get, mock_do_post):
+ github_app = self.get_app_by_type('github')
+ assert github_app
+ url = github_app.get_authorization_url()
+ self.assertTrue("github.com" in url)
+ self.assertTrue("client_id" in url)
+ mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
+ mock_do_get.return_value = json.dumps({
+ "avatar_url": "avatar_url",
+ "name": "name",
+ "id": "id",
+ "email": "email",
+ })
+ token = github_app.get_access_token_by_code('code')
+ userinfo = github_app.get_oauth_userinfo()
+ self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
+ self.assertEqual(userinfo.openid, 'id')
+
+ @patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
+ @patch("oauth.oauthmanager.FaceBookOauthManager.do_get")
+ def test_facebook_login(self, mock_do_get, mock_do_post):
+ facebook_app = self.get_app_by_type('facebook')
+ assert facebook_app
+ url = facebook_app.get_authorization_url()
+ self.assertTrue("facebook.com" in url)
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ })
+ mock_do_get.return_value = json.dumps({
+ "name": "name",
+ "id": "id",
+ "email": "email",
+ "picture": {
+ "data": {
+ "url": "url"
+ }
+ }
+ })
+ token = facebook_app.get_access_token_by_code('code')
+ userinfo = facebook_app.get_oauth_userinfo()
+ self.assertEqual(userinfo.token, 'access_token')
+
+ @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
+ 'access_token=access_token&expires_in=3600',
+ 'callback({"client_id":"appid","openid":"openid"} );',
+ json.dumps({
+ "nickname": "nickname",
+ "email": "email",
+ "figureurl": "figureurl",
+ "openid": "openid",
+ })
+ ])
+ def test_qq_login(self, mock_do_get):
+ qq_app = self.get_app_by_type('qq')
+ assert qq_app
+ url = qq_app.get_authorization_url()
+ self.assertTrue("qq.com" in url)
+ token = qq_app.get_access_token_by_code('code')
+ userinfo = qq_app.get_oauth_userinfo()
+ self.assertEqual(userinfo.token, 'access_token')
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
+
+ mock_do_post.return_value = json.dumps({"access_token": "access_token",
+ "uid": "uid"
+ })
+ mock_user_info = {
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name1",
+ "id": "id",
+ "email": "email",
+ }
+ mock_do_get.return_value = json.dumps(mock_user_info)
+
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("api.weibo.com" in response.url)
+
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/')
+
+ user = auth.get_user(self.client)
+ assert user.is_authenticated
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, mock_user_info['email'])
+ self.client.logout()
+
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/')
+
+ user = auth.get_user(self.client)
+ assert user.is_authenticated
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, mock_user_info['email'])
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
+
+ mock_do_post.return_value = json.dumps({"access_token": "access_token",
+ "uid": "uid"
+ })
+ mock_user_info = {
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name1",
+ "id": "id",
+ }
+ mock_do_get.return_value = json.dumps(mock_user_info)
+
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("api.weibo.com" in response.url)
+
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+
+ self.assertEqual(response.status_code, 302)
+
+ oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
+ self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
+
+ response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
+
+ self.assertEqual(response.status_code, 302)
+ sign = get_sha256(settings.SECRET_KEY +
+ str(oauth_user_id) + settings.SECRET_KEY)
+
+ url = reverse('oauth:bindsuccess', kwargs={
+ 'oauthid': oauth_user_id,
+ })
+ self.assertEqual(response.url, f'{url}?type=email')
+
+ path = reverse('oauth:email_confirm', kwargs={
+ 'id': oauth_user_id,
+ 'sign': sign
+ })
+ response = self.client.get(path)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
+ user = auth.get_user(self.client)
+ from oauth.models import OAuthUser
+ oauth_user = OAuthUser.objects.get(author=user)
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, 'test@gmail.com')
+ self.assertEqual(oauth_user.pk, oauth_user_id)
diff --git a/src/DjangoBlog-master/oauth/urls.py b/src/DjangoBlog-master/oauth/urls.py
new file mode 100644
index 0000000..c4a12a0
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/urls.py
@@ -0,0 +1,25 @@
+from django.urls import path
+
+from . import views
+
+app_name = "oauth"
+urlpatterns = [
+ path(
+ r'oauth/authorize',
+ views.authorize),
+ path(
+ r'oauth/requireemail/.html',
+ views.RequireEmailView.as_view(),
+ name='require_email'),
+ path(
+ r'oauth/emailconfirm//.html',
+ views.emailconfirm,
+ name='email_confirm'),
+ path(
+ r'oauth/bindsuccess/.html',
+ views.bindsuccess,
+ name='bindsuccess'),
+ path(
+ r'oauth/oauthlogin',
+ views.oauthlogin,
+ name='oauthlogin')]
diff --git a/src/DjangoBlog-master/oauth/views.py b/src/DjangoBlog-master/oauth/views.py
new file mode 100644
index 0000000..12e3a6e
--- /dev/null
+++ b/src/DjangoBlog-master/oauth/views.py
@@ -0,0 +1,253 @@
+import logging
+# Create your views here.
+from urllib.parse import urlparse
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth import login
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import FormView
+
+from djangoblog.blog_signals import oauth_user_login_signal
+from djangoblog.utils import get_current_site
+from djangoblog.utils import send_email, get_sha256
+from oauth.forms import RequireEmailForm
+from .models import OAuthUser
+from .oauthmanager import get_manager_by_type, OAuthAccessTokenException
+
+logger = logging.getLogger(__name__)
+
+
+def get_redirecturl(request):
+ nexturl = request.GET.get('next_url', None)
+ if not nexturl or nexturl == '/login/' or nexturl == '/login':
+ nexturl = '/'
+ return nexturl
+ p = urlparse(nexturl)
+ if p.netloc:
+ site = get_current_site().domain
+ if not p.netloc.replace('www.', '') == site.replace('www.', ''):
+ logger.info('非法url:' + nexturl)
+ return "/"
+ return nexturl
+
+
+def oauthlogin(request):
+ type = request.GET.get('type', None)
+ if not type:
+ return HttpResponseRedirect('/')
+ manager = get_manager_by_type(type)
+ if not manager:
+ return HttpResponseRedirect('/')
+ nexturl = get_redirecturl(request)
+ authorizeurl = manager.get_authorization_url(nexturl)
+ return HttpResponseRedirect(authorizeurl)
+
+
+def authorize(request):
+ type = request.GET.get('type', None)
+ if not type:
+ return HttpResponseRedirect('/')
+ manager = get_manager_by_type(type)
+ if not manager:
+ return HttpResponseRedirect('/')
+ code = request.GET.get('code', None)
+ try:
+ rsp = manager.get_access_token_by_code(code)
+ except OAuthAccessTokenException as e:
+ logger.warning("OAuthAccessTokenException:" + str(e))
+ return HttpResponseRedirect('/')
+ except Exception as e:
+ logger.error(e)
+ rsp = None
+ nexturl = get_redirecturl(request)
+ if not rsp:
+ return HttpResponseRedirect(manager.get_authorization_url(nexturl))
+ user = manager.get_oauth_userinfo()
+ if user:
+ if not user.nickname or not user.nickname.strip():
+ user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+ try:
+ temp = OAuthUser.objects.get(type=type, openid=user.openid)
+ temp.picture = user.picture
+ temp.metadata = user.metadata
+ temp.nickname = user.nickname
+ user = temp
+ except ObjectDoesNotExist:
+ pass
+ # facebook的token过长
+ if type == 'facebook':
+ user.token = ''
+ if user.email:
+ with transaction.atomic():
+ author = None
+ try:
+ author = get_user_model().objects.get(id=user.author_id)
+ except ObjectDoesNotExist:
+ pass
+ if not author:
+ result = get_user_model().objects.get_or_create(email=user.email)
+ author = result[0]
+ if result[1]:
+ try:
+ get_user_model().objects.get(username=user.nickname)
+ except ObjectDoesNotExist:
+ author.username = user.nickname
+ else:
+ author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+ author.source = 'authorize'
+ author.save()
+
+ user.author = author
+ user.save()
+
+ oauth_user_login_signal.send(
+ sender=authorize.__class__, id=user.id)
+ login(request, author)
+ return HttpResponseRedirect(nexturl)
+ else:
+ user.save()
+ url = reverse('oauth:require_email', kwargs={
+ 'oauthid': user.id
+ })
+
+ return HttpResponseRedirect(url)
+ else:
+ return HttpResponseRedirect(nexturl)
+
+
+def emailconfirm(request, id, sign):
+ if not sign:
+ return HttpResponseForbidden()
+ if not get_sha256(settings.SECRET_KEY +
+ str(id) +
+ settings.SECRET_KEY).upper() == sign.upper():
+ return HttpResponseForbidden()
+ oauthuser = get_object_or_404(OAuthUser, pk=id)
+ with transaction.atomic():
+ if oauthuser.author:
+ author = get_user_model().objects.get(pk=oauthuser.author_id)
+ else:
+ result = get_user_model().objects.get_or_create(email=oauthuser.email)
+ author = result[0]
+ if result[1]:
+ author.source = 'emailconfirm'
+ author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
+ ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+ author.save()
+ oauthuser.author = author
+ oauthuser.save()
+ oauth_user_login_signal.send(
+ sender=emailconfirm.__class__,
+ id=oauthuser.id)
+ login(request, author)
+
+ site = 'http://' + get_current_site().domain
+ content = _('''
+ Congratulations, you have successfully bound your email address. You can use
+ %(oauthuser_type)s to directly log in to this website without a password.
+ You are welcome to continue to follow this site, the address is
+ %(site)s
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(site)s
+ ''') % {'oauthuser_type': oauthuser.type, 'site': site}
+
+ send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
+ url = reverse('oauth:bindsuccess', kwargs={
+ 'oauthid': id
+ })
+ url = url + '?type=success'
+ return HttpResponseRedirect(url)
+
+
+class RequireEmailView(FormView):
+ form_class = RequireEmailForm
+ template_name = 'oauth/require_email.html'
+
+ def get(self, request, *args, **kwargs):
+ oauthid = self.kwargs['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if oauthuser.email:
+ pass
+ # return HttpResponseRedirect('/')
+
+ return super(RequireEmailView, self).get(request, *args, **kwargs)
+
+ def get_initial(self):
+ oauthid = self.kwargs['oauthid']
+ return {
+ 'email': '',
+ 'oauthid': oauthid
+ }
+
+ def get_context_data(self, **kwargs):
+ oauthid = self.kwargs['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if oauthuser.picture:
+ kwargs['picture'] = oauthuser.picture
+ return super(RequireEmailView, self).get_context_data(**kwargs)
+
+ def form_valid(self, form):
+ email = form.cleaned_data['email']
+ oauthid = form.cleaned_data['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ oauthuser.email = email
+ oauthuser.save()
+ sign = get_sha256(settings.SECRET_KEY +
+ str(oauthuser.id) + settings.SECRET_KEY)
+ site = get_current_site().domain
+ if settings.DEBUG:
+ site = '127.0.0.1:8000'
+ path = reverse('oauth:email_confirm', kwargs={
+ 'id': oauthid,
+ 'sign': sign
+ })
+ url = "http://{site}{path}".format(site=site, path=path)
+
+ content = _("""
+ Please click the link below to bind your email
+
+ %(url)s
+
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+
+ %(url)s
+ """) % {'url': url}
+ send_email(emailto=[email, ], title=_('Bind your email'), content=content)
+ url = reverse('oauth:bindsuccess', kwargs={
+ 'oauthid': oauthid
+ })
+ url = url + '?type=email'
+ return HttpResponseRedirect(url)
+
+
+def bindsuccess(request, oauthid):
+ type = request.GET.get('type', None)
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if type == 'email':
+ title = _('Bind your email')
+ content = _(
+ 'Congratulations, the binding is just one step away. '
+ 'Please log in to your email to check the email to complete the binding. Thank you.')
+ else:
+ title = _('Binding successful')
+ content = _(
+ "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
+ " to directly log in to this website without a password. You are welcome to continue to follow this site." % {
+ 'oauthuser_type': oauthuser.type})
+ return render(request, 'oauth/bindsuccess.html', {
+ 'title': title,
+ 'content': content
+ })
diff --git a/src/DjangoBlog-master/owntracks/__init__.py b/src/DjangoBlog-master/owntracks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/owntracks/admin.py b/src/DjangoBlog-master/owntracks/admin.py
new file mode 100644
index 0000000..655b535
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+
+# Register your models here.
+
+
+class OwnTrackLogsAdmin(admin.ModelAdmin):
+ pass
diff --git a/src/DjangoBlog-master/owntracks/apps.py b/src/DjangoBlog-master/owntracks/apps.py
new file mode 100644
index 0000000..1bc5f12
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class OwntracksConfig(AppConfig):
+ name = 'owntracks'
diff --git a/src/DjangoBlog-master/owntracks/migrations/0001_initial.py b/src/DjangoBlog-master/owntracks/migrations/0001_initial.py
new file mode 100644
index 0000000..9eee55c
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OwnTrackLog',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('tid', models.CharField(max_length=100, verbose_name='用户')),
+ ('lat', models.FloatField(verbose_name='纬度')),
+ ('lon', models.FloatField(verbose_name='经度')),
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ ],
+ options={
+ 'verbose_name': 'OwnTrackLogs',
+ 'verbose_name_plural': 'OwnTrackLogs',
+ 'ordering': ['created_time'],
+ 'get_latest_by': 'created_time',
+ },
+ ),
+ ]
diff --git a/src/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/src/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
new file mode 100644
index 0000000..b4f8dec
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('owntracks', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='owntracklog',
+ options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'},
+ ),
+ migrations.RenameField(
+ model_name='owntracklog',
+ old_name='created_time',
+ new_name='creation_time',
+ ),
+ ]
diff --git a/src/DjangoBlog-master/owntracks/migrations/__init__.py b/src/DjangoBlog-master/owntracks/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/owntracks/models.py b/src/DjangoBlog-master/owntracks/models.py
new file mode 100644
index 0000000..760942c
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/models.py
@@ -0,0 +1,20 @@
+from django.db import models
+from django.utils.timezone import now
+
+
+# Create your models here.
+
+class OwnTrackLog(models.Model):
+ tid = models.CharField(max_length=100, null=False, verbose_name='用户')
+ lat = models.FloatField(verbose_name='纬度')
+ lon = models.FloatField(verbose_name='经度')
+ creation_time = models.DateTimeField('创建时间', default=now)
+
+ def __str__(self):
+ return self.tid
+
+ class Meta:
+ ordering = ['creation_time']
+ verbose_name = "OwnTrackLogs"
+ verbose_name_plural = verbose_name
+ get_latest_by = 'creation_time'
diff --git a/src/DjangoBlog-master/owntracks/tests.py b/src/DjangoBlog-master/owntracks/tests.py
new file mode 100644
index 0000000..3b4b9d8
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/tests.py
@@ -0,0 +1,64 @@
+import json
+
+from django.test import Client, RequestFactory, TestCase
+
+from accounts.models import BlogUser
+from .models import OwnTrackLog
+
+
+# Create your tests here.
+
+class OwnTrackLogTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_own_track_log(self):
+ o = {
+ 'tid': 12,
+ 'lat': 123.123,
+ 'lon': 134.341
+ }
+
+ self.client.post(
+ '/owntracks/logtracks',
+ json.dumps(o),
+ content_type='application/json')
+ length = len(OwnTrackLog.objects.all())
+ self.assertEqual(length, 1)
+
+ o = {
+ 'tid': 12,
+ 'lat': 123.123
+ }
+
+ self.client.post(
+ '/owntracks/logtracks',
+ json.dumps(o),
+ content_type='application/json')
+ length = len(OwnTrackLog.objects.all())
+ self.assertEqual(length, 1)
+
+ rsp = self.client.get('/owntracks/show_maps')
+ self.assertEqual(rsp.status_code, 302)
+
+ user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+ s = OwnTrackLog()
+ s.tid = 12
+ s.lon = 123.234
+ s.lat = 34.234
+ s.save()
+
+ rsp = self.client.get('/owntracks/show_dates')
+ self.assertEqual(rsp.status_code, 200)
+ rsp = self.client.get('/owntracks/show_maps')
+ self.assertEqual(rsp.status_code, 200)
+ rsp = self.client.get('/owntracks/get_datas')
+ self.assertEqual(rsp.status_code, 200)
+ rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
+ self.assertEqual(rsp.status_code, 200)
diff --git a/src/DjangoBlog-master/owntracks/urls.py b/src/DjangoBlog-master/owntracks/urls.py
new file mode 100644
index 0000000..c19ada8
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+app_name = "owntracks"
+
+urlpatterns = [
+ path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
+ path('owntracks/show_maps', views.show_maps, name='show_maps'),
+ path('owntracks/get_datas', views.get_datas, name='get_datas'),
+ path('owntracks/show_dates', views.show_log_dates, name='show_dates')
+]
diff --git a/src/DjangoBlog-master/owntracks/views.py b/src/DjangoBlog-master/owntracks/views.py
new file mode 100644
index 0000000..4c72bdd
--- /dev/null
+++ b/src/DjangoBlog-master/owntracks/views.py
@@ -0,0 +1,127 @@
+# Create your views here.
+import datetime
+import itertools
+import json
+import logging
+from datetime import timezone
+from itertools import groupby
+
+import django
+import requests
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+from django.http import JsonResponse
+from django.shortcuts import render
+from django.views.decorators.csrf import csrf_exempt
+
+from .models import OwnTrackLog
+
+logger = logging.getLogger(__name__)
+
+
+@csrf_exempt
+def manage_owntrack_log(request):
+ try:
+ s = json.loads(request.read().decode('utf-8'))
+ tid = s['tid']
+ lat = s['lat']
+ lon = s['lon']
+
+ logger.info(
+ 'tid:{tid}.lat:{lat}.lon:{lon}'.format(
+ tid=tid, lat=lat, lon=lon))
+ if tid and lat and lon:
+ m = OwnTrackLog()
+ m.tid = tid
+ m.lat = lat
+ m.lon = lon
+ m.save()
+ return HttpResponse('ok')
+ else:
+ return HttpResponse('data error')
+ except Exception as e:
+ logger.error(e)
+ return HttpResponse('error')
+
+
+@login_required
+def show_maps(request):
+ if request.user.is_superuser:
+ defaultdate = str(datetime.datetime.now(timezone.utc).date())
+ date = request.GET.get('date', defaultdate)
+ context = {
+ 'date': date
+ }
+ return render(request, 'owntracks/show_maps.html', context)
+ else:
+ from django.http import HttpResponseForbidden
+ return HttpResponseForbidden()
+
+
+@login_required
+def show_log_dates(request):
+ dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
+ results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
+
+ context = {
+ 'results': results
+ }
+ return render(request, 'owntracks/show_log_dates.html', context)
+
+
+def convert_to_amap(locations):
+ convert_result = []
+ it = iter(locations)
+
+ item = list(itertools.islice(it, 30))
+ while item:
+ datas = ';'.join(
+ set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
+
+ key = '8440a376dfc9743d8924bf0ad141f28e'
+ api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
+ query = {
+ 'key': key,
+ 'locations': datas,
+ 'coordsys': 'gps'
+ }
+ rsp = requests.get(url=api, params=query)
+ result = json.loads(rsp.text)
+ if "locations" in result:
+ convert_result.append(result['locations'])
+ item = list(itertools.islice(it, 30))
+
+ return ";".join(convert_result)
+
+
+@login_required
+def get_datas(request):
+ now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
+ querydate = django.utils.timezone.datetime(
+ now.year, now.month, now.day, 0, 0, 0)
+ if request.GET.get('date', None):
+ date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
+ querydate = django.utils.timezone.datetime(
+ date[0], date[1], date[2], 0, 0, 0)
+ nextdate = querydate + datetime.timedelta(days=1)
+ models = OwnTrackLog.objects.filter(
+ creation_time__range=(querydate, nextdate))
+ result = list()
+ if models and len(models):
+ for tid, item in groupby(
+ sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
+
+ d = dict()
+ d["name"] = tid
+ paths = list()
+ # 使用高德转换后的经纬度
+ # locations = convert_to_amap(
+ # sorted(item, key=lambda x: x.creation_time))
+ # for i in locations.split(';'):
+ # paths.append(i.split(','))
+ # 使用GPS原始经纬度
+ for location in sorted(item, key=lambda x: x.creation_time):
+ paths.append([str(location.lon), str(location.lat)])
+ d["path"] = paths
+ result.append(d)
+ return JsonResponse(result, safe=False)
diff --git a/src/DjangoBlog-master/plugins/__init__.py b/src/DjangoBlog-master/plugins/__init__.py
new file mode 100644
index 0000000..e88afca
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
diff --git a/src/DjangoBlog-master/plugins/article_copyright/__init__.py b/src/DjangoBlog-master/plugins/article_copyright/__init__.py
new file mode 100644
index 0000000..e88afca
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/article_copyright/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
diff --git a/src/DjangoBlog-master/plugins/article_copyright/plugin.py b/src/DjangoBlog-master/plugins/article_copyright/plugin.py
new file mode 100644
index 0000000..317fed2
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/article_copyright/plugin.py
@@ -0,0 +1,32 @@
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+
+class ArticleCopyrightPlugin(BasePlugin):
+ PLUGIN_NAME = '文章结尾版权声明'
+ PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。'
+ PLUGIN_VERSION = '0.2.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ # 2. 实现 register_hooks 方法,专门用于注册钩子
+ def register_hooks(self):
+ # 在这里将插件的方法注册到指定的钩子上
+ hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content)
+
+ def add_copyright_to_content(self, content, *args, **kwargs):
+ """
+ 这个方法会被注册到 'the_content' 过滤器钩子上。
+ 它接收原始内容,并返回添加了版权信息的新内容。
+ """
+ article = kwargs.get('article')
+ if not article:
+ return content
+
+ copyright_info = f"\n本文由 {article.author.username} 原创,转载请注明出处。
"
+ return content + copyright_info
+
+
+# 3. 实例化插件。
+# 这会自动调用 BasePlugin.__init__,然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。
+plugin = ArticleCopyrightPlugin()
diff --git a/src/DjangoBlog-master/plugins/external_links/__init__.py b/src/DjangoBlog-master/plugins/external_links/__init__.py
new file mode 100644
index 0000000..e88afca
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/external_links/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
diff --git a/src/DjangoBlog-master/plugins/external_links/plugin.py b/src/DjangoBlog-master/plugins/external_links/plugin.py
new file mode 100644
index 0000000..5b2ef14
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/external_links/plugin.py
@@ -0,0 +1,48 @@
+import re
+from urllib.parse import urlparse
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+
+class ExternalLinksPlugin(BasePlugin):
+ PLUGIN_NAME = '外部链接处理器'
+ PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。'
+ PLUGIN_VERSION = '0.1.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def register_hooks(self):
+ hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links)
+
+ def process_external_links(self, content, *args, **kwargs):
+ from djangoblog.utils import get_current_site
+ site_domain = get_current_site().domain
+
+ # 正则表达式查找所有 标签
+ link_pattern = re.compile(r'( ]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE)
+
+ def replacer(match):
+ # match.group(1) 是 ...
+ href = match.group(2)
+
+ # 如果链接已经有 target 属性,则不处理
+ if 'target=' in match.group(0).lower():
+ return match.group(0)
+
+ # 解析链接
+ parsed_url = urlparse(href)
+
+ # 如果链接是外部的 (有域名且域名不等于当前网站域名)
+ if parsed_url.netloc and parsed_url.netloc != site_domain:
+ # 添加 target 和 rel 属性
+ return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}'
+
+ # 否则返回原样
+ return match.group(0)
+
+ return link_pattern.sub(replacer, content)
+
+
+plugin = ExternalLinksPlugin()
diff --git a/src/DjangoBlog-master/plugins/reading_time/__init__.py b/src/DjangoBlog-master/plugins/reading_time/__init__.py
new file mode 100644
index 0000000..e88afca
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/reading_time/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
diff --git a/src/DjangoBlog-master/plugins/reading_time/plugin.py b/src/DjangoBlog-master/plugins/reading_time/plugin.py
new file mode 100644
index 0000000..35f9db1
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/reading_time/plugin.py
@@ -0,0 +1,43 @@
+import math
+import re
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+
+class ReadingTimePlugin(BasePlugin):
+ PLUGIN_NAME = '阅读时间预测'
+ PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。'
+ PLUGIN_VERSION = '0.1.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def register_hooks(self):
+ hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time)
+
+ def add_reading_time(self, content, *args, **kwargs):
+ """
+ 计算阅读时间并添加到内容开头。
+ """
+ # 移除HTML标签和空白字符,以获得纯文本
+ clean_content = re.sub(r'<[^>]*>', '', content)
+ clean_content = clean_content.strip()
+
+ # 中文和英文单词混合计数的一个简单方法
+ # 匹配中文字符或连续的非中文字符(视为单词)
+ words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content)
+ word_count = len(words)
+
+ # 按平均每分钟200字的速度计算
+ reading_speed = 200
+ reading_minutes = math.ceil(word_count / reading_speed)
+
+ # 如果阅读时间少于1分钟,则显示为1分钟
+ if reading_minutes < 1:
+ reading_minutes = 1
+
+ reading_time_html = f'预计阅读时间:{reading_minutes} 分钟
'
+
+ return reading_time_html + content
+
+
+plugin = ReadingTimePlugin()
\ No newline at end of file
diff --git a/src/DjangoBlog-master/plugins/seo_optimizer/__init__.py b/src/DjangoBlog-master/plugins/seo_optimizer/__init__.py
new file mode 100644
index 0000000..e88afca
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/seo_optimizer/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
diff --git a/src/DjangoBlog-master/plugins/seo_optimizer/plugin.py b/src/DjangoBlog-master/plugins/seo_optimizer/plugin.py
new file mode 100644
index 0000000..b5b19a3
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/seo_optimizer/plugin.py
@@ -0,0 +1,142 @@
+import json
+from django.utils.html import strip_tags
+from django.template.defaultfilters import truncatewords
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from blog.models import Article, Category, Tag
+from djangoblog.utils import get_blog_setting
+
+
+class SeoOptimizerPlugin(BasePlugin):
+ PLUGIN_NAME = 'SEO 优化器'
+ PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
+ PLUGIN_VERSION = '0.2.0'
+ PLUGIN_AUTHOR = 'liuangliangyy'
+
+ def register_hooks(self):
+ hooks.register('head_meta', self.dispatch_seo_generation)
+
+ def _get_article_seo_data(self, context, request, blog_setting):
+ article = context.get('article')
+ if not isinstance(article, Article):
+ return None
+
+ description = strip_tags(article.body)[:150]
+ keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
+
+ meta_tags = f'''
+
+
+
+
+
+
+
+
+ '''
+ for tag in article.tags.all():
+ meta_tags += f' '
+ meta_tags += f' '
+
+ structured_data = {
+ "@context": "https://schema.org",
+ "@type": "Article",
+ "mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
+ "headline": article.title,
+ "description": description,
+ "image": request.build_absolute_uri(article.get_first_image_url()),
+ "datePublished": article.pub_time.isoformat(),
+ "dateModified": article.last_modify_time.isoformat(),
+ "author": {"@type": "Person", "name": article.author.username},
+ "publisher": {"@type": "Organization", "name": blog_setting.site_name}
+ }
+ if not structured_data.get("image"):
+ del structured_data["image"]
+
+ return {
+ "title": f"{article.title} | {blog_setting.site_name}",
+ "description": description,
+ "keywords": keywords,
+ "meta_tags": meta_tags,
+ "json_ld": structured_data
+ }
+
+ def _get_category_seo_data(self, context, request, blog_setting):
+ category_name = context.get('tag_name')
+ if not category_name:
+ return None
+
+ category = Category.objects.filter(name=category_name).first()
+ if not category:
+ return None
+
+ title = f"{category.name} | {blog_setting.site_name}"
+ description = strip_tags(category.name) or blog_setting.site_description
+ keywords = category.name
+
+ # BreadcrumbList structured data for category page
+ breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}]
+ breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
+
+ structured_data = {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ "itemListElement": breadcrumb_items
+ }
+
+ return {
+ "title": title,
+ "description": description,
+ "keywords": keywords,
+ "meta_tags": "",
+ "json_ld": structured_data
+ }
+
+ def _get_default_seo_data(self, context, request, blog_setting):
+ # Homepage and other default pages
+ structured_data = {
+ "@context": "https://schema.org",
+ "@type": "WebSite",
+ "url": request.build_absolute_uri('/'),
+ "potentialAction": {
+ "@type": "SearchAction",
+ "target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
+ "query-input": "required name=search_term_string"
+ }
+ }
+ return {
+ "title": f"{blog_setting.site_name} | {blog_setting.site_description}",
+ "description": blog_setting.site_description,
+ "keywords": blog_setting.site_keywords,
+ "meta_tags": "",
+ "json_ld": structured_data
+ }
+
+ def dispatch_seo_generation(self, metas, context):
+ request = context.get('request')
+ if not request:
+ return metas
+
+ view_name = request.resolver_match.view_name
+ blog_setting = get_blog_setting()
+
+ seo_data = None
+ if view_name == 'blog:detailbyid':
+ seo_data = self._get_article_seo_data(context, request, blog_setting)
+ elif view_name == 'blog:category_detail':
+ seo_data = self._get_category_seo_data(context, request, blog_setting)
+
+ if not seo_data:
+ seo_data = self._get_default_seo_data(context, request, blog_setting)
+
+ json_ld_script = f''
+
+ return f"""
+ {seo_data.get("title", "")}
+
+
+ {seo_data.get("meta_tags", "")}
+ {json_ld_script}
+ """
+
+plugin = SeoOptimizerPlugin()
diff --git a/src/DjangoBlog-master/plugins/view_count/__init__.py b/src/DjangoBlog-master/plugins/view_count/__init__.py
new file mode 100644
index 0000000..8804fdf
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/view_count/__init__.py
@@ -0,0 +1 @@
+# This file makes this a Python package
\ No newline at end of file
diff --git a/src/DjangoBlog-master/plugins/view_count/plugin.py b/src/DjangoBlog-master/plugins/view_count/plugin.py
new file mode 100644
index 0000000..15e9d94
--- /dev/null
+++ b/src/DjangoBlog-master/plugins/view_count/plugin.py
@@ -0,0 +1,18 @@
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+
+
+class ViewCountPlugin(BasePlugin):
+ PLUGIN_NAME = '文章浏览次数统计'
+ PLUGIN_DESCRIPTION = '统计文章的浏览次数'
+ PLUGIN_VERSION = '0.1.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def register_hooks(self):
+ hooks.register('after_article_body_get', self.record_view)
+
+ def record_view(self, article, *args, **kwargs):
+ article.viewed()
+
+
+plugin = ViewCountPlugin()
\ No newline at end of file
diff --git a/src/DjangoBlog-master/requirements.txt b/src/DjangoBlog-master/requirements.txt
new file mode 100644
index 0000000..9dc5c93
Binary files /dev/null and b/src/DjangoBlog-master/requirements.txt differ
diff --git a/src/DjangoBlog-master/servermanager/MemcacheStorage.py b/src/DjangoBlog-master/servermanager/MemcacheStorage.py
new file mode 100644
index 0000000..38a7990
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/MemcacheStorage.py
@@ -0,0 +1,32 @@
+from werobot.session import SessionStorage
+from werobot.utils import json_loads, json_dumps
+
+from djangoblog.utils import cache
+
+
+class MemcacheStorage(SessionStorage):
+ def __init__(self, prefix='ws_'):
+ self.prefix = prefix
+ self.cache = cache
+
+ @property
+ def is_available(self):
+ value = "1"
+ self.set('checkavaliable', value=value)
+ return value == self.get('checkavaliable')
+
+ def key_name(self, s):
+ return '{prefix}{s}'.format(prefix=self.prefix, s=s)
+
+ def get(self, id):
+ id = self.key_name(id)
+ session_json = self.cache.get(id) or '{}'
+ return json_loads(session_json)
+
+ def set(self, id, value):
+ id = self.key_name(id)
+ self.cache.set(id, json_dumps(value))
+
+ def delete(self, id):
+ id = self.key_name(id)
+ self.cache.delete(id)
diff --git a/src/DjangoBlog-master/servermanager/__init__.py b/src/DjangoBlog-master/servermanager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/servermanager/admin.py b/src/DjangoBlog-master/servermanager/admin.py
new file mode 100644
index 0000000..f26f4f6
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/admin.py
@@ -0,0 +1,19 @@
+from django.contrib import admin
+# Register your models here.
+
+
+class CommandsAdmin(admin.ModelAdmin):
+ list_display = ('title', 'command', 'describe')
+
+
+class EmailSendLogAdmin(admin.ModelAdmin):
+ list_display = ('title', 'emailto', 'send_result', 'creation_time')
+ readonly_fields = (
+ 'title',
+ 'emailto',
+ 'send_result',
+ 'creation_time',
+ 'content')
+
+ def has_add_permission(self, request):
+ return False
diff --git a/src/DjangoBlog-master/servermanager/api/__init__.py b/src/DjangoBlog-master/servermanager/api/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/api/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/DjangoBlog-master/servermanager/api/blogapi.py b/src/DjangoBlog-master/servermanager/api/blogapi.py
new file mode 100644
index 0000000..8a4d6ac
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/api/blogapi.py
@@ -0,0 +1,27 @@
+from haystack.query import SearchQuerySet
+
+from blog.models import Article, Category
+
+
+class BlogApi:
+ def __init__(self):
+ self.searchqueryset = SearchQuerySet()
+ self.searchqueryset.auto_query('')
+ self.__max_takecount__ = 8
+
+ def search_articles(self, query):
+ sqs = self.searchqueryset.auto_query(query)
+ sqs = sqs.load_all()
+ return sqs[:self.__max_takecount__]
+
+ def get_category_lists(self):
+ return Category.objects.all()
+
+ def get_category_articles(self, categoryname):
+ articles = Article.objects.filter(category__name=categoryname)
+ if articles:
+ return articles[:self.__max_takecount__]
+ return None
+
+ def get_recent_articles(self):
+ return Article.objects.all()[:self.__max_takecount__]
diff --git a/src/DjangoBlog-master/servermanager/api/commonapi.py b/src/DjangoBlog-master/servermanager/api/commonapi.py
new file mode 100644
index 0000000..83ad9ff
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/api/commonapi.py
@@ -0,0 +1,64 @@
+import logging
+import os
+
+import openai
+
+from servermanager.models import commands
+
+logger = logging.getLogger(__name__)
+
+openai.api_key = os.environ.get('OPENAI_API_KEY')
+if os.environ.get('HTTP_PROXY'):
+ openai.proxy = os.environ.get('HTTP_PROXY')
+
+
+class ChatGPT:
+
+ @staticmethod
+ def chat(prompt):
+ try:
+ completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
+ messages=[{"role": "user", "content": prompt}])
+ return completion.choices[0].message.content
+ except Exception as e:
+ logger.error(e)
+ return "服务器出错了"
+
+
+class CommandHandler:
+ def __init__(self):
+ self.commands = commands.objects.all()
+
+ def run(self, title):
+ """
+ 运行命令
+ :param title: 命令
+ :return: 返回命令执行结果
+ """
+ cmd = list(
+ filter(
+ lambda x: x.title.upper() == title.upper(),
+ self.commands))
+ if cmd:
+ return self.__run_command__(cmd[0].command)
+ else:
+ return "未找到相关命令,请输入hepme获得帮助。"
+
+ def __run_command__(self, cmd):
+ try:
+ res = os.popen(cmd).read()
+ return res
+ except BaseException:
+ return '命令执行出错!'
+
+ def get_help(self):
+ rsp = ''
+ for cmd in self.commands:
+ rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
+ return rsp
+
+
+if __name__ == '__main__':
+ chatbot = ChatGPT()
+ prompt = "写一篇1000字关于AI的论文"
+ print(chatbot.chat(prompt))
diff --git a/src/DjangoBlog-master/servermanager/apps.py b/src/DjangoBlog-master/servermanager/apps.py
new file mode 100644
index 0000000..03cc38d
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ServermanagerConfig(AppConfig):
+ name = 'servermanager'
diff --git a/src/DjangoBlog-master/servermanager/migrations/0001_initial.py b/src/DjangoBlog-master/servermanager/migrations/0001_initial.py
new file mode 100644
index 0000000..bbdbf77
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='commands',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=300, verbose_name='命令标题')),
+ ('command', models.CharField(max_length=2000, verbose_name='命令')),
+ ('describe', models.CharField(max_length=300, verbose_name='命令描述')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+ ],
+ options={
+ 'verbose_name': '命令',
+ 'verbose_name_plural': '命令',
+ },
+ ),
+ migrations.CreateModel(
+ name='EmailSendLog',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('emailto', models.CharField(max_length=300, verbose_name='收件人')),
+ ('title', models.CharField(max_length=2000, verbose_name='邮件标题')),
+ ('content', models.TextField(verbose_name='邮件内容')),
+ ('send_result', models.BooleanField(default=False, verbose_name='结果')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ],
+ options={
+ 'verbose_name': '邮件发送log',
+ 'verbose_name_plural': '邮件发送log',
+ 'ordering': ['-created_time'],
+ },
+ ),
+ ]
diff --git a/src/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/src/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
new file mode 100644
index 0000000..4858857
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servermanager', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='emailsendlog',
+ options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
+ ),
+ migrations.RenameField(
+ model_name='commands',
+ old_name='created_time',
+ new_name='creation_time',
+ ),
+ migrations.RenameField(
+ model_name='commands',
+ old_name='last_mod_time',
+ new_name='last_modify_time',
+ ),
+ migrations.RenameField(
+ model_name='emailsendlog',
+ old_name='created_time',
+ new_name='creation_time',
+ ),
+ ]
diff --git a/src/DjangoBlog-master/servermanager/migrations/__init__.py b/src/DjangoBlog-master/servermanager/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog-master/servermanager/models.py b/src/DjangoBlog-master/servermanager/models.py
new file mode 100644
index 0000000..4326c65
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/models.py
@@ -0,0 +1,33 @@
+from django.db import models
+
+
+# Create your models here.
+class commands(models.Model):
+ title = models.CharField('命令标题', max_length=300)
+ command = models.CharField('命令', max_length=2000)
+ describe = models.CharField('命令描述', max_length=300)
+ creation_time = models.DateTimeField('创建时间', auto_now_add=True)
+ last_modify_time = models.DateTimeField('修改时间', auto_now=True)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ verbose_name = '命令'
+ verbose_name_plural = verbose_name
+
+
+class EmailSendLog(models.Model):
+ emailto = models.CharField('收件人', max_length=300)
+ title = models.CharField('邮件标题', max_length=2000)
+ content = models.TextField('邮件内容')
+ send_result = models.BooleanField('结果', default=False)
+ creation_time = models.DateTimeField('创建时间', auto_now_add=True)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ verbose_name = '邮件发送log'
+ verbose_name_plural = verbose_name
+ ordering = ['-creation_time']
diff --git a/src/DjangoBlog-master/servermanager/robot.py b/src/DjangoBlog-master/servermanager/robot.py
new file mode 100644
index 0000000..7b45736
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/robot.py
@@ -0,0 +1,187 @@
+import logging
+import os
+import re
+
+import jsonpickle
+from django.conf import settings
+from werobot import WeRoBot
+from werobot.replies import ArticlesReply, Article
+from werobot.session.filestorage import FileStorage
+
+from djangoblog.utils import get_sha256
+from servermanager.api.blogapi import BlogApi
+from servermanager.api.commonapi import ChatGPT, CommandHandler
+from .MemcacheStorage import MemcacheStorage
+
+robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN')
+ or 'lylinux', enable_session=True)
+memstorage = MemcacheStorage()
+if memstorage.is_available:
+ robot.config['SESSION_STORAGE'] = memstorage
+else:
+ if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')):
+ os.remove(os.path.join(settings.BASE_DIR, 'werobot_session'))
+ robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
+
+blogapi = BlogApi()
+cmd_handler = CommandHandler()
+logger = logging.getLogger(__name__)
+
+
+def convert_to_article_reply(articles, message):
+ reply = ArticlesReply(message=message)
+ from blog.templatetags.blog_tags import truncatechars_content
+ for post in articles:
+ imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body)
+ imgurl = ''
+ if imgs:
+ imgurl = imgs[0]
+ article = Article(
+ title=post.title,
+ description=truncatechars_content(post.body),
+ img=imgurl,
+ url=post.get_full_url()
+ )
+ reply.add_article(article)
+ return reply
+
+
+@robot.filter(re.compile(r"^\?.*"))
+def search(message, session):
+ s = message.content
+ searchstr = str(s).replace('?', '')
+ result = blogapi.search_articles(searchstr)
+ if result:
+ articles = list(map(lambda x: x.object, result))
+ reply = convert_to_article_reply(articles, message)
+ return reply
+ else:
+ return '没有找到相关文章。'
+
+
+@robot.filter(re.compile(r'^category\s*$', re.I))
+def category(message, session):
+ categorys = blogapi.get_category_lists()
+ content = ','.join(map(lambda x: x.name, categorys))
+ return '所有文章分类目录:' + content
+
+
+@robot.filter(re.compile(r'^recent\s*$', re.I))
+def recents(message, session):
+ articles = blogapi.get_recent_articles()
+ if articles:
+ reply = convert_to_article_reply(articles, message)
+ return reply
+ else:
+ return "暂时还没有文章"
+
+
+@robot.filter(re.compile('^help$', re.I))
+def help(message, session):
+ return '''欢迎关注!
+ 默认会与图灵机器人聊天~~
+ 你可以通过下面这些命令来获得信息
+ ?关键字搜索文章.
+ 如?python.
+ category获得文章分类目录及文章数.
+ category-***获得该分类目录文章
+ 如category-python
+ recent获得最新文章
+ help获得帮助.
+ weather:获得天气
+ 如weather:西安
+ idcard:获得身份证信息
+ 如idcard:61048119xxxxxxxxxx
+ music:音乐搜索
+ 如music:阴天快乐
+ PS:以上标点符号都不支持中文标点~~
+ '''
+
+
+@robot.filter(re.compile(r'^weather\:.*$', re.I))
+def weather(message, session):
+ return "建设中..."
+
+
+@robot.filter(re.compile(r'^idcard\:.*$', re.I))
+def idcard(message, session):
+ return "建设中..."
+
+
+@robot.handler
+def echo(message, session):
+ handler = MessageHandler(message, session)
+ return handler.handler()
+
+
+class MessageHandler:
+ def __init__(self, message, session):
+ userid = message.source
+ self.message = message
+ self.session = session
+ self.userid = userid
+ try:
+ info = session[userid]
+ self.userinfo = jsonpickle.decode(info)
+ except Exception as e:
+ userinfo = WxUserInfo()
+ self.userinfo = userinfo
+
+ @property
+ def is_admin(self):
+ return self.userinfo.isAdmin
+
+ @property
+ def is_password_set(self):
+ return self.userinfo.isPasswordSet
+
+ def save_session(self):
+ info = jsonpickle.encode(self.userinfo)
+ self.session[self.userid] = info
+
+ def handler(self):
+ info = self.message.content
+
+ if self.userinfo.isAdmin and info.upper() == 'EXIT':
+ self.userinfo = WxUserInfo()
+ self.save_session()
+ return "退出成功"
+ if info.upper() == 'ADMIN':
+ self.userinfo.isAdmin = True
+ self.save_session()
+ return "输入管理员密码"
+ if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
+ passwd = settings.WXADMIN
+ if settings.TESTING:
+ passwd = '123'
+ if passwd.upper() == get_sha256(get_sha256(info)).upper():
+ self.userinfo.isPasswordSet = True
+ self.save_session()
+ return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
+ else:
+ if self.userinfo.Count >= 3:
+ self.userinfo = WxUserInfo()
+ self.save_session()
+ return "超过验证次数"
+ self.userinfo.Count += 1
+ self.save_session()
+ return "验证失败,请重新输入管理员密码:"
+ if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
+ if self.userinfo.Command != '' and info.upper() == 'Y':
+ return cmd_handler.run(self.userinfo.Command)
+ else:
+ if info.upper() == 'HELPME':
+ return cmd_handler.get_help()
+ self.userinfo.Command = info
+ self.save_session()
+ return "确认执行: " + info + " 命令?"
+
+ return ChatGPT.chat(info)
+
+
+class WxUserInfo():
+ def __init__(self):
+ self.isAdmin = False
+ self.isPasswordSet = False
+ self.Count = 0
+ self.Command = ''
diff --git a/src/DjangoBlog-master/servermanager/tests.py b/src/DjangoBlog-master/servermanager/tests.py
new file mode 100644
index 0000000..22a6689
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/tests.py
@@ -0,0 +1,79 @@
+from django.test import Client, RequestFactory, TestCase
+from django.utils import timezone
+from werobot.messages.messages import TextMessage
+
+from accounts.models import BlogUser
+from blog.models import Category, Article
+from servermanager.api.commonapi import ChatGPT
+from .models import commands
+from .robot import MessageHandler, CommandHandler
+from .robot import search, category, recents
+
+
+# Create your tests here.
+class ServerManagerTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_chat_gpt(self):
+ content = ChatGPT.chat("你好")
+ self.assertIsNotNone(content)
+
+ def test_validate_comment(self):
+ user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ c = Category()
+ c.name = "categoryccc"
+ c.save()
+
+ article = Article()
+ article.title = "nicetitleccc"
+ article.body = "nicecontentccc"
+ article.author = user
+ article.category = c
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+ s = TextMessage([])
+ s.content = "nice"
+ rsp = search(s, None)
+ rsp = category(None, None)
+ self.assertIsNotNone(rsp)
+ rsp = recents(None, None)
+ self.assertTrue(rsp != '暂时还没有文章')
+
+ cmd = commands()
+ cmd.title = "test"
+ cmd.command = "ls"
+ cmd.describe = "test"
+ cmd.save()
+
+ cmdhandler = CommandHandler()
+ rsp = cmdhandler.run('test')
+ self.assertIsNotNone(rsp)
+ s.source = 'u'
+ s.content = 'test'
+ msghandler = MessageHandler(s, {})
+
+ # msghandler.userinfo.isPasswordSet = True
+ # msghandler.userinfo.isAdmin = True
+ msghandler.handler()
+ s.content = 'y'
+ msghandler.handler()
+ s.content = 'idcard:12321233'
+ msghandler.handler()
+ s.content = 'weather:上海'
+ msghandler.handler()
+ s.content = 'admin'
+ msghandler.handler()
+ s.content = '123'
+ msghandler.handler()
+
+ s.content = 'exit'
+ msghandler.handler()
diff --git a/src/DjangoBlog-master/servermanager/urls.py b/src/DjangoBlog-master/servermanager/urls.py
new file mode 100644
index 0000000..8d134d2
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path
+from werobot.contrib.django import make_view
+
+from .robot import robot
+
+app_name = "servermanager"
+urlpatterns = [
+ path(r'robot', make_view(robot)),
+
+]
diff --git a/src/DjangoBlog-master/servermanager/views.py b/src/DjangoBlog-master/servermanager/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/src/DjangoBlog-master/servermanager/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/src/DjangoBlog-master/templates/account/forget_password.html b/src/DjangoBlog-master/templates/account/forget_password.html
new file mode 100644
index 0000000..3384531
--- /dev/null
+++ b/src/DjangoBlog-master/templates/account/forget_password.html
@@ -0,0 +1,30 @@
+{% extends 'share_layout/base_account.html' %}
+{% load i18n %}
+{% load static %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/account/login.html b/src/DjangoBlog-master/templates/account/login.html
new file mode 100644
index 0000000..cff8d33
--- /dev/null
+++ b/src/DjangoBlog-master/templates/account/login.html
@@ -0,0 +1,46 @@
+{% extends 'share_layout/base_account.html' %}
+{% load static %}
+{% load i18n %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/account/registration_form.html b/src/DjangoBlog-master/templates/account/registration_form.html
new file mode 100644
index 0000000..65e7549
--- /dev/null
+++ b/src/DjangoBlog-master/templates/account/registration_form.html
@@ -0,0 +1,29 @@
+{% extends 'share_layout/base_account.html' %}
+{% load static %}
+{% block content %}
+
+
+
Create Your Account
+
+
+
+
+
+
+
+ Sign In
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/account/result.html b/src/DjangoBlog-master/templates/account/result.html
new file mode 100644
index 0000000..23c9094
--- /dev/null
+++ b/src/DjangoBlog-master/templates/account/result.html
@@ -0,0 +1,27 @@
+{% extends 'share_layout/base.html' %}
+{% load i18n %}
+{% block header %}
+ {{ title }}
+{% endblock %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/blog/article_archives.html b/src/DjangoBlog-master/templates/blog/article_archives.html
new file mode 100644
index 0000000..959319e
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/article_archives.html
@@ -0,0 +1,60 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% load i18n %}
+{% block header %}
+
+ {% trans 'article archive' %} | {{ SITE_DESCRIPTION }}
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ {% regroup article_list by pub_time.year as year_post_group %}
+
+ {% for year in year_post_group %}
+ {{ year.grouper }} {% trans 'year' %}
+ {% regroup year.list by pub_time.month as month_post_group %}
+
+ {% for month in month_post_group %}
+ {{ month.grouper }} {% trans 'month' %}
+
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/src/DjangoBlog-master/templates/blog/article_detail.html b/src/DjangoBlog-master/templates/blog/article_detail.html
new file mode 100644
index 0000000..a74a0db
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/article_detail.html
@@ -0,0 +1,52 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+
+{% block header %}
+{% endblock %}
+{% block content %}
+
+
+ {% load_article_detail article False user %}
+
+ {% if article.type == 'a' %}
+
+ 文章导航
+ {% if next_article %}
+
+ ← {{ next_article.title }}
+ {% endif %}
+ {% if prev_article %}
+ {{ prev_article.title }} →
+ {% endif %}
+
+ {% endif %}
+
+
+ {% if article.comment_status == "o" and OPEN_SITE_COMMENT %}
+
+
+ {% include 'comments/tags/comment_list.html' %}
+ {% if user.is_authenticated %}
+ {% include 'comments/tags/post_comment.html' %}
+ {% else %}
+
+ {% endif %}
+ {% endif %}
+
+
+{% endblock %}
+
+{% block sidebar %}
+ {% load_sidebar user "p" %}
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/blog/article_index.html b/src/DjangoBlog-master/templates/blog/article_index.html
new file mode 100644
index 0000000..0ee6150
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/article_index.html
@@ -0,0 +1,42 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+ {% if tag_name %}
+ {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }}
+ {% comment %} {% endcomment %}
+ {% else %}
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+ {% if page_type and tag_name %}
+
+ {% endif %}
+
+ {% for article in article_list %}
+ {% load_article_detail article True user %}
+ {% endfor %}
+ {% if is_paginated %}
+ {% load_pagination_info page_obj page_type tag_name %}
+
+ {% endif %}
+
+
+
+{% endblock %}
+{% block sidebar %}
+ {% load_sidebar user linktype %}
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/blog/error_page.html b/src/DjangoBlog-master/templates/blog/error_page.html
new file mode 100644
index 0000000..d41cfb6
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/error_page.html
@@ -0,0 +1,45 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+ {% if tag_name %}
+ {% if statuscode == '404' %}
+ 404 NotFound
+ {% elif statuscode == '403' %}
+ Permission Denied
+ {% elif statuscode == '500' %}
+ 500 Error
+ {% else %}
+
+ {% endif %}
+ {% comment %} {% endcomment %}
+ {% else %}
+ {{ SITE_NAME }} | {{ SITE_DESCRIPTION }}
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/src/DjangoBlog-master/templates/blog/links_list.html b/src/DjangoBlog-master/templates/blog/links_list.html
new file mode 100644
index 0000000..ccecbea
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/links_list.html
@@ -0,0 +1,44 @@
+{% extends 'share_layout/base.html' %}
+{% load blog_tags %}
+{% load cache %}
+{% block header %}
+
+ 友情链接 | {{ SITE_DESCRIPTION }}
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+{% endblock %}
+
+
+{% block sidebar %}
+ {% load_sidebar user 'i' %}
+{% endblock %}
+
+
diff --git a/src/DjangoBlog-master/templates/blog/tags/article_info.html b/src/DjangoBlog-master/templates/blog/tags/article_info.html
new file mode 100644
index 0000000..3deec44
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/article_info.html
@@ -0,0 +1,74 @@
+{% load blog_tags %}
+{% load cache %}
+{% load i18n %}
+
+
+
+
+ {% if isindex %}
+ {{ article.body|custom_markdown|escape|truncatechars_content }}
+
Read more
+ {% else %}
+
+ {% if article.show_toc %}
+ {% get_markdown_toc article.body as toc %}
+
{% trans 'toc' %}:
+ {{ toc|safe }}
+
+
+ {% endif %}
+
+
+ {{ article.body|custom_markdown|escape }}
+
+
+ {% endif %}
+
+
+
+ {% load_article_metas article user %}
+
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html b/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html
new file mode 100644
index 0000000..cb6111c
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html
@@ -0,0 +1,59 @@
+{% load i18n %}
+{% load blog_tags %}
+
+
+
+
+
diff --git a/src/DjangoBlog-master/templates/blog/tags/article_pagination.html b/src/DjangoBlog-master/templates/blog/tags/article_pagination.html
new file mode 100644
index 0000000..95514ff
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/article_pagination.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
+
+ {% trans 'article navigation' %}
+
+ {% if page_obj.has_next and next_url%}
+
+ {% endif %}
+ {% if page_obj.has_previous and previous_url %}
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html b/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html
new file mode 100644
index 0000000..c8ba474
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html
@@ -0,0 +1,19 @@
+{% load i18n %}
+{% if article_tags_list %}
+
+{% endif %}
diff --git a/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html b/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html
new file mode 100644
index 0000000..67087d5
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html
@@ -0,0 +1,19 @@
+
+
+ {% for name,url in names %}
+
+
+ {{ name }}
+
+
+
+ {% endfor %}
+
+ {{ title }}
+
+
+
+
+
diff --git a/src/DjangoBlog-master/templates/blog/tags/sidebar.html b/src/DjangoBlog-master/templates/blog/tags/sidebar.html
new file mode 100644
index 0000000..f70544c
--- /dev/null
+++ b/src/DjangoBlog-master/templates/blog/tags/sidebar.html
@@ -0,0 +1,136 @@
+{% load blog_tags %}
+{% load i18n %}
+
diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_item.html b/src/DjangoBlog-master/templates/comments/tags/comment_item.html
new file mode 100644
index 0000000..ebb0388
--- /dev/null
+++ b/src/DjangoBlog-master/templates/comments/tags/comment_item.html
@@ -0,0 +1,34 @@
+{% load blog_tags %}
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html
new file mode 100644
index 0000000..a9decd1
--- /dev/null
+++ b/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html
@@ -0,0 +1,54 @@
+{% load blog_tags %}
+
+{% query article_comments parent_comment=comment_item as cc_comments %}
+{% for cc in cc_comments %}
+ {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %}
+ {% if depth >= 1 %}
+ {% include template_name %}
+ {% else %}
+ {% with depth=depth|add:1 %}
+ {% include template_name %}
+ {% endwith %}
+ {% endif %}
+ {% endwith %}
+{% endfor %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_list.html b/src/DjangoBlog-master/templates/comments/tags/comment_list.html
new file mode 100644
index 0000000..4092161
--- /dev/null
+++ b/src/DjangoBlog-master/templates/comments/tags/comment_list.html
@@ -0,0 +1,45 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/comments/tags/post_comment.html b/src/DjangoBlog-master/templates/comments/tags/post_comment.html
new file mode 100644
index 0000000..3ae5a27
--- /dev/null
+++ b/src/DjangoBlog-master/templates/comments/tags/post_comment.html
@@ -0,0 +1,33 @@
+
+
+
diff --git a/src/DjangoBlog-master/templates/oauth/bindsuccess.html b/src/DjangoBlog-master/templates/oauth/bindsuccess.html
new file mode 100644
index 0000000..4bee77c
--- /dev/null
+++ b/src/DjangoBlog-master/templates/oauth/bindsuccess.html
@@ -0,0 +1,22 @@
+{% extends 'share_layout/base.html' %}
+{% block header %}
+ {{ title }}
+{% endblock %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/oauth/oauth_applications.html b/src/DjangoBlog-master/templates/oauth/oauth_applications.html
new file mode 100644
index 0000000..a841ad2
--- /dev/null
+++ b/src/DjangoBlog-master/templates/oauth/oauth_applications.html
@@ -0,0 +1,13 @@
+{% load i18n %}
+
diff --git a/src/DjangoBlog-master/templates/oauth/require_email.html b/src/DjangoBlog-master/templates/oauth/require_email.html
new file mode 100644
index 0000000..3adef12
--- /dev/null
+++ b/src/DjangoBlog-master/templates/oauth/require_email.html
@@ -0,0 +1,46 @@
+{% extends 'share_layout/base_account.html' %}
+
+{% load static %}
+{% block content %}
+
+
+
绑定您的邮箱账号
+
+
+ {% if picture %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+ 登录
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/owntracks/show_log_dates.html b/src/DjangoBlog-master/templates/owntracks/show_log_dates.html
new file mode 100644
index 0000000..7dbba21
--- /dev/null
+++ b/src/DjangoBlog-master/templates/owntracks/show_log_dates.html
@@ -0,0 +1,17 @@
+
+
+
+
+ 记录日期
+
+
+
+
+ {% for date in results %}
+
+ {{ date }}
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/src/DjangoBlog-master/templates/owntracks/show_maps.html b/src/DjangoBlog-master/templates/owntracks/show_maps.html
new file mode 100644
index 0000000..3aeda36
--- /dev/null
+++ b/src/DjangoBlog-master/templates/owntracks/show_maps.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+ 运动轨迹
+
+
+
+
+
+
+
+
+
+
+
+
+ {# {% query article_comments parent_comment=None as parent_comments %}#} + {% for comment_item in p_comments %} + + {% with 0 as depth %} + {% include "comments/tags/comment_item_tree.html" %} + {% endwith %} + {% endfor %} + +
++