From e1fb5ed3cc90fd912097d4c27e0e9d0adfa43be6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BC=A0=E6=81=92=E7=A5=BA?= <1581408258@qq.com>
Date: Tue, 25 Nov 2025 03:02:11 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.coveragerc | 10 -
.dockerignore | 11 -
.gitattributes | 6 -
.github/ISSUE_TEMPLATE.md | 18 -
.github/workflows/codeql-analysis.yml | 47 --
.github/workflows/django.yml | 136 ----
.github/workflows/docker.yml | 43 --
.github/workflows/publish-release.yml | 39 -
.gitignore | 80 --
Dockerfile | 15 -
LICENSE | 20 -
README.md | 158 ----
accounts/__init__.py | 0
accounts/admin.py | 59 --
accounts/apps.py | 5 -
accounts/forms.py | 117 ---
accounts/migrations/0001_initial.py | 49 --
...s_remove_bloguser_created_time_and_more.py | 46 --
accounts/migrations/__init__.py | 0
accounts/models.py | 35 -
accounts/templatetags/__init__.py | 0
accounts/tests.py | 207 ------
accounts/urls.py | 28 -
accounts/user_login_backend.py | 26 -
accounts/utils.py | 49 --
accounts/views.py | 204 ------
blog/__init__.py | 0
blog/admin.py | 112 ---
blog/apps.py | 5 -
blog/context_processors.py | 43 --
blog/documents.py | 213 ------
blog/forms.py | 19 -
blog/management/__init__.py | 0
blog/management/commands/__init__.py | 0
blog/management/commands/build_index.py | 18 -
.../management/commands/build_search_words.py | 13 -
blog/management/commands/clear_cache.py | 11 -
blog/management/commands/create_testdata.py | 40 -
blog/management/commands/ping_baidu.py | 50 --
blog/management/commands/sync_user_avatar.py | 47 --
blog/middleware.py | 42 --
blog/migrations/0001_initial.py | 137 ----
...002_blogsettings_global_footer_and_more.py | 23 -
.../0003_blogsettings_comment_need_review.py | 17 -
...de_blogsettings_analytics_code_and_more.py | 27 -
...options_alter_category_options_and_more.py | 300 --------
.../0006_alter_blogsettings_options.py | 17 -
blog/migrations/__init__.py | 0
blog/models.py | 376 ----------
blog/search_indexes.py | 13 -
blog/templatetags/__init__.py | 0
blog/templatetags/blog_tags.py | 344 ---------
blog/tests.py | 232 ------
blog/urls.py | 62 --
blog/views.py | 379 ----------
comments/__init__.py | 0
comments/admin.py | 47 --
comments/apps.py | 5 -
comments/forms.py | 13 -
comments/migrations/0001_initial.py | 38 -
.../0002_alter_comment_is_enable.py | 18 -
...ns_remove_comment_created_time_and_more.py | 60 --
comments/migrations/__init__.py | 0
comments/models.py | 39 -
comments/templatetags/__init__.py | 0
comments/templatetags/comments_tags.py | 30 -
comments/tests.py | 109 ---
comments/urls.py | 11 -
comments/utils.py | 38 -
comments/views.py | 63 --
deploy/docker-compose/docker-compose.es.yml | 48 --
deploy/docker-compose/docker-compose.yml | 60 --
deploy/entrypoint.sh | 31 -
deploy/k8s/configmap.yaml | 119 ---
deploy/k8s/deployment.yaml | 274 -------
deploy/k8s/gateway.yaml | 17 -
deploy/k8s/pv.yaml | 94 ---
deploy/k8s/pvc.yaml | 60 --
deploy/k8s/service.yaml | 80 --
deploy/k8s/storageclass.yaml | 10 -
deploy/nginx.conf | 50 --
docs/README-en.md | 158 ----
docs/config-en.md | 64 --
docs/config.md | 58 --
docs/docker-en.md | 114 ---
docs/docker.md | 114 ---
docs/es.md | 28 -
docs/imgs/alipay.jpg | Bin 17961 -> 0 bytes
docs/imgs/pycharm_logo.png | Bin 132045 -> 0 bytes
docs/imgs/wechat.jpg | Bin 24722 -> 0 bytes
docs/k8s-en.md | 141 ----
docs/k8s.md | 141 ----
locale/en/LC_MESSAGES/django.mo | Bin 11097 -> 0 bytes
locale/en/LC_MESSAGES/django.po | 685 ------------------
locale/zh_Hans/LC_MESSAGES/django.mo | Bin 10321 -> 0 bytes
locale/zh_Hans/LC_MESSAGES/django.po | 667 -----------------
locale/zh_Hant/LC_MESSAGES/django.mo | Bin 10268 -> 0 bytes
locale/zh_Hant/LC_MESSAGES/django.po | 668 -----------------
manage.py | 22 -
oauth/__init__.py | 0
oauth/admin.py | 54 --
oauth/apps.py | 5 -
oauth/forms.py | 12 -
oauth/migrations/0001_initial.py | 57 --
...ptions_alter_oauthuser_options_and_more.py | 86 ---
.../0003_alter_oauthuser_nickname.py | 18 -
oauth/migrations/__init__.py | 0
oauth/models.py | 67 --
oauth/oauthmanager.py | 504 -------------
oauth/templatetags/__init__.py | 1 -
oauth/templatetags/oauth_tags.py | 22 -
oauth/tests.py | 249 -------
oauth/urls.py | 25 -
oauth/views.py | 253 -------
owntracks/__init__.py | 0
owntracks/admin.py | 7 -
owntracks/apps.py | 5 -
owntracks/migrations/0001_initial.py | 31 -
...0002_alter_owntracklog_options_and_more.py | 22 -
owntracks/migrations/__init__.py | 0
owntracks/models.py | 20 -
owntracks/tests.py | 64 --
owntracks/urls.py | 12 -
owntracks/views.py | 127 ----
plugins/__init__.py | 1 -
plugins/article_copyright/__init__.py | 1 -
plugins/article_copyright/plugin.py | 32 -
plugins/external_links/__init__.py | 1 -
plugins/external_links/plugin.py | 48 --
plugins/reading_time/__init__.py | 1 -
plugins/reading_time/plugin.py | 43 --
plugins/seo_optimizer/__init__.py | 1 -
plugins/seo_optimizer/plugin.py | 142 ----
plugins/view_count/__init__.py | 1 -
plugins/view_count/plugin.py | 18 -
requirements.txt | Bin 2554 -> 0 bytes
servermanager/MemcacheStorage.py | 32 -
servermanager/__init__.py | 0
servermanager/admin.py | 19 -
servermanager/api/__init__.py | 1 -
servermanager/api/blogapi.py | 27 -
servermanager/api/commonapi.py | 64 --
servermanager/apps.py | 5 -
servermanager/migrations/0001_initial.py | 45 --
...002_alter_emailsendlog_options_and_more.py | 32 -
servermanager/migrations/__init__.py | 0
servermanager/models.py | 33 -
servermanager/robot.py | 187 -----
servermanager/tests.py | 79 --
servermanager/urls.py | 10 -
servermanager/views.py | 1 -
templates/account/forget_password.html | 30 -
templates/account/login.html | 46 --
templates/account/registration_form.html | 29 -
templates/account/result.html | 27 -
templates/blog/article_archives.html | 60 --
templates/blog/article_detail.html | 52 --
templates/blog/article_index.html | 42 --
templates/blog/error_page.html | 45 --
templates/blog/links_list.html | 44 --
templates/blog/tags/article_info.html | 74 --
templates/blog/tags/article_meta_info.html | 59 --
templates/blog/tags/article_pagination.html | 17 -
templates/blog/tags/article_tag_list.html | 19 -
templates/blog/tags/breadcrumb.html | 19 -
templates/blog/tags/sidebar.html | 136 ----
templates/comments/tags/comment_item.html | 34 -
.../comments/tags/comment_item_tree.html | 54 --
templates/comments/tags/comment_list.html | 45 --
templates/comments/tags/post_comment.html | 33 -
templates/oauth/bindsuccess.html | 22 -
templates/oauth/oauth_applications.html | 13 -
templates/oauth/require_email.html | 46 --
templates/owntracks/show_log_dates.html | 17 -
templates/owntracks/show_maps.html | 135 ----
.../search/indexes/blog/article_text.txt | 3 -
templates/search/search.html | 66 --
templates/share_layout/adsense.html | 6 -
templates/share_layout/base.html | 123 ----
templates/share_layout/base_account.html | 47 --
templates/share_layout/footer.html | 56 --
templates/share_layout/nav.html | 30 -
templates/share_layout/nav_node.html | 19 -
183 files changed, 12035 deletions(-)
delete mode 100644 .coveragerc
delete mode 100644 .dockerignore
delete mode 100644 .gitattributes
delete mode 100644 .github/ISSUE_TEMPLATE.md
delete mode 100644 .github/workflows/codeql-analysis.yml
delete mode 100644 .github/workflows/django.yml
delete mode 100644 .github/workflows/docker.yml
delete mode 100644 .github/workflows/publish-release.yml
delete mode 100644 .gitignore
delete mode 100644 Dockerfile
delete mode 100644 LICENSE
delete mode 100644 README.md
delete mode 100644 accounts/__init__.py
delete mode 100644 accounts/admin.py
delete mode 100644 accounts/apps.py
delete mode 100644 accounts/forms.py
delete mode 100644 accounts/migrations/0001_initial.py
delete mode 100644 accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
delete mode 100644 accounts/migrations/__init__.py
delete mode 100644 accounts/models.py
delete mode 100644 accounts/templatetags/__init__.py
delete mode 100644 accounts/tests.py
delete mode 100644 accounts/urls.py
delete mode 100644 accounts/user_login_backend.py
delete mode 100644 accounts/utils.py
delete mode 100644 accounts/views.py
delete mode 100644 blog/__init__.py
delete mode 100644 blog/admin.py
delete mode 100644 blog/apps.py
delete mode 100644 blog/context_processors.py
delete mode 100644 blog/documents.py
delete mode 100644 blog/forms.py
delete mode 100644 blog/management/__init__.py
delete mode 100644 blog/management/commands/__init__.py
delete mode 100644 blog/management/commands/build_index.py
delete mode 100644 blog/management/commands/build_search_words.py
delete mode 100644 blog/management/commands/clear_cache.py
delete mode 100644 blog/management/commands/create_testdata.py
delete mode 100644 blog/management/commands/ping_baidu.py
delete mode 100644 blog/management/commands/sync_user_avatar.py
delete mode 100644 blog/middleware.py
delete mode 100644 blog/migrations/0001_initial.py
delete mode 100644 blog/migrations/0002_blogsettings_global_footer_and_more.py
delete mode 100644 blog/migrations/0003_blogsettings_comment_need_review.py
delete mode 100644 blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
delete mode 100644 blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
delete mode 100644 blog/migrations/0006_alter_blogsettings_options.py
delete mode 100644 blog/migrations/__init__.py
delete mode 100644 blog/models.py
delete mode 100644 blog/search_indexes.py
delete mode 100644 blog/templatetags/__init__.py
delete mode 100644 blog/templatetags/blog_tags.py
delete mode 100644 blog/tests.py
delete mode 100644 blog/urls.py
delete mode 100644 blog/views.py
delete mode 100644 comments/__init__.py
delete mode 100644 comments/admin.py
delete mode 100644 comments/apps.py
delete mode 100644 comments/forms.py
delete mode 100644 comments/migrations/0001_initial.py
delete mode 100644 comments/migrations/0002_alter_comment_is_enable.py
delete mode 100644 comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
delete mode 100644 comments/migrations/__init__.py
delete mode 100644 comments/models.py
delete mode 100644 comments/templatetags/__init__.py
delete mode 100644 comments/templatetags/comments_tags.py
delete mode 100644 comments/tests.py
delete mode 100644 comments/urls.py
delete mode 100644 comments/utils.py
delete mode 100644 comments/views.py
delete mode 100644 deploy/docker-compose/docker-compose.es.yml
delete mode 100644 deploy/docker-compose/docker-compose.yml
delete mode 100644 deploy/entrypoint.sh
delete mode 100644 deploy/k8s/configmap.yaml
delete mode 100644 deploy/k8s/deployment.yaml
delete mode 100644 deploy/k8s/gateway.yaml
delete mode 100644 deploy/k8s/pv.yaml
delete mode 100644 deploy/k8s/pvc.yaml
delete mode 100644 deploy/k8s/service.yaml
delete mode 100644 deploy/k8s/storageclass.yaml
delete mode 100644 deploy/nginx.conf
delete mode 100644 docs/README-en.md
delete mode 100644 docs/config-en.md
delete mode 100644 docs/config.md
delete mode 100644 docs/docker-en.md
delete mode 100644 docs/docker.md
delete mode 100644 docs/es.md
delete mode 100644 docs/imgs/alipay.jpg
delete mode 100644 docs/imgs/pycharm_logo.png
delete mode 100644 docs/imgs/wechat.jpg
delete mode 100644 docs/k8s-en.md
delete mode 100644 docs/k8s.md
delete mode 100644 locale/en/LC_MESSAGES/django.mo
delete mode 100644 locale/en/LC_MESSAGES/django.po
delete mode 100644 locale/zh_Hans/LC_MESSAGES/django.mo
delete mode 100644 locale/zh_Hans/LC_MESSAGES/django.po
delete mode 100644 locale/zh_Hant/LC_MESSAGES/django.mo
delete mode 100644 locale/zh_Hant/LC_MESSAGES/django.po
delete mode 100644 manage.py
delete mode 100644 oauth/__init__.py
delete mode 100644 oauth/admin.py
delete mode 100644 oauth/apps.py
delete mode 100644 oauth/forms.py
delete mode 100644 oauth/migrations/0001_initial.py
delete mode 100644 oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
delete mode 100644 oauth/migrations/0003_alter_oauthuser_nickname.py
delete mode 100644 oauth/migrations/__init__.py
delete mode 100644 oauth/models.py
delete mode 100644 oauth/oauthmanager.py
delete mode 100644 oauth/templatetags/__init__.py
delete mode 100644 oauth/templatetags/oauth_tags.py
delete mode 100644 oauth/tests.py
delete mode 100644 oauth/urls.py
delete mode 100644 oauth/views.py
delete mode 100644 owntracks/__init__.py
delete mode 100644 owntracks/admin.py
delete mode 100644 owntracks/apps.py
delete mode 100644 owntracks/migrations/0001_initial.py
delete mode 100644 owntracks/migrations/0002_alter_owntracklog_options_and_more.py
delete mode 100644 owntracks/migrations/__init__.py
delete mode 100644 owntracks/models.py
delete mode 100644 owntracks/tests.py
delete mode 100644 owntracks/urls.py
delete mode 100644 owntracks/views.py
delete mode 100644 plugins/__init__.py
delete mode 100644 plugins/article_copyright/__init__.py
delete mode 100644 plugins/article_copyright/plugin.py
delete mode 100644 plugins/external_links/__init__.py
delete mode 100644 plugins/external_links/plugin.py
delete mode 100644 plugins/reading_time/__init__.py
delete mode 100644 plugins/reading_time/plugin.py
delete mode 100644 plugins/seo_optimizer/__init__.py
delete mode 100644 plugins/seo_optimizer/plugin.py
delete mode 100644 plugins/view_count/__init__.py
delete mode 100644 plugins/view_count/plugin.py
delete mode 100644 requirements.txt
delete mode 100644 servermanager/MemcacheStorage.py
delete mode 100644 servermanager/__init__.py
delete mode 100644 servermanager/admin.py
delete mode 100644 servermanager/api/__init__.py
delete mode 100644 servermanager/api/blogapi.py
delete mode 100644 servermanager/api/commonapi.py
delete mode 100644 servermanager/apps.py
delete mode 100644 servermanager/migrations/0001_initial.py
delete mode 100644 servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
delete mode 100644 servermanager/migrations/__init__.py
delete mode 100644 servermanager/models.py
delete mode 100644 servermanager/robot.py
delete mode 100644 servermanager/tests.py
delete mode 100644 servermanager/urls.py
delete mode 100644 servermanager/views.py
delete mode 100644 templates/account/forget_password.html
delete mode 100644 templates/account/login.html
delete mode 100644 templates/account/registration_form.html
delete mode 100644 templates/account/result.html
delete mode 100644 templates/blog/article_archives.html
delete mode 100644 templates/blog/article_detail.html
delete mode 100644 templates/blog/article_index.html
delete mode 100644 templates/blog/error_page.html
delete mode 100644 templates/blog/links_list.html
delete mode 100644 templates/blog/tags/article_info.html
delete mode 100644 templates/blog/tags/article_meta_info.html
delete mode 100644 templates/blog/tags/article_pagination.html
delete mode 100644 templates/blog/tags/article_tag_list.html
delete mode 100644 templates/blog/tags/breadcrumb.html
delete mode 100644 templates/blog/tags/sidebar.html
delete mode 100644 templates/comments/tags/comment_item.html
delete mode 100644 templates/comments/tags/comment_item_tree.html
delete mode 100644 templates/comments/tags/comment_list.html
delete mode 100644 templates/comments/tags/post_comment.html
delete mode 100644 templates/oauth/bindsuccess.html
delete mode 100644 templates/oauth/oauth_applications.html
delete mode 100644 templates/oauth/require_email.html
delete mode 100644 templates/owntracks/show_log_dates.html
delete mode 100644 templates/owntracks/show_maps.html
delete mode 100644 templates/search/indexes/blog/article_text.txt
delete mode 100644 templates/search/search.html
delete mode 100644 templates/share_layout/adsense.html
delete mode 100644 templates/share_layout/base.html
delete mode 100644 templates/share_layout/base_account.html
delete mode 100644 templates/share_layout/footer.html
delete mode 100644 templates/share_layout/nav.html
delete mode 100644 templates/share_layout/nav_node.html
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 9757484..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,10 +0,0 @@
-[run]
-source = .
-include = *.py
-omit =
- *migrations*
- *tests*
- *.html
- *whoosh_cn_backend*
- *settings.py*
- *venv*
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 2818c38..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,11 +0,0 @@
-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/.gitattributes b/.gitattributes
deleted file mode 100644
index fd52ece..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,6 +0,0 @@
-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/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 2b5b7aa..0000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-**我确定我已经查看了** (标注`[ ]`为`[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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 6b76522..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: "CodeQL"
-
-on:
- push:
- branches:
- - master
- - dev
- paths-ignore:
- - '**/*.md'
- - '**/*.css'
- - '**/*.js'
- - '**/*.yml'
- - '**/*.txt'
- pull_request:
- branches:
- - master
- - dev
- paths-ignore:
- - '**/*.md'
- - '**/*.css'
- - '**/*.js'
- - '**/*.yml'
- - '**/*.txt'
- schedule:
- - cron: '30 1 * * 0'
-
-
-jobs:
- CodeQL-Build:
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- actions: read
- contents: read
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - name: Initialize CodeQL
- uses: github/codeql-action/init@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/.github/workflows/django.yml b/.github/workflows/django.yml
deleted file mode 100644
index 94baea9..0000000
--- a/.github/workflows/django.yml
+++ /dev/null
@@ -1,136 +0,0 @@
-name: Django CI
-
-on:
- push:
- branches:
- - master
- - dev
- paths-ignore:
- - '**/*.md'
- - '**/*.css'
- - '**/*.js'
- pull_request:
- branches:
- - master
- - dev
- paths-ignore:
- - '**/*.md'
- - '**/*.css'
- - '**/*.js'
-
-jobs:
- 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/.github/workflows/docker.yml b/.github/workflows/docker.yml
deleted file mode 100644
index a312e2f..0000000
--- a/.github/workflows/docker.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-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/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
deleted file mode 100644
index 5eb0853..0000000
--- a/.github/workflows/publish-release.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: publish release
-
-on:
- release:
- types: [ published ]
-
-jobs:
- docker:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v3
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@v3
- with:
- images: name/app
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
- - name: Login to DockerHub
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Build and push
- uses: docker/build-push-action@v3
- with:
- context: .
- push: true
- platforms: |
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/arm/v6
- linux/386
- tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 3015816..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,80 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-
-# Translations
-*.pot
-
-# Django stuff:
-*.log
-logs/
-
-# Sphinx documentation
-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/Dockerfile b/Dockerfile
deleted file mode 100644
index 80b46ac..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,15 +0,0 @@
-FROM python:3.11
-ENV PYTHONUNBUFFERED 1
-WORKDIR /code/djangoblog/
-RUN apt-get update && \
- apt-get install default-libmysqlclient-dev gettext -y && \
- rm -rf /var/lib/apt/lists/*
-ADD requirements.txt requirements.txt
-RUN pip install --upgrade pip && \
- pip install --no-cache-dir -r requirements.txt && \
- pip install --no-cache-dir gunicorn[gevent] && \
- pip cache purge
-
-ADD . .
-RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
-ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 3b08474..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2025 车亮亮
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index 56aa4cc..0000000
--- a/README.md
+++ /dev/null
@@ -1,158 +0,0 @@
-# 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/accounts/__init__.py b/accounts/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/accounts/admin.py b/accounts/admin.py
deleted file mode 100644
index 32e483c..0000000
--- a/accounts/admin.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from django import forms
-from django.contrib.auth.admin import UserAdmin
-from django.contrib.auth.forms import UserChangeForm
-from django.contrib.auth.forms import UsernameField
-from django.utils.translation import gettext_lazy as _
-
-# Register your models here.
-from .models import BlogUser
-
-
-class BlogUserCreationForm(forms.ModelForm):
- password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
- password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
-
- class Meta:
- model = BlogUser
- fields = ('email',)
-
- def clean_password2(self):
- # Check that the two password entries match
- password1 = self.cleaned_data.get("password1")
- password2 = self.cleaned_data.get("password2")
- if password1 and password2 and password1 != password2:
- raise forms.ValidationError(_("passwords do not match"))
- return password2
-
- def save(self, commit=True):
- # Save the provided password in hashed format
- user = super().save(commit=False)
- user.set_password(self.cleaned_data["password1"])
- if commit:
- user.source = 'adminsite'
- user.save()
- return user
-
-
-class BlogUserChangeForm(UserChangeForm):
- class Meta:
- model = BlogUser
- fields = '__all__'
- field_classes = {'username': UsernameField}
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
-
-class BlogUserAdmin(UserAdmin):
- form = BlogUserChangeForm
- add_form = BlogUserCreationForm
- list_display = (
- 'id',
- 'nickname',
- 'username',
- 'email',
- 'last_login',
- 'date_joined',
- 'source')
- list_display_links = ('id', 'username')
- ordering = ('-id',)
diff --git a/accounts/apps.py b/accounts/apps.py
deleted file mode 100644
index 9b3fc5a..0000000
--- a/accounts/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class AccountsConfig(AppConfig):
- name = 'accounts'
diff --git a/accounts/forms.py b/accounts/forms.py
deleted file mode 100644
index fce4137..0000000
--- a/accounts/forms.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from django import forms
-from django.contrib.auth import get_user_model, password_validation
-from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
-from django.core.exceptions import ValidationError
-from django.forms import widgets
-from django.utils.translation import gettext_lazy as _
-from . import utils
-from .models import BlogUser
-
-
-class LoginForm(AuthenticationForm):
- def __init__(self, *args, **kwargs):
- super(LoginForm, self).__init__(*args, **kwargs)
- self.fields['username'].widget = widgets.TextInput(
- attrs={'placeholder': "username", "class": "form-control"})
- self.fields['password'].widget = widgets.PasswordInput(
- attrs={'placeholder': "password", "class": "form-control"})
-
-
-class RegisterForm(UserCreationForm):
- def __init__(self, *args, **kwargs):
- super(RegisterForm, self).__init__(*args, **kwargs)
-
- self.fields['username'].widget = widgets.TextInput(
- attrs={'placeholder': "username", "class": "form-control"})
- self.fields['email'].widget = widgets.EmailInput(
- attrs={'placeholder': "email", "class": "form-control"})
- self.fields['password1'].widget = widgets.PasswordInput(
- attrs={'placeholder': "password", "class": "form-control"})
- self.fields['password2'].widget = widgets.PasswordInput(
- attrs={'placeholder': "repeat password", "class": "form-control"})
-
- def clean_email(self):
- email = self.cleaned_data['email']
- if get_user_model().objects.filter(email=email).exists():
- raise ValidationError(_("email already exists"))
- return email
-
- class Meta:
- model = get_user_model()
- fields = ("username", "email")
-
-
-class ForgetPasswordForm(forms.Form):
- new_password1 = forms.CharField(
- label=_("New password"),
- widget=forms.PasswordInput(
- attrs={
- "class": "form-control",
- 'placeholder': _("New password")
- }
- ),
- )
-
- new_password2 = forms.CharField(
- label="确认密码",
- widget=forms.PasswordInput(
- attrs={
- "class": "form-control",
- 'placeholder': _("Confirm password")
- }
- ),
- )
-
- email = forms.EmailField(
- label='邮箱',
- widget=forms.TextInput(
- attrs={
- 'class': 'form-control',
- 'placeholder': _("Email")
- }
- ),
- )
-
- code = forms.CharField(
- label=_('Code'),
- widget=forms.TextInput(
- attrs={
- 'class': 'form-control',
- 'placeholder': _("Code")
- }
- ),
- )
-
- def clean_new_password2(self):
- password1 = self.data.get("new_password1")
- password2 = self.data.get("new_password2")
- if password1 and password2 and password1 != password2:
- raise ValidationError(_("passwords do not match"))
- password_validation.validate_password(password2)
-
- return password2
-
- def clean_email(self):
- user_email = self.cleaned_data.get("email")
- if not BlogUser.objects.filter(
- email=user_email
- ).exists():
- # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
- raise ValidationError(_("email does not exist"))
- return user_email
-
- def clean_code(self):
- code = self.cleaned_data.get("code")
- error = utils.verify(
- email=self.cleaned_data.get("email"),
- code=code,
- )
- if error:
- raise ValidationError(error)
- return code
-
-
-class ForgetPasswordCodeForm(forms.Form):
- email = forms.EmailField(
- label=_('Email'),
- )
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
deleted file mode 100644
index d2fbcab..0000000
--- a/accounts/migrations/0001_initial.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Generated by Django 4.1.7 on 2023-03-02 07:14
-
-import django.contrib.auth.models
-import django.contrib.auth.validators
-from django.db import migrations, models
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('auth', '0012_alter_user_first_name_max_length'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='BlogUser',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('password', models.CharField(max_length=128, verbose_name='password')),
- ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
- ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
- ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
- ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
- ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
- ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
- ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
- ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
- ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
- ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
- ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
- ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
- ],
- options={
- 'verbose_name': '用户',
- 'verbose_name_plural': '用户',
- 'ordering': ['-id'],
- 'get_latest_by': 'id',
- },
- managers=[
- ('objects', django.contrib.auth.models.UserManager()),
- ],
- ),
- ]
diff --git a/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
deleted file mode 100644
index 1a9f509..0000000
--- a/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Generated by Django 4.2.5 on 2023-09-06 13:13
-
-from django.db import migrations, models
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('accounts', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='bloguser',
- options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
- ),
- migrations.RemoveField(
- model_name='bloguser',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='bloguser',
- name='last_mod_time',
- ),
- migrations.AddField(
- model_name='bloguser',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='bloguser',
- name='last_modify_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
- ),
- migrations.AlterField(
- model_name='bloguser',
- name='nickname',
- field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
- ),
- migrations.AlterField(
- model_name='bloguser',
- name='source',
- field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
- ),
- ]
diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/accounts/models.py b/accounts/models.py
deleted file mode 100644
index 3baddbb..0000000
--- a/accounts/models.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from django.contrib.auth.models import AbstractUser
-from django.db import models
-from django.urls import reverse
-from django.utils.timezone import now
-from django.utils.translation import gettext_lazy as _
-from djangoblog.utils import get_current_site
-
-
-# Create your models here.
-
-class BlogUser(AbstractUser):
- nickname = models.CharField(_('nick name'), max_length=100, blank=True)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_modify_time = models.DateTimeField(_('last modify time'), default=now)
- source = models.CharField(_('create source'), max_length=100, blank=True)
-
- def get_absolute_url(self):
- return reverse(
- 'blog:author_detail', kwargs={
- 'author_name': self.username})
-
- def __str__(self):
- return self.email
-
- def get_full_url(self):
- site = get_current_site().domain
- url = "https://{site}{path}".format(site=site,
- path=self.get_absolute_url())
- return url
-
- class Meta:
- ordering = ['-id']
- verbose_name = _('user')
- verbose_name_plural = verbose_name
- get_latest_by = 'id'
diff --git a/accounts/templatetags/__init__.py b/accounts/templatetags/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/accounts/tests.py b/accounts/tests.py
deleted file mode 100644
index 6893411..0000000
--- a/accounts/tests.py
+++ /dev/null
@@ -1,207 +0,0 @@
-from django.test import Client, RequestFactory, TestCase
-from django.urls import reverse
-from django.utils import timezone
-from django.utils.translation import gettext_lazy as _
-
-from accounts.models import BlogUser
-from blog.models import Article, Category
-from djangoblog.utils import *
-from . import utils
-
-
-# Create your tests here.
-
-class AccountTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
- self.blog_user = BlogUser.objects.create_user(
- username="test",
- email="admin@admin.com",
- password="12345678"
- )
- self.new_test = "xxx123--="
-
- def test_validate_account(self):
- site = get_current_site().domain
- user = BlogUser.objects.create_superuser(
- email="liangliangyy1@gmail.com",
- username="liangliangyy1",
- password="qwer!@#$ggg")
- testuser = BlogUser.objects.get(username='liangliangyy1')
-
- loginresult = self.client.login(
- username='liangliangyy1',
- password='qwer!@#$ggg')
- self.assertEqual(loginresult, True)
- response = self.client.get('/admin/')
- self.assertEqual(response.status_code, 200)
-
- category = Category()
- category.name = "categoryaaa"
- category.creation_time = timezone.now()
- category.last_modify_time = timezone.now()
- category.save()
-
- article = Article()
- article.title = "nicetitleaaa"
- article.body = "nicecontentaaa"
- article.author = user
- article.category = category
- article.type = 'a'
- article.status = 'p'
- article.save()
-
- response = self.client.get(article.get_admin_url())
- self.assertEqual(response.status_code, 200)
-
- def test_validate_register(self):
- self.assertEquals(
- 0, len(
- BlogUser.objects.filter(
- email='user123@user.com')))
- response = self.client.post(reverse('account:register'), {
- 'username': 'user1233',
- 'email': 'user123@user.com',
- 'password1': 'password123!q@wE#R$T',
- 'password2': 'password123!q@wE#R$T',
- })
- self.assertEquals(
- 1, len(
- BlogUser.objects.filter(
- email='user123@user.com')))
- user = BlogUser.objects.filter(email='user123@user.com')[0]
- sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
- path = reverse('accounts:result')
- url = '{path}?type=validation&id={id}&sign={sign}'.format(
- path=path, id=user.id, sign=sign)
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
-
- self.client.login(username='user1233', password='password123!q@wE#R$T')
- user = BlogUser.objects.filter(email='user123@user.com')[0]
- user.is_superuser = True
- user.is_staff = True
- user.save()
- delete_sidebar_cache()
- category = Category()
- category.name = "categoryaaa"
- category.creation_time = timezone.now()
- category.last_modify_time = timezone.now()
- category.save()
-
- article = Article()
- article.category = category
- article.title = "nicetitle333"
- article.body = "nicecontentttt"
- article.author = user
-
- article.type = 'a'
- article.status = 'p'
- article.save()
-
- response = self.client.get(article.get_admin_url())
- self.assertEqual(response.status_code, 200)
-
- response = self.client.get(reverse('account:logout'))
- self.assertIn(response.status_code, [301, 302, 200])
-
- response = self.client.get(article.get_admin_url())
- self.assertIn(response.status_code, [301, 302, 200])
-
- response = self.client.post(reverse('account:login'), {
- 'username': 'user1233',
- 'password': 'password123'
- })
- self.assertIn(response.status_code, [301, 302, 200])
-
- response = self.client.get(article.get_admin_url())
- self.assertIn(response.status_code, [301, 302, 200])
-
- def test_verify_email_code(self):
- to_email = "admin@admin.com"
- code = generate_code()
- utils.set_code(to_email, code)
- utils.send_verify_email(to_email, code)
-
- err = utils.verify("admin@admin.com", code)
- self.assertEqual(err, None)
-
- err = utils.verify("admin@123.com", code)
- self.assertEqual(type(err), str)
-
- def test_forget_password_email_code_success(self):
- resp = self.client.post(
- path=reverse("account:forget_password_code"),
- data=dict(email="admin@admin.com")
- )
-
- self.assertEqual(resp.status_code, 200)
- self.assertEqual(resp.content.decode("utf-8"), "ok")
-
- def test_forget_password_email_code_fail(self):
- resp = self.client.post(
- path=reverse("account:forget_password_code"),
- data=dict()
- )
- self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
-
- resp = self.client.post(
- path=reverse("account:forget_password_code"),
- data=dict(email="admin@com")
- )
- self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
-
- def test_forget_password_email_success(self):
- code = generate_code()
- utils.set_code(self.blog_user.email, code)
- data = dict(
- new_password1=self.new_test,
- new_password2=self.new_test,
- email=self.blog_user.email,
- code=code,
- )
- resp = self.client.post(
- path=reverse("account:forget_password"),
- data=data
- )
- self.assertEqual(resp.status_code, 302)
-
- # 验证用户密码是否修改成功
- blog_user = BlogUser.objects.filter(
- email=self.blog_user.email,
- ).first() # type: BlogUser
- self.assertNotEqual(blog_user, None)
- self.assertEqual(blog_user.check_password(data["new_password1"]), True)
-
- def test_forget_password_email_not_user(self):
- data = dict(
- new_password1=self.new_test,
- new_password2=self.new_test,
- email="123@123.com",
- code="123456",
- )
- resp = self.client.post(
- path=reverse("account:forget_password"),
- data=data
- )
-
- self.assertEqual(resp.status_code, 200)
-
-
- def test_forget_password_email_code_error(self):
- code = generate_code()
- utils.set_code(self.blog_user.email, code)
- data = dict(
- new_password1=self.new_test,
- new_password2=self.new_test,
- email=self.blog_user.email,
- code="111111",
- )
- resp = self.client.post(
- path=reverse("account:forget_password"),
- data=data
- )
-
- self.assertEqual(resp.status_code, 200)
-
diff --git a/accounts/urls.py b/accounts/urls.py
deleted file mode 100644
index 107a801..0000000
--- a/accounts/urls.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from django.urls import path
-from django.urls import re_path
-
-from . import views
-from .forms import LoginForm
-
-app_name = "accounts"
-
-urlpatterns = [re_path(r'^login/$',
- views.LoginView.as_view(success_url='/'),
- name='login',
- kwargs={'authentication_form': LoginForm}),
- re_path(r'^register/$',
- views.RegisterView.as_view(success_url="/"),
- name='register'),
- re_path(r'^logout/$',
- views.LogoutView.as_view(),
- name='logout'),
- path(r'account/result.html',
- views.account_result,
- name='result'),
- re_path(r'^forget_password/$',
- views.ForgetPasswordView.as_view(),
- name='forget_password'),
- re_path(r'^forget_password_code/$',
- views.ForgetPasswordEmailCode.as_view(),
- name='forget_password_code'),
- ]
diff --git a/accounts/user_login_backend.py b/accounts/user_login_backend.py
deleted file mode 100644
index 73cdca1..0000000
--- a/accounts/user_login_backend.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.contrib.auth.backends import ModelBackend
-
-
-class EmailOrUsernameModelBackend(ModelBackend):
- """
- 允许使用用户名或邮箱登录
- """
-
- def authenticate(self, request, username=None, password=None, **kwargs):
- if '@' in username:
- kwargs = {'email': username}
- else:
- kwargs = {'username': username}
- try:
- user = get_user_model().objects.get(**kwargs)
- if user.check_password(password):
- return user
- except get_user_model().DoesNotExist:
- return None
-
- def get_user(self, username):
- try:
- return get_user_model().objects.get(pk=username)
- except get_user_model().DoesNotExist:
- return None
diff --git a/accounts/utils.py b/accounts/utils.py
deleted file mode 100644
index 4b94bdf..0000000
--- a/accounts/utils.py
+++ /dev/null
@@ -1,49 +0,0 @@
-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/accounts/views.py b/accounts/views.py
deleted file mode 100644
index ae67aec..0000000
--- a/accounts/views.py
+++ /dev/null
@@ -1,204 +0,0 @@
-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/blog/__init__.py b/blog/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/admin.py b/blog/admin.py
deleted file mode 100644
index 46c3420..0000000
--- a/blog/admin.py
+++ /dev/null
@@ -1,112 +0,0 @@
-from django import forms
-from django.contrib import admin
-from django.contrib.auth import get_user_model
-from django.urls import reverse
-from django.utils.html import format_html
-from django.utils.translation import gettext_lazy as _
-
-# Register your models here.
-from .models import Article
-
-
-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/blog/apps.py b/blog/apps.py
deleted file mode 100644
index 7930587..0000000
--- a/blog/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class BlogConfig(AppConfig):
- name = 'blog'
diff --git a/blog/context_processors.py b/blog/context_processors.py
deleted file mode 100644
index 73e3088..0000000
--- a/blog/context_processors.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import logging
-
-from django.utils import timezone
-
-from djangoblog.utils import cache, get_blog_setting
-from .models import Category, Article
-
-logger = logging.getLogger(__name__)
-
-
-def seo_processor(requests):
- key = 'seo_processor'
- value = cache.get(key)
- if value:
- return value
- else:
- logger.info('set processor cache.')
- setting = get_blog_setting()
- value = {
- 'SITE_NAME': setting.site_name,
- 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
- 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
- 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
- 'SITE_DESCRIPTION': setting.site_description,
- 'SITE_KEYWORDS': setting.site_keywords,
- 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
- 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
- 'nav_category_list': Category.objects.all(),
- 'nav_pages': Article.objects.filter(
- type='p',
- status='p'),
- 'OPEN_SITE_COMMENT': setting.open_site_comment,
- 'BEIAN_CODE': setting.beian_code,
- 'ANALYTICS_CODE': setting.analytics_code,
- "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
- "SHOW_GONGAN_CODE": setting.show_gongan_code,
- "CURRENT_YEAR": timezone.now().year,
- "GLOBAL_HEADER": setting.global_header,
- "GLOBAL_FOOTER": setting.global_footer,
- "COMMENT_NEED_REVIEW": setting.comment_need_review,
- }
- cache.set(key, value, 60 * 60 * 10)
- return value
diff --git a/blog/documents.py b/blog/documents.py
deleted file mode 100644
index 0f1db7b..0000000
--- a/blog/documents.py
+++ /dev/null
@@ -1,213 +0,0 @@
-import time
-
-import elasticsearch.client
-from django.conf import settings
-from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
-from elasticsearch_dsl.connections import connections
-
-from blog.models import Article
-
-ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
-
-if ELASTICSEARCH_ENABLED:
- connections.create_connection(
- hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
- from elasticsearch import Elasticsearch
-
- es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- from elasticsearch.client import IngestClient
-
- c = IngestClient(es)
- try:
- c.get_pipeline('geoip')
- except elasticsearch.exceptions.NotFoundError:
- c.put_pipeline('geoip', body='''{
- "description" : "Add geoip info",
- "processors" : [
- {
- "geoip" : {
- "field" : "ip"
- }
- }
- ]
- }''')
-
-
-class GeoIp(InnerDoc):
- continent_name = Keyword()
- country_iso_code = Keyword()
- country_name = Keyword()
- location = GeoPoint()
-
-
-class UserAgentBrowser(InnerDoc):
- Family = Keyword()
- Version = Keyword()
-
-
-class UserAgentOS(UserAgentBrowser):
- pass
-
-
-class UserAgentDevice(InnerDoc):
- Family = Keyword()
- Brand = Keyword()
- Model = Keyword()
-
-
-class UserAgent(InnerDoc):
- browser = Object(UserAgentBrowser, required=False)
- os = Object(UserAgentOS, required=False)
- device = Object(UserAgentDevice, required=False)
- string = Text()
- is_bot = Boolean()
-
-
-class ElapsedTimeDocument(Document):
- url = Keyword()
- time_taken = Long()
- log_datetime = Date()
- ip = Keyword()
- geoip = Object(GeoIp, required=False)
- useragent = Object(UserAgent, required=False)
-
- class Index:
- name = 'performance'
- settings = {
- "number_of_shards": 1,
- "number_of_replicas": 0
- }
-
- class Meta:
- doc_type = 'ElapsedTime'
-
-
-class ElaspedTimeDocumentManager:
- @staticmethod
- def build_index():
- from elasticsearch import Elasticsearch
- client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- res = client.indices.exists(index="performance")
- if not res:
- ElapsedTimeDocument.init()
-
- @staticmethod
- def delete_index():
- from elasticsearch import Elasticsearch
- es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- es.indices.delete(index='performance', ignore=[400, 404])
-
- @staticmethod
- def create(url, time_taken, log_datetime, useragent, ip):
- ElaspedTimeDocumentManager.build_index()
- ua = UserAgent()
- ua.browser = UserAgentBrowser()
- ua.browser.Family = useragent.browser.family
- ua.browser.Version = useragent.browser.version_string
-
- ua.os = UserAgentOS()
- ua.os.Family = useragent.os.family
- ua.os.Version = useragent.os.version_string
-
- ua.device = UserAgentDevice()
- ua.device.Family = useragent.device.family
- ua.device.Brand = useragent.device.brand
- ua.device.Model = useragent.device.model
- ua.string = useragent.ua_string
- ua.is_bot = useragent.is_bot
-
- doc = ElapsedTimeDocument(
- meta={
- 'id': int(
- round(
- time.time() *
- 1000))
- },
- url=url,
- time_taken=time_taken,
- log_datetime=log_datetime,
- useragent=ua, ip=ip)
- doc.save(pipeline="geoip")
-
-
-class ArticleDocument(Document):
- body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
- title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
- author = Object(properties={
- 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
- 'id': Integer()
- })
- category = Object(properties={
- 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
- 'id': Integer()
- })
- tags = Object(properties={
- 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
- 'id': Integer()
- })
-
- pub_time = Date()
- status = Text()
- comment_status = Text()
- type = Text()
- views = Integer()
- article_order = Integer()
-
- class Index:
- name = 'blog'
- settings = {
- "number_of_shards": 1,
- "number_of_replicas": 0
- }
-
- class Meta:
- doc_type = 'Article'
-
-
-class ArticleDocumentManager():
-
- def __init__(self):
- self.create_index()
-
- def create_index(self):
- ArticleDocument.init()
-
- def delete_index(self):
- from elasticsearch import Elasticsearch
- es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
- es.indices.delete(index='blog', ignore=[400, 404])
-
- def convert_to_doc(self, articles):
- return [
- ArticleDocument(
- meta={
- 'id': article.id},
- body=article.body,
- title=article.title,
- author={
- 'nickname': article.author.username,
- 'id': article.author.id},
- category={
- 'name': article.category.name,
- 'id': article.category.id},
- tags=[
- {
- 'name': t.name,
- 'id': t.id} for t in article.tags.all()],
- pub_time=article.pub_time,
- status=article.status,
- comment_status=article.comment_status,
- type=article.type,
- views=article.views,
- article_order=article.article_order) for article in articles]
-
- def rebuild(self, articles=None):
- ArticleDocument.init()
- articles = articles if articles else Article.objects.all()
- docs = self.convert_to_doc(articles)
- for doc in docs:
- doc.save()
-
- def update_docs(self, docs):
- for doc in docs:
- doc.save()
diff --git a/blog/forms.py b/blog/forms.py
deleted file mode 100644
index 715be76..0000000
--- a/blog/forms.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import logging
-
-from django import forms
-from haystack.forms import SearchForm
-
-logger = logging.getLogger(__name__)
-
-
-class BlogSearchForm(SearchForm):
- querydata = forms.CharField(required=True)
-
- def search(self):
- datas = super(BlogSearchForm, self).search()
- if not self.is_valid():
- return self.no_query_found()
-
- if self.cleaned_data['querydata']:
- logger.info(self.cleaned_data['querydata'])
- return datas
diff --git a/blog/management/__init__.py b/blog/management/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/management/commands/__init__.py b/blog/management/commands/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/management/commands/build_index.py b/blog/management/commands/build_index.py
deleted file mode 100644
index 3c4acd7..0000000
--- a/blog/management/commands/build_index.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
- ELASTICSEARCH_ENABLED
-
-
-# TODO 参数化
-class Command(BaseCommand):
- help = 'build search index'
-
- def handle(self, *args, **options):
- if ELASTICSEARCH_ENABLED:
- ElaspedTimeDocumentManager.build_index()
- manager = ElapsedTimeDocument()
- manager.init()
- manager = ArticleDocumentManager()
- manager.delete_index()
- manager.rebuild()
diff --git a/blog/management/commands/build_search_words.py b/blog/management/commands/build_search_words.py
deleted file mode 100644
index cfe7e0d..0000000
--- a/blog/management/commands/build_search_words.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from blog.models import Tag, Category
-
-
-# TODO 参数化
-class Command(BaseCommand):
- help = 'build search words'
-
- def handle(self, *args, **options):
- datas = set([t.name for t in Tag.objects.all()] +
- [t.name for t in Category.objects.all()])
- print('\n'.join(datas))
diff --git a/blog/management/commands/clear_cache.py b/blog/management/commands/clear_cache.py
deleted file mode 100644
index 0d66172..0000000
--- a/blog/management/commands/clear_cache.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from djangoblog.utils import cache
-
-
-class Command(BaseCommand):
- help = 'clear the whole cache'
-
- def handle(self, *args, **options):
- cache.clear()
- self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
diff --git a/blog/management/commands/create_testdata.py b/blog/management/commands/create_testdata.py
deleted file mode 100644
index 675d2ba..0000000
--- a/blog/management/commands/create_testdata.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.contrib.auth.hashers import make_password
-from django.core.management.base import BaseCommand
-
-from blog.models import Article, Tag, Category
-
-
-class Command(BaseCommand):
- help = 'create test datas'
-
- def handle(self, *args, **options):
- user = get_user_model().objects.get_or_create(
- email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
-
- pcategory = Category.objects.get_or_create(
- name='我是父类目', parent_category=None)[0]
-
- category = Category.objects.get_or_create(
- name='子类目', parent_category=pcategory)[0]
-
- category.save()
- basetag = Tag()
- basetag.name = "标签"
- basetag.save()
- for i in range(1, 20):
- article = Article.objects.get_or_create(
- category=category,
- title='nice title ' + str(i),
- body='nice content ' + str(i),
- author=user)[0]
- tag = Tag()
- tag.name = "标签" + str(i)
- tag.save()
- article.tags.add(tag)
- article.tags.add(basetag)
- article.save()
-
- from djangoblog.utils import cache
- cache.clear()
- self.stdout.write(self.style.SUCCESS('created test datas \n'))
diff --git a/blog/management/commands/ping_baidu.py b/blog/management/commands/ping_baidu.py
deleted file mode 100644
index 2c7fbdd..0000000
--- a/blog/management/commands/ping_baidu.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from djangoblog.spider_notify import SpiderNotify
-from djangoblog.utils import get_current_site
-from blog.models import Article, Tag, Category
-
-site = get_current_site().domain
-
-
-class Command(BaseCommand):
- help = 'notify baidu url'
-
- def add_arguments(self, parser):
- parser.add_argument(
- 'data_type',
- type=str,
- choices=[
- 'all',
- 'article',
- 'tag',
- 'category'],
- help='article : all article,tag : all tag,category: all category,all: All of these')
-
- def get_full_url(self, path):
- url = "https://{site}{path}".format(site=site, path=path)
- return url
-
- def handle(self, *args, **options):
- type = options['data_type']
- self.stdout.write('start get %s' % type)
-
- urls = []
- if type == 'article' or type == 'all':
- for article in Article.objects.filter(status='p'):
- urls.append(article.get_full_url())
- if type == 'tag' or type == 'all':
- for tag in Tag.objects.all():
- url = tag.get_absolute_url()
- urls.append(self.get_full_url(url))
- if type == 'category' or type == 'all':
- for category in Category.objects.all():
- url = category.get_absolute_url()
- urls.append(self.get_full_url(url))
-
- self.stdout.write(
- self.style.SUCCESS(
- 'start notify %d urls' %
- len(urls)))
- SpiderNotify.baidu_notify(urls)
- self.stdout.write(self.style.SUCCESS('finish notify'))
diff --git a/blog/management/commands/sync_user_avatar.py b/blog/management/commands/sync_user_avatar.py
deleted file mode 100644
index d0f4612..0000000
--- a/blog/management/commands/sync_user_avatar.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import requests
-from django.core.management.base import BaseCommand
-from django.templatetags.static import static
-
-from djangoblog.utils import save_user_avatar
-from oauth.models import OAuthUser
-from oauth.oauthmanager import get_manager_by_type
-
-
-class Command(BaseCommand):
- help = 'sync user avatar'
-
- def test_picture(self, url):
- try:
- if requests.get(url, timeout=2).status_code == 200:
- return True
- except:
- pass
-
- def handle(self, *args, **options):
- static_url = static("../")
- users = OAuthUser.objects.all()
- self.stdout.write(f'开始同步{len(users)}个用户头像')
- for u in users:
- self.stdout.write(f'开始同步:{u.nickname}')
- url = u.picture
- if url:
- if url.startswith(static_url):
- if self.test_picture(url):
- continue
- else:
- if u.metadata:
- manage = get_manager_by_type(u.type)
- url = manage.get_picture(u.metadata)
- url = save_user_avatar(url)
- else:
- url = static('blog/img/avatar.png')
- else:
- url = save_user_avatar(url)
- else:
- url = static('blog/img/avatar.png')
- if url:
- self.stdout.write(
- f'结束同步:{u.nickname}.url:{url}')
- u.picture = url
- u.save()
- self.stdout.write('结束同步')
diff --git a/blog/middleware.py b/blog/middleware.py
deleted file mode 100644
index 94dd70c..0000000
--- a/blog/middleware.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import logging
-import time
-
-from ipware import get_client_ip
-from user_agents import parse
-
-from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
-
-logger = logging.getLogger(__name__)
-
-
-class OnlineMiddleware(object):
- def __init__(self, get_response=None):
- self.get_response = get_response
- super().__init__()
-
- def __call__(self, request):
- ''' page render time '''
- start_time = time.time()
- response = self.get_response(request)
- http_user_agent = request.META.get('HTTP_USER_AGENT', '')
- ip, _ = get_client_ip(request)
- user_agent = parse(http_user_agent)
- if not response.streaming:
- try:
- cast_time = time.time() - start_time
- if ELASTICSEARCH_ENABLED:
- time_taken = round((cast_time) * 1000, 2)
- url = request.path
- from django.utils import timezone
- ElaspedTimeDocumentManager.create(
- url=url,
- time_taken=time_taken,
- log_datetime=timezone.now(),
- useragent=user_agent,
- ip=ip)
- response.content = response.content.replace(
- b'', str.encode(str(cast_time)[:5]))
- except Exception as e:
- logger.error("Error OnlineMiddleware: %s" % e)
-
- return response
diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py
deleted file mode 100644
index 3d391b6..0000000
--- a/blog/migrations/0001_initial.py
+++ /dev/null
@@ -1,137 +0,0 @@
-# Generated by Django 4.1.7 on 2023-03-02 07:14
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-import mdeditor.fields
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='BlogSettings',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
- ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
- ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
- ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
- ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
- ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
- ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
- ('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
- ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
- ('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
- ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
- ('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
- ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
- ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
- ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
- ],
- options={
- 'verbose_name': '网站配置',
- 'verbose_name_plural': '网站配置',
- },
- ),
- migrations.CreateModel(
- name='Links',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
- ('link', models.URLField(verbose_name='链接地址')),
- ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
- ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
- ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ],
- options={
- 'verbose_name': '友情链接',
- 'verbose_name_plural': '友情链接',
- 'ordering': ['sequence'],
- },
- ),
- migrations.CreateModel(
- name='SideBar',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=100, verbose_name='标题')),
- ('content', models.TextField(verbose_name='内容')),
- ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
- ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ],
- options={
- 'verbose_name': '侧边栏',
- 'verbose_name_plural': '侧边栏',
- 'ordering': ['sequence'],
- },
- ),
- migrations.CreateModel(
- name='Tag',
- fields=[
- ('id', models.AutoField(primary_key=True, serialize=False)),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
- ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
- ],
- options={
- 'verbose_name': '标签',
- 'verbose_name_plural': '标签',
- 'ordering': ['name'],
- },
- ),
- migrations.CreateModel(
- name='Category',
- fields=[
- ('id', models.AutoField(primary_key=True, serialize=False)),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
- ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
- ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
- ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
- ],
- options={
- 'verbose_name': '分类',
- 'verbose_name_plural': '分类',
- 'ordering': ['-index'],
- },
- ),
- migrations.CreateModel(
- name='Article',
- fields=[
- ('id', models.AutoField(primary_key=True, serialize=False)),
- ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
- ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
- ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
- ('body', mdeditor.fields.MDTextField(verbose_name='正文')),
- ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
- ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
- ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
- ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
- ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
- ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
- ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
- ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
- ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
- ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
- ],
- options={
- 'verbose_name': '文章',
- 'verbose_name_plural': '文章',
- 'ordering': ['-article_order', '-pub_time'],
- 'get_latest_by': 'id',
- },
- ),
- ]
diff --git a/blog/migrations/0002_blogsettings_global_footer_and_more.py b/blog/migrations/0002_blogsettings_global_footer_and_more.py
deleted file mode 100644
index adbaa36..0000000
--- a/blog/migrations/0002_blogsettings_global_footer_and_more.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 4.1.7 on 2023-03-29 06:08
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('blog', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='blogsettings',
- name='global_footer',
- field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
- ),
- migrations.AddField(
- model_name='blogsettings',
- name='global_header',
- field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
- ),
- ]
diff --git a/blog/migrations/0003_blogsettings_comment_need_review.py b/blog/migrations/0003_blogsettings_comment_need_review.py
deleted file mode 100644
index e9f5502..0000000
--- a/blog/migrations/0003_blogsettings_comment_need_review.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 4.2.1 on 2023-05-09 07:45
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ('blog', '0002_blogsettings_global_footer_and_more'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='blogsettings',
- name='comment_need_review',
- field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
- ),
- ]
diff --git a/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
deleted file mode 100644
index ceb1398..0000000
--- a/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by Django 4.2.1 on 2023-05-09 07:51
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ('blog', '0003_blogsettings_comment_need_review'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='blogsettings',
- old_name='analyticscode',
- new_name='analytics_code',
- ),
- migrations.RenameField(
- model_name='blogsettings',
- old_name='beiancode',
- new_name='beian_code',
- ),
- migrations.RenameField(
- model_name='blogsettings',
- old_name='sitename',
- new_name='site_name',
- ),
- ]
diff --git a/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
deleted file mode 100644
index d08e853..0000000
--- a/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
+++ /dev/null
@@ -1,300 +0,0 @@
-# Generated by Django 4.2.5 on 2023-09-06 13:13
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-import mdeditor.fields
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='article',
- options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
- ),
- migrations.AlterModelOptions(
- name='category',
- options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
- ),
- migrations.AlterModelOptions(
- name='links',
- options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
- ),
- migrations.AlterModelOptions(
- name='sidebar',
- options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
- ),
- migrations.AlterModelOptions(
- name='tag',
- options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
- ),
- migrations.RemoveField(
- model_name='article',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='article',
- name='last_mod_time',
- ),
- migrations.RemoveField(
- model_name='category',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='category',
- name='last_mod_time',
- ),
- migrations.RemoveField(
- model_name='links',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='sidebar',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='tag',
- name='created_time',
- ),
- migrations.RemoveField(
- model_name='tag',
- name='last_mod_time',
- ),
- migrations.AddField(
- model_name='article',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='article',
- name='last_modify_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
- ),
- migrations.AddField(
- model_name='category',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='category',
- name='last_modify_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
- ),
- migrations.AddField(
- model_name='links',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='sidebar',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='tag',
- name='creation_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
- ),
- migrations.AddField(
- model_name='tag',
- name='last_modify_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
- ),
- migrations.AlterField(
- model_name='article',
- name='article_order',
- field=models.IntegerField(default=0, verbose_name='order'),
- ),
- migrations.AlterField(
- model_name='article',
- name='author',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
- ),
- migrations.AlterField(
- model_name='article',
- name='body',
- field=mdeditor.fields.MDTextField(verbose_name='body'),
- ),
- migrations.AlterField(
- model_name='article',
- name='category',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
- ),
- migrations.AlterField(
- model_name='article',
- name='comment_status',
- field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
- ),
- migrations.AlterField(
- model_name='article',
- name='pub_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
- ),
- migrations.AlterField(
- model_name='article',
- name='show_toc',
- field=models.BooleanField(default=False, verbose_name='show toc'),
- ),
- migrations.AlterField(
- model_name='article',
- name='status',
- field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
- ),
- migrations.AlterField(
- model_name='article',
- name='tags',
- field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
- ),
- migrations.AlterField(
- model_name='article',
- name='title',
- field=models.CharField(max_length=200, unique=True, verbose_name='title'),
- ),
- migrations.AlterField(
- model_name='article',
- name='type',
- field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
- ),
- migrations.AlterField(
- model_name='article',
- name='views',
- field=models.PositiveIntegerField(default=0, verbose_name='views'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='article_comment_count',
- field=models.IntegerField(default=5, verbose_name='article comment count'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='article_sub_length',
- field=models.IntegerField(default=300, verbose_name='article sub length'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='google_adsense_codes',
- field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='open_site_comment',
- field=models.BooleanField(default=True, verbose_name='open site comment'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='show_google_adsense',
- field=models.BooleanField(default=False, verbose_name='show adsense'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='sidebar_article_count',
- field=models.IntegerField(default=10, verbose_name='sidebar article count'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='sidebar_comment_count',
- field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='site_description',
- field=models.TextField(default='', max_length=1000, verbose_name='site description'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='site_keywords',
- field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='site_name',
- field=models.CharField(default='', max_length=200, verbose_name='site name'),
- ),
- migrations.AlterField(
- model_name='blogsettings',
- name='site_seo_description',
- field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
- ),
- migrations.AlterField(
- model_name='category',
- name='index',
- field=models.IntegerField(default=0, verbose_name='index'),
- ),
- migrations.AlterField(
- model_name='category',
- name='name',
- field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
- ),
- migrations.AlterField(
- model_name='category',
- name='parent_category',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
- ),
- migrations.AlterField(
- model_name='links',
- name='is_enable',
- field=models.BooleanField(default=True, verbose_name='is show'),
- ),
- migrations.AlterField(
- model_name='links',
- name='last_mod_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
- ),
- migrations.AlterField(
- model_name='links',
- name='link',
- field=models.URLField(verbose_name='link'),
- ),
- migrations.AlterField(
- model_name='links',
- name='name',
- field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
- ),
- migrations.AlterField(
- model_name='links',
- name='sequence',
- field=models.IntegerField(unique=True, verbose_name='order'),
- ),
- migrations.AlterField(
- model_name='links',
- name='show_type',
- field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
- ),
- migrations.AlterField(
- model_name='sidebar',
- name='content',
- field=models.TextField(verbose_name='content'),
- ),
- migrations.AlterField(
- model_name='sidebar',
- name='is_enable',
- field=models.BooleanField(default=True, verbose_name='is enable'),
- ),
- migrations.AlterField(
- model_name='sidebar',
- name='last_mod_time',
- field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
- ),
- migrations.AlterField(
- model_name='sidebar',
- name='name',
- field=models.CharField(max_length=100, verbose_name='title'),
- ),
- migrations.AlterField(
- model_name='sidebar',
- name='sequence',
- field=models.IntegerField(unique=True, verbose_name='order'),
- ),
- migrations.AlterField(
- model_name='tag',
- name='name',
- field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
- ),
- ]
diff --git a/blog/migrations/0006_alter_blogsettings_options.py b/blog/migrations/0006_alter_blogsettings_options.py
deleted file mode 100644
index e36feb4..0000000
--- a/blog/migrations/0006_alter_blogsettings_options.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 4.2.7 on 2024-01-26 02:41
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('blog', '0005_alter_article_options_alter_category_options_and_more'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='blogsettings',
- options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
- ),
- ]
diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/models.py b/blog/models.py
deleted file mode 100644
index 083788b..0000000
--- a/blog/models.py
+++ /dev/null
@@ -1,376 +0,0 @@
-import logging
-import re
-from abc import abstractmethod
-
-from django.conf import settings
-from django.core.exceptions import ValidationError
-from django.db import models
-from django.urls import reverse
-from django.utils.timezone import now
-from django.utils.translation import gettext_lazy as _
-from mdeditor.fields import MDTextField
-from uuslug import slugify
-
-from djangoblog.utils import cache_decorator, cache
-from djangoblog.utils import get_current_site
-
-logger = logging.getLogger(__name__)
-
-
-class LinkShowType(models.TextChoices):
- I = ('i', _('index'))
- L = ('l', _('list'))
- P = ('p', _('post'))
- A = ('a', _('all'))
- S = ('s', _('slide'))
-
-
-class BaseModel(models.Model):
- id = models.AutoField(primary_key=True)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_modify_time = models.DateTimeField(_('modify time'), default=now)
-
- def save(self, *args, **kwargs):
- is_update_views = isinstance(
- self,
- Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
- if is_update_views:
- Article.objects.filter(pk=self.pk).update(views=self.views)
- else:
- if 'slug' in self.__dict__:
- slug = getattr(
- self, 'title') if 'title' in self.__dict__ else getattr(
- self, 'name')
- setattr(self, 'slug', slugify(slug))
- super().save(*args, **kwargs)
-
- def get_full_url(self):
- site = get_current_site().domain
- url = "https://{site}{path}".format(site=site,
- path=self.get_absolute_url())
- return url
-
- class Meta:
- abstract = True
-
- @abstractmethod
- def get_absolute_url(self):
- pass
-
-
-class Article(BaseModel):
- """文章"""
- STATUS_CHOICES = (
- ('d', _('Draft')),
- ('p', _('Published')),
- )
- COMMENT_STATUS = (
- ('o', _('Open')),
- ('c', _('Close')),
- )
- TYPE = (
- ('a', _('Article')),
- ('p', _('Page')),
- )
- title = models.CharField(_('title'), max_length=200, unique=True)
- body = MDTextField(_('body'))
- pub_time = models.DateTimeField(
- _('publish time'), blank=False, null=False, default=now)
- status = models.CharField(
- _('status'),
- max_length=1,
- choices=STATUS_CHOICES,
- default='p')
- comment_status = models.CharField(
- _('comment status'),
- max_length=1,
- choices=COMMENT_STATUS,
- default='o')
- type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
- views = models.PositiveIntegerField(_('views'), default=0)
- author = models.ForeignKey(
- settings.AUTH_USER_MODEL,
- verbose_name=_('author'),
- blank=False,
- null=False,
- on_delete=models.CASCADE)
- article_order = models.IntegerField(
- _('order'), blank=False, null=False, default=0)
- show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
- category = models.ForeignKey(
- 'Category',
- verbose_name=_('category'),
- on_delete=models.CASCADE,
- blank=False,
- null=False)
- tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
-
- def body_to_string(self):
- return self.body
-
- def __str__(self):
- return self.title
-
- class Meta:
- ordering = ['-article_order', '-pub_time']
- verbose_name = _('article')
- verbose_name_plural = verbose_name
- get_latest_by = 'id'
-
- def get_absolute_url(self):
- return reverse('blog:detailbyid', kwargs={
- 'article_id': self.id,
- 'year': self.creation_time.year,
- 'month': self.creation_time.month,
- 'day': self.creation_time.day
- })
-
- @cache_decorator(60 * 60 * 10)
- def get_category_tree(self):
- tree = self.category.get_category_tree()
- names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
-
- return names
-
- def save(self, *args, **kwargs):
- super().save(*args, **kwargs)
-
- def viewed(self):
- self.views += 1
- self.save(update_fields=['views'])
-
- def comment_list(self):
- cache_key = 'article_comments_{id}'.format(id=self.id)
- value = cache.get(cache_key)
- if value:
- logger.info('get article comments:{id}'.format(id=self.id))
- return value
- else:
- comments = self.comment_set.filter(is_enable=True).order_by('-id')
- cache.set(cache_key, comments, 60 * 100)
- logger.info('set article comments:{id}'.format(id=self.id))
- return comments
-
- def get_admin_url(self):
- info = (self._meta.app_label, self._meta.model_name)
- return reverse('admin:%s_%s_change' % info, args=(self.pk,))
-
- @cache_decorator(expiration=60 * 100)
- def next_article(self):
- # 下一篇
- return Article.objects.filter(
- id__gt=self.id, status='p').order_by('id').first()
-
- @cache_decorator(expiration=60 * 100)
- def prev_article(self):
- # 前一篇
- return Article.objects.filter(id__lt=self.id, status='p').first()
-
- def get_first_image_url(self):
- """
- Get the first image url from article.body.
- :return:
- """
- match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
- if match:
- return match.group(1)
- return ""
-
-
-class Category(BaseModel):
- """文章分类"""
- name = models.CharField(_('category name'), max_length=30, unique=True)
- parent_category = models.ForeignKey(
- 'self',
- verbose_name=_('parent category'),
- blank=True,
- null=True,
- on_delete=models.CASCADE)
- slug = models.SlugField(default='no-slug', max_length=60, blank=True)
- index = models.IntegerField(default=0, verbose_name=_('index'))
-
- class Meta:
- ordering = ['-index']
- verbose_name = _('category')
- verbose_name_plural = verbose_name
-
- def get_absolute_url(self):
- return reverse(
- 'blog:category_detail', kwargs={
- 'category_name': self.slug})
-
- def __str__(self):
- return self.name
-
- @cache_decorator(60 * 60 * 10)
- def get_category_tree(self):
- """
- 递归获得分类目录的父级
- :return:
- """
- categorys = []
-
- def parse(category):
- categorys.append(category)
- if category.parent_category:
- parse(category.parent_category)
-
- parse(self)
- return categorys
-
- @cache_decorator(60 * 60 * 10)
- def get_sub_categorys(self):
- """
- 获得当前分类目录所有子集
- :return:
- """
- categorys = []
- all_categorys = Category.objects.all()
-
- def parse(category):
- if category not in categorys:
- categorys.append(category)
- childs = all_categorys.filter(parent_category=category)
- for child in childs:
- if category not in categorys:
- categorys.append(child)
- parse(child)
-
- parse(self)
- return categorys
-
-
-class Tag(BaseModel):
- """文章标签"""
- name = models.CharField(_('tag name'), max_length=30, unique=True)
- slug = models.SlugField(default='no-slug', max_length=60, blank=True)
-
- def __str__(self):
- return self.name
-
- def get_absolute_url(self):
- return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
-
- @cache_decorator(60 * 60 * 10)
- def get_article_count(self):
- return Article.objects.filter(tags__name=self.name).distinct().count()
-
- class Meta:
- ordering = ['name']
- verbose_name = _('tag')
- verbose_name_plural = verbose_name
-
-
-class Links(models.Model):
- """友情链接"""
-
- name = models.CharField(_('link name'), max_length=30, unique=True)
- link = models.URLField(_('link'))
- sequence = models.IntegerField(_('order'), unique=True)
- is_enable = models.BooleanField(
- _('is show'), default=True, blank=False, null=False)
- show_type = models.CharField(
- _('show type'),
- max_length=1,
- choices=LinkShowType.choices,
- default=LinkShowType.I)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_mod_time = models.DateTimeField(_('modify time'), default=now)
-
- class Meta:
- ordering = ['sequence']
- verbose_name = _('link')
- verbose_name_plural = verbose_name
-
- def __str__(self):
- return self.name
-
-
-class SideBar(models.Model):
- """侧边栏,可以展示一些html内容"""
- name = models.CharField(_('title'), max_length=100)
- content = models.TextField(_('content'))
- sequence = models.IntegerField(_('order'), unique=True)
- is_enable = models.BooleanField(_('is enable'), default=True)
- creation_time = models.DateTimeField(_('creation time'), default=now)
- last_mod_time = models.DateTimeField(_('modify time'), default=now)
-
- class Meta:
- ordering = ['sequence']
- verbose_name = _('sidebar')
- verbose_name_plural = verbose_name
-
- def __str__(self):
- return self.name
-
-
-class BlogSettings(models.Model):
- """blog的配置"""
- site_name = models.CharField(
- _('site name'),
- max_length=200,
- null=False,
- blank=False,
- default='')
- site_description = models.TextField(
- _('site description'),
- max_length=1000,
- null=False,
- blank=False,
- default='')
- site_seo_description = models.TextField(
- _('site seo description'), max_length=1000, null=False, blank=False, default='')
- site_keywords = models.TextField(
- _('site keywords'),
- max_length=1000,
- null=False,
- blank=False,
- default='')
- article_sub_length = models.IntegerField(_('article sub length'), default=300)
- sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
- sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
- article_comment_count = models.IntegerField(_('article comment count'), default=5)
- show_google_adsense = models.BooleanField(_('show adsense'), default=False)
- google_adsense_codes = models.TextField(
- _('adsense code'), max_length=2000, null=True, blank=True, default='')
- open_site_comment = models.BooleanField(_('open site comment'), default=True)
- global_header = models.TextField("公共头部", null=True, blank=True, default='')
- global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
- beian_code = models.CharField(
- '备案号',
- max_length=2000,
- null=True,
- blank=True,
- default='')
- analytics_code = models.TextField(
- "网站统计代码",
- max_length=1000,
- null=False,
- blank=False,
- default='')
- show_gongan_code = models.BooleanField(
- '是否显示公安备案号', default=False, null=False)
- gongan_beiancode = models.TextField(
- '公安备案号',
- max_length=2000,
- null=True,
- blank=True,
- default='')
- comment_need_review = models.BooleanField(
- '评论是否需要审核', default=False, null=False)
-
- class Meta:
- verbose_name = _('Website configuration')
- verbose_name_plural = verbose_name
-
- def __str__(self):
- return self.site_name
-
- def clean(self):
- if BlogSettings.objects.exclude(id=self.id).count():
- raise ValidationError(_('There can only be one configuration'))
-
- def save(self, *args, **kwargs):
- super().save(*args, **kwargs)
- from djangoblog.utils import cache
- cache.clear()
diff --git a/blog/search_indexes.py b/blog/search_indexes.py
deleted file mode 100644
index 7f1dfac..0000000
--- a/blog/search_indexes.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from haystack import indexes
-
-from blog.models import Article
-
-
-class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
- text = indexes.CharField(document=True, use_template=True)
-
- def get_model(self):
- return Article
-
- def index_queryset(self, using=None):
- return self.get_model().objects.filter(status='p')
diff --git a/blog/templatetags/__init__.py b/blog/templatetags/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py
deleted file mode 100644
index d6cd5d5..0000000
--- a/blog/templatetags/blog_tags.py
+++ /dev/null
@@ -1,344 +0,0 @@
-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/blog/tests.py b/blog/tests.py
deleted file mode 100644
index ee13505..0000000
--- a/blog/tests.py
+++ /dev/null
@@ -1,232 +0,0 @@
-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/blog/urls.py b/blog/urls.py
deleted file mode 100644
index adf2703..0000000
--- a/blog/urls.py
+++ /dev/null
@@ -1,62 +0,0 @@
-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/blog/views.py b/blog/views.py
deleted file mode 100644
index d5dc7ec..0000000
--- a/blog/views.py
+++ /dev/null
@@ -1,379 +0,0 @@
-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/comments/__init__.py b/comments/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/comments/admin.py b/comments/admin.py
deleted file mode 100644
index a814f3f..0000000
--- a/comments/admin.py
+++ /dev/null
@@ -1,47 +0,0 @@
-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/comments/apps.py b/comments/apps.py
deleted file mode 100644
index ff01b77..0000000
--- a/comments/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class CommentsConfig(AppConfig):
- name = 'comments'
diff --git a/comments/forms.py b/comments/forms.py
deleted file mode 100644
index e83737d..0000000
--- a/comments/forms.py
+++ /dev/null
@@ -1,13 +0,0 @@
-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/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py
deleted file mode 100644
index 61d1e53..0000000
--- a/comments/migrations/0001_initial.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Generated by Django 4.1.7 on 2023-03-02 07:14
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-
-
-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/comments/migrations/0002_alter_comment_is_enable.py b/comments/migrations/0002_alter_comment_is_enable.py
deleted file mode 100644
index 17c44db..0000000
--- a/comments/migrations/0002_alter_comment_is_enable.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# 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/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
deleted file mode 100644
index a1ca970..0000000
--- a/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Generated by Django 4.2.5 on 2023-09-06 13:13
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
-
-
-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/comments/migrations/__init__.py b/comments/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/comments/models.py b/comments/models.py
deleted file mode 100644
index 7c3bbc8..0000000
--- a/comments/models.py
+++ /dev/null
@@ -1,39 +0,0 @@
-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/comments/templatetags/__init__.py b/comments/templatetags/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/comments/templatetags/comments_tags.py b/comments/templatetags/comments_tags.py
deleted file mode 100644
index fde02b4..0000000
--- a/comments/templatetags/comments_tags.py
+++ /dev/null
@@ -1,30 +0,0 @@
-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/comments/tests.py b/comments/tests.py
deleted file mode 100644
index 2a7f55f..0000000
--- a/comments/tests.py
+++ /dev/null
@@ -1,109 +0,0 @@
-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/comments/urls.py b/comments/urls.py
deleted file mode 100644
index 7df3fab..0000000
--- a/comments/urls.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.urls import path
-
-from . import views
-
-app_name = "comments"
-urlpatterns = [
- path(
- 'article//postcomment',
- views.CommentPostView.as_view(),
- name='postcomment'),
-]
diff --git a/comments/utils.py b/comments/utils.py
deleted file mode 100644
index f01dba7..0000000
--- a/comments/utils.py
+++ /dev/null
@@ -1,38 +0,0 @@
-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/comments/views.py b/comments/views.py
deleted file mode 100644
index ad9b2b9..0000000
--- a/comments/views.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# 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/deploy/docker-compose/docker-compose.es.yml b/deploy/docker-compose/docker-compose.es.yml
deleted file mode 100644
index 83e35ff..0000000
--- a/deploy/docker-compose/docker-compose.es.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-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/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml
deleted file mode 100644
index 9609af3..0000000
--- a/deploy/docker-compose/docker-compose.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-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/deploy/entrypoint.sh b/deploy/entrypoint.sh
deleted file mode 100644
index 2fb6491..0000000
--- a/deploy/entrypoint.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/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/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml
deleted file mode 100644
index 835d4ad..0000000
--- a/deploy/k8s/configmap.yaml
+++ /dev/null
@@ -1,119 +0,0 @@
-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/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml
deleted file mode 100644
index 414fdcc..0000000
--- a/deploy/k8s/deployment.yaml
+++ /dev/null
@@ -1,274 +0,0 @@
-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/deploy/k8s/gateway.yaml b/deploy/k8s/gateway.yaml
deleted file mode 100644
index a8de073..0000000
--- a/deploy/k8s/gateway.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-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/deploy/k8s/pv.yaml b/deploy/k8s/pv.yaml
deleted file mode 100644
index 874b72f..0000000
--- a/deploy/k8s/pv.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-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/deploy/k8s/pvc.yaml b/deploy/k8s/pvc.yaml
deleted file mode 100644
index ef238c5..0000000
--- a/deploy/k8s/pvc.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-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/deploy/k8s/service.yaml b/deploy/k8s/service.yaml
deleted file mode 100644
index 4ef2931..0000000
--- a/deploy/k8s/service.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-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/deploy/k8s/storageclass.yaml b/deploy/k8s/storageclass.yaml
deleted file mode 100644
index 5d5a14c..0000000
--- a/deploy/k8s/storageclass.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-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/deploy/nginx.conf b/deploy/nginx.conf
deleted file mode 100644
index 32161d8..0000000
--- a/deploy/nginx.conf
+++ /dev/null
@@ -1,50 +0,0 @@
-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/docs/README-en.md b/docs/README-en.md
deleted file mode 100644
index 37ea069..0000000
--- a/docs/README-en.md
+++ /dev/null
@@ -1,158 +0,0 @@
-# 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/docs/config-en.md b/docs/config-en.md
deleted file mode 100644
index b877efb..0000000
--- a/docs/config-en.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# 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/docs/config.md b/docs/config.md
deleted file mode 100644
index 24673a3..0000000
--- a/docs/config.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# 主要功能配置介绍:
-
-## 缓存:
-缓存默认使用`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/docs/docker-en.md b/docs/docker-en.md
deleted file mode 100644
index 8d5d59e..0000000
--- a/docs/docker-en.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# 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/docs/docker.md b/docs/docker.md
deleted file mode 100644
index e7c255a..0000000
--- a/docs/docker.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# 使用 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/docs/es.md b/docs/es.md
deleted file mode 100644
index 97226c5..0000000
--- a/docs/es.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# 集成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/docs/imgs/alipay.jpg b/docs/imgs/alipay.jpg
deleted file mode 100644
index 424d70a2ffbb629b481e0c27d72d6076727e8041..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 17961
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
#-7z5T*_tVDNm_fWm2J7~E{0EUH
zaTS1=8oR}i)Yu5IzhuaJUQpk;{71-fgn99r)D@c@raD#Jg%rK8OP47l=598G)R@Wj
z3sTPhZiV`Q96~rJBXt8Q=)!u8yhE%_RpnofgHAR5AyUT@l4pNCRG}~Y=Dj!tKLCypG0l6F-03=k8_xdFVtKtN$L;0@
zdllSZ8bb4G@{$AlXnPw?(qZ&2i;za~=DRe!%4ZZC8;W+TFN_-@m{VN{HK)4kjPA{M
zXH#(!=3rgs@D?*K6N=(Bj}?rM^|%Mhm(NZSlyM4$ZhUriJrnEe)
zRTrlvJqpsdr3&x#o3PF)w9dA4vIjNRNjnw|N*`)|T;=Y;n2VBrT)?}?qB7Dv%KC-k
z=)n_;TisLzWGH;eJQ6`@!No#iqD9T-fedVB88xA
zo;JC|uZgi!#Co3ph0}|BB{faa&67DZQZ{0o-XKATf{#ZZ%+&fF-U+AAOtAa?aLb}0l5WnG!>`?^H
zx;`p{5TRCD<&pDIgyuBDu1=!sVf*15{)I(*0r6PfNrn+cCs{@i`14?)I&y!`f8JMd
z*kb%Z+CGVHJ7sZ>lnL!6;R51P0M#nlTO&kwSc0l3X%OB1-+EPj7eFzDkPZFU(}W57
z5|=uiOyIL9nIqTRgi$pZPG-%!Mf-Or&w2l|+#ZpQ?4UM5&}dG?LIg_IU1eTafvAC3jwBAh%
z9S0LF>sJsKN_hHwa+N=0)+^Uv9jPa19pl+^f4`NfU6>zvKq?rDO+|{&*r;1RAH`Nf
zRpp~+Uw_j=Vh`;lbgJ95{8YG4e0|Of!f3Ex;VS#J=xb3=VFOFb;234CzO=o6DZq?{
zkhs2UeEwu79L22C4qkbyps>(LLEx{S&C9!
zqFx9jK>9e%B;6VYNcEhXRc7t6XE0G_Z}PaJb<4OS9=lU$p?WhSV2HkccLqhdGHg(S
zWrF|~p<%J>3qJo&Pz-2lNFnq^)@59eu$a4eoMPzfcAjiF2;bX5(Yk`+D%2LD|MixU
zd_mo!|NBit2-hAGKj~7Iv$l)#L*f@3=0wuTv>^0Z2EcD~O@A=r&v+`CF1;pg`O#~R
zdFh)!bZZ&CjU`0!k~h6o!*?3ig)3#y5Dr9yl!a%FcI?y_3y*6ZwT!Y&Gp`gzGp6qf
z3!mXrO#mC4@-(282uWdko4($zc0XG5OPDmMp%ba0uuiMEql&`da93afQHT^9j7r>j
zo_L1@zagwUwX-$#0jB}nxD*8LIW4%Fi4R{y%0fola}$zC&Os$7{dZ8LOg&aDDkvt*
z6{|v6K&v164{d@r;P~b4$r{B*5p6*OuFfai)OxCSo1~XEHuz7)CLS2wBwbVKtnT0}
zbJ+zuN1{ox6C_`^KtuQc_7gu!Q;3NTkdj=Idhi4l$%o`|>io%B4U#{JDTAC+Gv{50
z`r9cOO06j7F5xS$h}?jv{u+hxM<*_GH}}|ov}`aD>E{8|E9h-Sis-$GDh$e7|8%#h
z*sG||^lCY>D`Sg)=Ma!M`(m@9#t2%1?XU1Z?;%RWm&DZEc742@DRG(@p4~zzl*2hQqqGixEB@_XUwvGfowJb6dIkeE}_PtEnQ>*cqZ@
z`u%SM63p&Hl*CkhPZP<5Rnat6d4q+|3pvTSk7jPUpIy~b1;$&&8bRd{H~AkooZ@Wc
z3Ek7VKj7|!e@!Z#-&AoGi5S=*W)7$wkpT0mYKJGbx(L;WGn@GZy0J|#j@wfC+lyw3
zN@Ae*m6)Li9uo&GC(+@%(3S8&0wBto6#SQDH{VpUDCR3jjq4$J$7~--n+MaP0Njak
zIHjlIG2_4!VxnDz4#Qc~FcQ(kw6`mi`Svqf62mZRuzHr__N
zX|⁡O0bI!3Bi@jSTgKn>Bg&Q53II7XEA`NhoOY$25L(eK?<1>~>hujoRs}yUHVX
zQTt}~MN1Zw0!xe7-aUbnm3fxd*KjwLLobx-SSZdGT`vUtB-=hS^~#Rs9K<#Wao{Zp
z{kmB)_tVwwglj-W2qla4$Dx
zz53|611jp`Ro8}#TnY+1OdA}SN;lGY+!=)=QY;q0b}yA~i0O^|s1#r34{5DET&Qh`
z+yAF+h#H($?p6{{X
zz@F@jlhUjuyW%|!{Q!~$q>$)y1Hsl$FMYS;#r=c7U7SN-8Uv-lv<@mZOXKOlXLy4C
z3v?;Yh1KbtsO!ynC%cyr-`4MR8{Y~e28GXK7yRZ9{3d5(xoP=XYdX*Uy?#pU$a5>&
z`JUOV_
zRl}`OH8`y;U%XD+MFBD!HxL@B`JSH_G!;0V9-A+_4Hhf%=wJ%txE{qNaHkfkLsS+h
zfe@JrYreKrFKhU2`1kD)K_#LCd!v$sI$is2ee^7Am=@kq2Z-`cU(l{<8*ON_J?@@&
zOR+4E3%sN-vIK-%81R}RdPK$bCrBoGF
zRu$>N^0s*R^!6R919|j@|cY|A}(o`$`Onha=;(7(EQNg;0
z^sK+j)DQ>i-wCKpkXH^lH{QNw|FWJ%bUle^xtPfLdv#(gNpA7lv59GyIOXp*E+vz_!y|AkM=lB7mYXSWGfxr6#WlV5UW{v7tVn1!e
zwg=v}kG!o+0qXfY-FyUozk}%&PS$ZRL^NoDuz%nVw(p)FVMPG6Ph*mgs4z0~C)P`9
zWsg_X!-)T2FkH=VqaRBu8Zkq=AX6oLSWpg(yQ^NIftdmxjx
diff --git a/docs/imgs/pycharm_logo.png b/docs/imgs/pycharm_logo.png
deleted file mode 100644
index 7f2a4b0ea66469bd218774de8cb3027a9c18b84d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 132045
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