From e232d595dca59ba2acc45d971c3f591aa70e4968 Mon Sep 17 00:00:00 2001 From: p8b4i7hte <2518549229@qq.com> Date: Thu, 18 Sep 2025 18:41:56 +0800 Subject: [PATCH 1/8] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4de878 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# src + -- 2.34.1 From 5a044ad32c4f23acbbb748ae3ed365c87d9c2661 Mon Sep 17 00:00:00 2001 From: p8b4i7hte <2518549229@qq.com> Date: Thu, 18 Sep 2025 20:49:59 +0800 Subject: [PATCH 2/8] Add src --- src | 1 + 1 file changed, 1 insertion(+) create mode 100644 src diff --git a/src b/src new file mode 100644 index 0000000..66dc905 --- /dev/null +++ b/src @@ -0,0 +1 @@ +undefined \ No newline at end of file -- 2.34.1 From 70efeae26c9b11ab1555050d7371f98cc2e9cfa7 Mon Sep 17 00:00:00 2001 From: p8b4i7hte <2518549229@qq.com> Date: Thu, 18 Sep 2025 20:50:17 +0800 Subject: [PATCH 3/8] Add doc --- doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc diff --git a/doc b/doc new file mode 100644 index 0000000..66dc905 --- /dev/null +++ b/doc @@ -0,0 +1 @@ +undefined \ No newline at end of file -- 2.34.1 From 5b910b544571890ca5000b46fcd8672852ac42e4 Mon Sep 17 00:00:00 2001 From: p8b4i7hte <2518549229@qq.com> Date: Thu, 18 Sep 2025 20:51:00 +0800 Subject: [PATCH 4/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4de878..139597f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# src + -- 2.34.1 From 9797bce737eb14dc40186af82b28fdd43c5a08ba Mon Sep 17 00:00:00 2001 From: lixue <2518549229@qq.com> Date: Thu, 25 Sep 2025 22:45:29 +0800 Subject: [PATCH 5/8] =?UTF-8?q?init:=20=E5=88=9B=E5=BB=BA=20src(=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=9B=AE=E5=BD=95)=20=E5=92=8C=20doc(?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=9B=AE=E5=BD=95)=EF=BC=8C=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84?= 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 + djangoblog/__init__.py | 1 + djangoblog/admin_site.py | 64 + djangoblog/apps.py | 11 + djangoblog/blog_signals.py | 122 ++ djangoblog/elasticsearch_backend.py | 183 +++ djangoblog/feeds.py | 40 + djangoblog/logentryadmin.py | 91 ++ djangoblog/plugin_manage/base_plugin.py | 41 + djangoblog/plugin_manage/hook_constants.py | 7 + djangoblog/plugin_manage/hooks.py | 44 + djangoblog/plugin_manage/loader.py | 19 + djangoblog/settings.py | 343 ++++++ djangoblog/sitemap.py | 59 + djangoblog/spider_notify.py | 21 + djangoblog/tests.py | 32 + djangoblog/urls.py | 64 + djangoblog/utils.py | 232 ++++ djangoblog/whoosh_cn_backend.py | 1044 +++++++++++++++++ djangoblog/wsgi.py | 16 + doc/.gitkeep | 0 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 0 -> 17961 bytes docs/imgs/pycharm_logo.png | Bin 0 -> 132045 bytes docs/imgs/wechat.jpg | Bin 0 -> 24722 bytes docs/k8s-en.md | 141 +++ docs/k8s.md | 141 +++ locale/en/LC_MESSAGES/django.mo | Bin 0 -> 11097 bytes locale/en/LC_MESSAGES/django.po | 685 +++++++++++ locale/zh_Hans/LC_MESSAGES/django.mo | Bin 0 -> 10321 bytes locale/zh_Hans/LC_MESSAGES/django.po | 667 +++++++++++ locale/zh_Hant/LC_MESSAGES/django.mo | Bin 0 -> 10268 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 0 -> 2554 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 + src/.gitkeep | 0 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 + 204 files changed, 14469 insertions(+) create mode 100644 .coveragerc create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/django.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/forms.py create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/templatetags/__init__.py create mode 100644 accounts/tests.py create mode 100644 accounts/urls.py create mode 100644 accounts/user_login_backend.py create mode 100644 accounts/utils.py create mode 100644 accounts/views.py create mode 100644 blog/__init__.py create mode 100644 blog/admin.py create mode 100644 blog/apps.py create mode 100644 blog/context_processors.py create mode 100644 blog/documents.py create mode 100644 blog/forms.py create mode 100644 blog/management/__init__.py create mode 100644 blog/management/commands/__init__.py create mode 100644 blog/management/commands/build_index.py create mode 100644 blog/management/commands/build_search_words.py create mode 100644 blog/management/commands/clear_cache.py create mode 100644 blog/management/commands/create_testdata.py create mode 100644 blog/management/commands/ping_baidu.py create mode 100644 blog/management/commands/sync_user_avatar.py create mode 100644 blog/middleware.py create mode 100644 blog/migrations/0001_initial.py create mode 100644 blog/migrations/0002_blogsettings_global_footer_and_more.py create mode 100644 blog/migrations/0003_blogsettings_comment_need_review.py create mode 100644 blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py create mode 100644 blog/migrations/0005_alter_article_options_alter_category_options_and_more.py create mode 100644 blog/migrations/0006_alter_blogsettings_options.py create mode 100644 blog/migrations/__init__.py create mode 100644 blog/models.py create mode 100644 blog/search_indexes.py create mode 100644 blog/templatetags/__init__.py create mode 100644 blog/templatetags/blog_tags.py create mode 100644 blog/tests.py create mode 100644 blog/urls.py create mode 100644 blog/views.py create mode 100644 comments/__init__.py create mode 100644 comments/admin.py create mode 100644 comments/apps.py create mode 100644 comments/forms.py create mode 100644 comments/migrations/0001_initial.py create mode 100644 comments/migrations/0002_alter_comment_is_enable.py create mode 100644 comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py create mode 100644 comments/migrations/__init__.py create mode 100644 comments/models.py create mode 100644 comments/templatetags/__init__.py create mode 100644 comments/templatetags/comments_tags.py create mode 100644 comments/tests.py create mode 100644 comments/urls.py create mode 100644 comments/utils.py create mode 100644 comments/views.py create mode 100644 deploy/docker-compose/docker-compose.es.yml create mode 100644 deploy/docker-compose/docker-compose.yml create mode 100644 deploy/entrypoint.sh create mode 100644 deploy/k8s/configmap.yaml create mode 100644 deploy/k8s/deployment.yaml create mode 100644 deploy/k8s/gateway.yaml create mode 100644 deploy/k8s/pv.yaml create mode 100644 deploy/k8s/pvc.yaml create mode 100644 deploy/k8s/service.yaml create mode 100644 deploy/k8s/storageclass.yaml create mode 100644 deploy/nginx.conf create mode 100644 djangoblog/__init__.py create mode 100644 djangoblog/admin_site.py create mode 100644 djangoblog/apps.py create mode 100644 djangoblog/blog_signals.py create mode 100644 djangoblog/elasticsearch_backend.py create mode 100644 djangoblog/feeds.py create mode 100644 djangoblog/logentryadmin.py create mode 100644 djangoblog/plugin_manage/base_plugin.py create mode 100644 djangoblog/plugin_manage/hook_constants.py create mode 100644 djangoblog/plugin_manage/hooks.py create mode 100644 djangoblog/plugin_manage/loader.py create mode 100644 djangoblog/settings.py create mode 100644 djangoblog/sitemap.py create mode 100644 djangoblog/spider_notify.py create mode 100644 djangoblog/tests.py create mode 100644 djangoblog/urls.py create mode 100644 djangoblog/utils.py create mode 100644 djangoblog/whoosh_cn_backend.py create mode 100644 djangoblog/wsgi.py create mode 100644 doc/.gitkeep create mode 100644 docs/README-en.md create mode 100644 docs/config-en.md create mode 100644 docs/config.md create mode 100644 docs/docker-en.md create mode 100644 docs/docker.md create mode 100644 docs/es.md create mode 100644 docs/imgs/alipay.jpg create mode 100644 docs/imgs/pycharm_logo.png create mode 100644 docs/imgs/wechat.jpg create mode 100644 docs/k8s-en.md create mode 100644 docs/k8s.md create mode 100644 locale/en/LC_MESSAGES/django.mo create mode 100644 locale/en/LC_MESSAGES/django.po create mode 100644 locale/zh_Hans/LC_MESSAGES/django.mo create mode 100644 locale/zh_Hans/LC_MESSAGES/django.po create mode 100644 locale/zh_Hant/LC_MESSAGES/django.mo create mode 100644 locale/zh_Hant/LC_MESSAGES/django.po create mode 100644 manage.py create mode 100644 oauth/__init__.py create mode 100644 oauth/admin.py create mode 100644 oauth/apps.py create mode 100644 oauth/forms.py create mode 100644 oauth/migrations/0001_initial.py create mode 100644 oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py create mode 100644 oauth/migrations/0003_alter_oauthuser_nickname.py create mode 100644 oauth/migrations/__init__.py create mode 100644 oauth/models.py create mode 100644 oauth/oauthmanager.py create mode 100644 oauth/templatetags/__init__.py create mode 100644 oauth/templatetags/oauth_tags.py create mode 100644 oauth/tests.py create mode 100644 oauth/urls.py create mode 100644 oauth/views.py create mode 100644 owntracks/__init__.py create mode 100644 owntracks/admin.py create mode 100644 owntracks/apps.py create mode 100644 owntracks/migrations/0001_initial.py create mode 100644 owntracks/migrations/0002_alter_owntracklog_options_and_more.py create mode 100644 owntracks/migrations/__init__.py create mode 100644 owntracks/models.py create mode 100644 owntracks/tests.py create mode 100644 owntracks/urls.py create mode 100644 owntracks/views.py create mode 100644 plugins/__init__.py create mode 100644 plugins/article_copyright/__init__.py create mode 100644 plugins/article_copyright/plugin.py create mode 100644 plugins/external_links/__init__.py create mode 100644 plugins/external_links/plugin.py create mode 100644 plugins/reading_time/__init__.py create mode 100644 plugins/reading_time/plugin.py create mode 100644 plugins/seo_optimizer/__init__.py create mode 100644 plugins/seo_optimizer/plugin.py create mode 100644 plugins/view_count/__init__.py create mode 100644 plugins/view_count/plugin.py create mode 100644 requirements.txt create mode 100644 servermanager/MemcacheStorage.py create mode 100644 servermanager/__init__.py create mode 100644 servermanager/admin.py create mode 100644 servermanager/api/__init__.py create mode 100644 servermanager/api/blogapi.py create mode 100644 servermanager/api/commonapi.py create mode 100644 servermanager/apps.py create mode 100644 servermanager/migrations/0001_initial.py create mode 100644 servermanager/migrations/0002_alter_emailsendlog_options_and_more.py create mode 100644 servermanager/migrations/__init__.py create mode 100644 servermanager/models.py create mode 100644 servermanager/robot.py create mode 100644 servermanager/tests.py create mode 100644 servermanager/urls.py create mode 100644 servermanager/views.py create mode 100644 src/.gitkeep create mode 100644 templates/account/forget_password.html create mode 100644 templates/account/login.html create mode 100644 templates/account/registration_form.html create mode 100644 templates/account/result.html create mode 100644 templates/blog/article_archives.html create mode 100644 templates/blog/article_detail.html create mode 100644 templates/blog/article_index.html create mode 100644 templates/blog/error_page.html create mode 100644 templates/blog/links_list.html create mode 100644 templates/blog/tags/article_info.html create mode 100644 templates/blog/tags/article_meta_info.html create mode 100644 templates/blog/tags/article_pagination.html create mode 100644 templates/blog/tags/article_tag_list.html create mode 100644 templates/blog/tags/breadcrumb.html create mode 100644 templates/blog/tags/sidebar.html create mode 100644 templates/comments/tags/comment_item.html create mode 100644 templates/comments/tags/comment_item_tree.html create mode 100644 templates/comments/tags/comment_list.html create mode 100644 templates/comments/tags/post_comment.html create mode 100644 templates/oauth/bindsuccess.html create mode 100644 templates/oauth/oauth_applications.html create mode 100644 templates/oauth/require_email.html create mode 100644 templates/owntracks/show_log_dates.html create mode 100644 templates/owntracks/show_maps.html create mode 100644 templates/search/indexes/blog/article_text.txt create mode 100644 templates/search/search.html create mode 100644 templates/share_layout/adsense.html create mode 100644 templates/share_layout/base.html create mode 100644 templates/share_layout/base_account.html create mode 100644 templates/share_layout/footer.html create mode 100644 templates/share_layout/nav.html create mode 100644 templates/share_layout/nav_node.html diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..9757484 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +source = . +include = *.py +omit = + *migrations* + *tests* + *.html + *whoosh_cn_backend* + *settings.py* + *venv* diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2818c38 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +bin/data/ +# virtualenv +venv/ +collectedstatic/ +djangoblog/whoosh_index/ +uploads/ +settings_production.py +*.md +docs/ +logs/ +static/ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fd52ece --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +blog/static/* linguist-vendored +*.js linguist-vendored +*.css linguist-vendored +* text=auto +*.sh text eol=lf +*.conf text eol=lf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..2b5b7aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ + + +**我确定我已经查看了** (标注`[ ]`为`[x]`) + +- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md) +- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md) +- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues) + +---- + +**我要申请** (标注`[ ]`为`[x]`) + +- [ ] BUG 反馈 +- [ ] 添加新的特性或者功能 +- [ ] 请求技术支持 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..6b76522 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,47 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + schedule: + - cron: '30 1 * * 0' + + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000..94baea9 --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,136 @@ +name: Django CI + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + +jobs: + build-normal: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + run: | + python manage.py makemigrations + python manage.py migrate + python manage.py test + + build-with-es: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - uses: miyataka/elasticsearch-github-actions@1 + + with: + stack-version: '7.12.1' + plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip' + + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200 + run: | + python manage.py makemigrations + python manage.py migrate + coverage run manage.py test + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: false + tags: djangoblog/djangoblog:dev diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a312e2f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,43 @@ +name: docker + +on: + push: + paths-ignore: + - '**/*.md' + - '**/*.yml' + branches: + - 'master' + - 'dev' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set env to docker dev tag + if: endsWith(github.ref, '/dev') + run: | + echo "DOCKER_TAG=test" >> $GITHUB_ENV + - name: Set env to docker latest tag + if: endsWith(github.ref, '/master') + run: | + echo "DOCKER_TAG=latest" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}} + + diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..5eb0853 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,39 @@ +name: publish release + +on: + release: + types: [ published ] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: name/app + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + platforms: | + linux/amd64 + linux/arm64 + linux/arm/v7 + linux/arm/v6 + linux/386 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3015816 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.pot + +# Django stuff: +*.log +logs/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +# PyCharm +# http://www.jetbrains.com/pycharm/webhelp/project.html +.idea +.iml +static/ +# virtualenv +venv/ + +collectedstatic/ +djangoblog/whoosh_index/ +google93fd32dbd906620a.html +baidu_verify_FlHL7cUyC9.html +BingSiteAuth.xml +cb9339dbe2ff86a5aa169d28dba5f615.txt +werobot_session.* +django.jpg +uploads/ +settings_production.py +werobot_session.db +bin/datas/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..80b46ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 +ENV PYTHONUNBUFFERED 1 +WORKDIR /code/djangoblog/ +RUN apt-get update && \ + apt-get install default-libmysqlclient-dev gettext -y && \ + rm -rf /var/lib/apt/lists/* +ADD requirements.txt requirements.txt +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir gunicorn[gevent] && \ + pip cache purge + +ADD . . +RUN chmod +x /code/djangoblog/deploy/entrypoint.sh +ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b08474 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 车亮亮 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..56aa4cc --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# DjangoBlog + +

+ Django CI + CodeQL + codecov + license +

+ +

+ 一款功能强大、设计优雅的现代化博客系统 +
+ 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** 为本项目提供的免费开源许可证。 + +

+ + JetBrains Logo + +

+ +--- +> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。 diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..32e483c --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,59 @@ +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.forms import UsernameField +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import BlogUser + + +class BlogUserCreationForm(forms.ModelForm): + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) + + class Meta: + model = BlogUser + fields = ('email',) + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("passwords do not match")) + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.source = 'adminsite' + user.save() + return user + + +class BlogUserChangeForm(UserChangeForm): + class Meta: + model = BlogUser + fields = '__all__' + field_classes = {'username': UsernameField} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class BlogUserAdmin(UserAdmin): + form = BlogUserChangeForm + add_form = BlogUserCreationForm + list_display = ( + 'id', + 'nickname', + 'username', + 'email', + 'last_login', + 'date_joined', + 'source') + list_display_links = ('id', 'username') + ordering = ('-id',) diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..fce4137 --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,117 @@ +from django import forms +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.core.exceptions import ValidationError +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ +from . import utils +from .models import BlogUser + + +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['password'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + + +class RegisterForm(UserCreationForm): + def __init__(self, *args, **kwargs): + super(RegisterForm, self).__init__(*args, **kwargs) + + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) + self.fields['password1'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + self.fields['password2'].widget = widgets.PasswordInput( + attrs={'placeholder': "repeat password", "class": "form-control"}) + + def clean_email(self): + email = self.cleaned_data['email'] + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError(_("email already exists")) + return email + + class Meta: + model = get_user_model() + fields = ("username", "email") + + +class ForgetPasswordForm(forms.Form): + new_password1 = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("New password") + } + ), + ) + + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") + } + ), + ) + + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") + } + ), + ) + + code = forms.CharField( + label=_('Code'), + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") + } + ), + ) + + def clean_new_password2(self): + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) + password_validation.validate_password(password2) + + return password2 + + def clean_email(self): + user_email = self.cleaned_data.get("email") + if not BlogUser.objects.filter( + email=user_email + ).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + raise ValidationError(_("email does not exist")) + return user_email + + def clean_code(self): + code = self.cleaned_data.get("code") + error = utils.verify( + email=self.cleaned_data.get("email"), + code=code, + ) + if error: + raise ValidationError(error) + return code + + +class ForgetPasswordCodeForm(forms.Form): + email = forms.EmailField( + label=_('Email'), + ) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..d2fbcab --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='BlogUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + 'ordering': ['-id'], + 'get_latest_by': 'id', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/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 new file mode 100644 index 0000000..1a9f509 --- /dev/null +++ b/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bloguser', + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, + ), + migrations.RemoveField( + model_name='bloguser', + name='created_time', + ), + migrations.RemoveField( + model_name='bloguser', + name='last_mod_time', + ), + migrations.AddField( + model_name='bloguser', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='bloguser', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='bloguser', + name='nickname', + field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), + ), + migrations.AlterField( + model_name='bloguser', + name='source', + field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..3baddbb --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,35 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from djangoblog.utils import get_current_site + + +# Create your models here. + +class BlogUser(AbstractUser): + nickname = models.CharField(_('nick name'), max_length=100, blank=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + source = models.CharField(_('create source'), max_length=100, blank=True) + + def get_absolute_url(self): + return reverse( + 'blog:author_detail', kwargs={ + 'author_name': self.username}) + + def __str__(self): + return self.email + + def get_full_url(self): + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + ordering = ['-id'] + verbose_name = _('user') + verbose_name_plural = verbose_name + get_latest_by = 'id' diff --git a/accounts/templatetags/__init__.py b/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..6893411 --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,207 @@ +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from accounts.models import BlogUser +from blog.models import Article, Category +from djangoblog.utils import * +from . import utils + + +# Create your tests here. + +class AccountTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + self.blog_user = BlogUser.objects.create_user( + username="test", + email="admin@admin.com", + password="12345678" + ) + self.new_test = "xxx123--=" + + def test_validate_account(self): + site = get_current_site().domain + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="qwer!@#$ggg") + testuser = BlogUser.objects.get(username='liangliangyy1') + + loginresult = self.client.login( + username='liangliangyy1', + password='qwer!@#$ggg') + self.assertEqual(loginresult, True) + response = self.client.get('/admin/') + self.assertEqual(response.status_code, 200) + + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.title = "nicetitleaaa" + article.body = "nicecontentaaa" + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_register(self): + self.assertEquals( + 0, len( + BlogUser.objects.filter( + email='user123@user.com'))) + response = self.client.post(reverse('account:register'), { + 'username': 'user1233', + 'email': 'user123@user.com', + 'password1': 'password123!q@wE#R$T', + 'password2': 'password123!q@wE#R$T', + }) + self.assertEquals( + 1, len( + BlogUser.objects.filter( + email='user123@user.com'))) + user = BlogUser.objects.filter(email='user123@user.com')[0] + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + path = reverse('accounts:result') + url = '{path}?type=validation&id={id}&sign={sign}'.format( + path=path, id=user.id, sign=sign) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.client.login(username='user1233', password='password123!q@wE#R$T') + user = BlogUser.objects.filter(email='user123@user.com')[0] + user.is_superuser = True + user.is_staff = True + user.save() + delete_sidebar_cache() + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.category = category + article.title = "nicetitle333" + article.body = "nicecontentttt" + article.author = user + + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse('account:logout')) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.post(reverse('account:login'), { + 'username': 'user1233', + 'password': 'password123' + }) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + def test_verify_email_code(self): + to_email = "admin@admin.com" + code = generate_code() + utils.set_code(to_email, code) + utils.send_verify_email(to_email, code) + + err = utils.verify("admin@admin.com", code) + self.assertEqual(err, None) + + err = utils.verify("admin@123.com", code) + self.assertEqual(type(err), str) + + def test_forget_password_email_code_success(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@admin.com") + ) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content.decode("utf-8"), "ok") + + def test_forget_password_email_code_fail(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict() + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@com") + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + def test_forget_password_email_success(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code=code, + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + self.assertEqual(resp.status_code, 302) + + # 验证用户密码是否修改成功 + blog_user = BlogUser.objects.filter( + email=self.blog_user.email, + ).first() # type: BlogUser + self.assertNotEqual(blog_user, None) + self.assertEqual(blog_user.check_password(data["new_password1"]), True) + + def test_forget_password_email_not_user(self): + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email="123@123.com", + code="123456", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + + + def test_forget_password_email_code_error(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code="111111", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..107a801 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,28 @@ +from django.urls import path +from django.urls import re_path + +from . import views +from .forms import LoginForm + +app_name = "accounts" + +urlpatterns = [re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + path(r'account/result.html', + views.account_result, + name='result'), + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), + ] diff --git a/accounts/user_login_backend.py b/accounts/user_login_backend.py new file mode 100644 index 0000000..73cdca1 --- /dev/null +++ b/accounts/user_login_backend.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailOrUsernameModelBackend(ModelBackend): + """ + 允许使用用户名或邮箱登录 + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + if '@' in username: + kwargs = {'email': username} + else: + kwargs = {'username': username} + try: + user = get_user_model().objects.get(**kwargs) + if user.check_password(password): + return user + except get_user_model().DoesNotExist: + return None + + def get_user(self, username): + try: + return get_user_model().objects.get(pk=username) + except get_user_model().DoesNotExist: + return None diff --git a/accounts/utils.py b/accounts/utils.py new file mode 100644 index 0000000..4b94bdf --- /dev/null +++ b/accounts/utils.py @@ -0,0 +1,49 @@ +import typing +from datetime import timedelta + +from django.core.cache import cache +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ + +from djangoblog.utils import send_email + +_code_ttl = timedelta(minutes=5) + + +def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): + """发送重设密码验证码 + Args: + to_mail: 接受邮箱 + subject: 邮件主题 + code: 验证码 + """ + html_content = _( + "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " + "properly") % {'code': code} + send_email([to_mail], subject, html_content) + + +def verify(email: str, code: str) -> typing.Optional[str]: + """验证code是否有效 + Args: + email: 请求邮箱 + code: 验证码 + Return: + 如果有错误就返回错误str + Node: + 这里的错误处理不太合理,应该采用raise抛出 + 否测调用方也需要对error进行处理 + """ + cache_code = get_code(email) + if cache_code != code: + return gettext("Verification code error") + + +def set_code(email: str, code: str): + """设置code""" + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """获取code""" + return cache.get(email) diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..ae67aec --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,204 @@ +import logging +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import get_user_model +from django.contrib.auth import logout +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.hashers import make_password +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +from django.views import View +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic import FormView, RedirectView + +from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache +from . import utils +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from .models import BlogUser + +logger = logging.getLogger(__name__) + + +# Create your views here. + +class RegisterView(FormView): + form_class = RegisterForm + template_name = 'account/registration_form.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(RegisterView, self).dispatch(*args, **kwargs) + + def form_valid(self, form): + if form.is_valid(): + user = form.save(False) + user.is_active = False + user.source = 'Register' + user.save(True) + site = get_current_site().domain + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('account:result') + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( + site=site, path=path, id=user.id, sign=sign) + + content = """ +

请点击下面链接验证您的邮箱

+ + {url} + + 再次感谢您! +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + send_email( + emailto=[ + user.email, + ], + title='验证您的电子邮箱', + content=content) + + url = reverse('accounts:result') + \ + '?type=register&id=' + str(user.id) + return HttpResponseRedirect(url) + else: + return self.render_to_response({ + 'form': form + }) + + +class LogoutView(RedirectView): + url = '/login/' + + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + return super(LogoutView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + logout(request) + delete_sidebar_cache() + return super(LogoutView, self).get(request, *args, **kwargs) + + +class LoginView(FormView): + form_class = LoginForm + template_name = 'account/login.html' + success_url = '/' + redirect_field_name = REDIRECT_FIELD_NAME + login_ttl = 2626560 # 一个月的时间 + + @method_decorator(sensitive_post_parameters('password')) + @method_decorator(csrf_protect) + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + + return super(LoginView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + redirect_to = self.request.GET.get(self.redirect_field_name) + if redirect_to is None: + redirect_to = '/' + kwargs['redirect_to'] = redirect_to + + return super(LoginView, self).get_context_data(**kwargs) + + def form_valid(self, form): + form = AuthenticationForm(data=self.request.POST, request=self.request) + + if form.is_valid(): + delete_sidebar_cache() + logger.info(self.redirect_field_name) + + auth.login(self.request, form.get_user()) + if self.request.POST.get("remember"): + self.request.session.set_expiry(self.login_ttl) + return super(LoginView, self).form_valid(form) + # return HttpResponseRedirect('/') + else: + return self.render_to_response({ + 'form': form + }) + + def get_success_url(self): + + redirect_to = self.request.POST.get(self.redirect_field_name) + if not url_has_allowed_host_and_scheme( + url=redirect_to, allowed_hosts=[ + self.request.get_host()]): + redirect_to = self.success_url + return redirect_to + + +def account_result(request): + type = request.GET.get('type') + id = request.GET.get('id') + + user = get_object_or_404(get_user_model(), id=id) + logger.info(type) + if user.is_active: + return HttpResponseRedirect('/') + if type and type in ['register', 'validation']: + if type == 'register': + content = ''' + 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 + ''' + title = '注册成功' + else: + c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + sign = request.GET.get('sign') + if sign != c_sign: + return HttpResponseForbidden() + user.is_active = True + user.save() + content = ''' + 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 + ''' + title = '验证成功' + return render(request, 'account/result.html', { + 'title': title, + 'content': content + }) + else: + return HttpResponseRedirect('/') + + +class ForgetPasswordView(FormView): + form_class = ForgetPasswordForm + template_name = 'account/forget_password.html' + + def form_valid(self, form): + if form.is_valid(): + blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + blog_user.password = make_password(form.cleaned_data["new_password2"]) + blog_user.save() + return HttpResponseRedirect('/login/') + else: + return self.render_to_response({'form': form}) + + +class ForgetPasswordEmailCode(View): + + def post(self, request: HttpRequest): + form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): + return HttpResponse("错误的邮箱") + to_email = form.cleaned_data["email"] + + code = generate_code() + utils.send_verify_email(to_email, code) + utils.set_code(to_email, code) + + return HttpResponse("ok") diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000..46c3420 --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,112 @@ +from django import forms +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import Article + + +class ArticleForm(forms.ModelForm): + # body = forms.CharField(widget=AdminPagedownWidget()) + + class Meta: + model = Article + fields = '__all__' + + +def makr_article_publish(modeladmin, request, queryset): + queryset.update(status='p') + + +def draft_article(modeladmin, request, queryset): + queryset.update(status='d') + + +def close_article_commentstatus(modeladmin, request, queryset): + queryset.update(comment_status='c') + + +def open_article_commentstatus(modeladmin, request, queryset): + queryset.update(comment_status='o') + + +makr_article_publish.short_description = _('Publish selected articles') +draft_article.short_description = _('Draft selected articles') +close_article_commentstatus.short_description = _('Close article comments') +open_article_commentstatus.short_description = _('Open article comments') + + +class ArticlelAdmin(admin.ModelAdmin): + list_per_page = 20 + search_fields = ('body', 'title') + form = ArticleForm + list_display = ( + 'id', + 'title', + 'author', + 'link_to_category', + 'creation_time', + 'views', + 'status', + 'type', + 'article_order') + list_display_links = ('id', 'title') + list_filter = ('status', 'type', 'category') + filter_horizontal = ('tags',) + exclude = ('creation_time', 'last_modify_time') + view_on_site = True + actions = [ + makr_article_publish, + draft_article, + close_article_commentstatus, + open_article_commentstatus] + + def link_to_category(self, obj): + info = (obj.category._meta.app_label, obj.category._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + return format_html(u'%s' % (link, obj.category.name)) + + link_to_category.short_description = _('category') + + def get_form(self, request, obj=None, **kwargs): + form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + form.base_fields['author'].queryset = get_user_model( + ).objects.filter(is_superuser=True) + return form + + def save_model(self, request, obj, form, change): + super(ArticlelAdmin, self).save_model(request, obj, form, change) + + def get_view_on_site_url(self, obj=None): + if obj: + url = obj.get_full_url() + return url + else: + from djangoblog.utils import get_current_site + site = get_current_site().domain + return site + + +class TagAdmin(admin.ModelAdmin): + exclude = ('slug', 'last_mod_time', 'creation_time') + + +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'parent_category', 'index') + exclude = ('slug', 'last_mod_time', 'creation_time') + + +class LinksAdmin(admin.ModelAdmin): + exclude = ('last_mod_time', 'creation_time') + + +class SideBarAdmin(admin.ModelAdmin): + list_display = ('name', 'content', 'is_enable', 'sequence') + exclude = ('last_mod_time', 'creation_time') + + +class BlogSettingsAdmin(admin.ModelAdmin): + pass diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000..7930587 --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + name = 'blog' diff --git a/blog/context_processors.py b/blog/context_processors.py new file mode 100644 index 0000000..73e3088 --- /dev/null +++ b/blog/context_processors.py @@ -0,0 +1,43 @@ +import logging + +from django.utils import timezone + +from djangoblog.utils import cache, get_blog_setting +from .models import Category, Article + +logger = logging.getLogger(__name__) + + +def seo_processor(requests): + key = 'seo_processor' + value = cache.get(key) + if value: + return value + else: + logger.info('set processor cache.') + setting = get_blog_setting() + value = { + 'SITE_NAME': setting.site_name, + 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, + 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, + 'SITE_SEO_DESCRIPTION': setting.site_seo_description, + 'SITE_DESCRIPTION': setting.site_description, + 'SITE_KEYWORDS': setting.site_keywords, + 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', + 'ARTICLE_SUB_LENGTH': setting.article_sub_length, + 'nav_category_list': Category.objects.all(), + 'nav_pages': Article.objects.filter( + type='p', + status='p'), + 'OPEN_SITE_COMMENT': setting.open_site_comment, + 'BEIAN_CODE': setting.beian_code, + 'ANALYTICS_CODE': setting.analytics_code, + "BEIAN_CODE_GONGAN": setting.gongan_beiancode, + "SHOW_GONGAN_CODE": setting.show_gongan_code, + "CURRENT_YEAR": timezone.now().year, + "GLOBAL_HEADER": setting.global_header, + "GLOBAL_FOOTER": setting.global_footer, + "COMMENT_NEED_REVIEW": setting.comment_need_review, + } + cache.set(key, value, 60 * 60 * 10) + return value diff --git a/blog/documents.py b/blog/documents.py new file mode 100644 index 0000000..0f1db7b --- /dev/null +++ b/blog/documents.py @@ -0,0 +1,213 @@ +import time + +import elasticsearch.client +from django.conf import settings +from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean +from elasticsearch_dsl.connections import connections + +from blog.models import Article + +ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') + +if ELASTICSEARCH_ENABLED: + connections.create_connection( + hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + from elasticsearch import Elasticsearch + + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + from elasticsearch.client import IngestClient + + c = IngestClient(es) + try: + c.get_pipeline('geoip') + except elasticsearch.exceptions.NotFoundError: + c.put_pipeline('geoip', body='''{ + "description" : "Add geoip info", + "processors" : [ + { + "geoip" : { + "field" : "ip" + } + } + ] + }''') + + +class GeoIp(InnerDoc): + continent_name = Keyword() + country_iso_code = Keyword() + country_name = Keyword() + location = GeoPoint() + + +class UserAgentBrowser(InnerDoc): + Family = Keyword() + Version = Keyword() + + +class UserAgentOS(UserAgentBrowser): + pass + + +class UserAgentDevice(InnerDoc): + Family = Keyword() + Brand = Keyword() + Model = Keyword() + + +class UserAgent(InnerDoc): + browser = Object(UserAgentBrowser, required=False) + os = Object(UserAgentOS, required=False) + device = Object(UserAgentDevice, required=False) + string = Text() + is_bot = Boolean() + + +class ElapsedTimeDocument(Document): + url = Keyword() + time_taken = Long() + log_datetime = Date() + ip = Keyword() + geoip = Object(GeoIp, required=False) + useragent = Object(UserAgent, required=False) + + class Index: + name = 'performance' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } + + class Meta: + doc_type = 'ElapsedTime' + + +class ElaspedTimeDocumentManager: + @staticmethod + def build_index(): + from elasticsearch import Elasticsearch + client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + res = client.indices.exists(index="performance") + if not res: + ElapsedTimeDocument.init() + + @staticmethod + def delete_index(): + from elasticsearch import Elasticsearch + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + es.indices.delete(index='performance', ignore=[400, 404]) + + @staticmethod + def create(url, time_taken, log_datetime, useragent, ip): + ElaspedTimeDocumentManager.build_index() + ua = UserAgent() + ua.browser = UserAgentBrowser() + ua.browser.Family = useragent.browser.family + ua.browser.Version = useragent.browser.version_string + + ua.os = UserAgentOS() + ua.os.Family = useragent.os.family + ua.os.Version = useragent.os.version_string + + ua.device = UserAgentDevice() + ua.device.Family = useragent.device.family + ua.device.Brand = useragent.device.brand + ua.device.Model = useragent.device.model + ua.string = useragent.ua_string + ua.is_bot = useragent.is_bot + + doc = ElapsedTimeDocument( + meta={ + 'id': int( + round( + time.time() * + 1000)) + }, + url=url, + time_taken=time_taken, + log_datetime=log_datetime, + useragent=ua, ip=ip) + doc.save(pipeline="geoip") + + +class ArticleDocument(Document): + body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + author = Object(properties={ + 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + category = Object(properties={ + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + tags = Object(properties={ + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + + pub_time = Date() + status = Text() + comment_status = Text() + type = Text() + views = Integer() + article_order = Integer() + + class Index: + name = 'blog' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } + + class Meta: + doc_type = 'Article' + + +class ArticleDocumentManager(): + + def __init__(self): + self.create_index() + + def create_index(self): + ArticleDocument.init() + + def delete_index(self): + from elasticsearch import Elasticsearch + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + es.indices.delete(index='blog', ignore=[400, 404]) + + def convert_to_doc(self, articles): + return [ + ArticleDocument( + meta={ + 'id': article.id}, + body=article.body, + title=article.title, + author={ + 'nickname': article.author.username, + 'id': article.author.id}, + category={ + 'name': article.category.name, + 'id': article.category.id}, + tags=[ + { + 'name': t.name, + 'id': t.id} for t in article.tags.all()], + pub_time=article.pub_time, + status=article.status, + comment_status=article.comment_status, + type=article.type, + views=article.views, + article_order=article.article_order) for article in articles] + + def rebuild(self, articles=None): + ArticleDocument.init() + articles = articles if articles else Article.objects.all() + docs = self.convert_to_doc(articles) + for doc in docs: + doc.save() + + def update_docs(self, docs): + for doc in docs: + doc.save() diff --git a/blog/forms.py b/blog/forms.py new file mode 100644 index 0000000..715be76 --- /dev/null +++ b/blog/forms.py @@ -0,0 +1,19 @@ +import logging + +from django import forms +from haystack.forms import SearchForm + +logger = logging.getLogger(__name__) + + +class BlogSearchForm(SearchForm): + querydata = forms.CharField(required=True) + + def search(self): + datas = super(BlogSearchForm, self).search() + if not self.is_valid(): + return self.no_query_found() + + if self.cleaned_data['querydata']: + logger.info(self.cleaned_data['querydata']) + return datas diff --git a/blog/management/__init__.py b/blog/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/management/commands/__init__.py b/blog/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/management/commands/build_index.py b/blog/management/commands/build_index.py new file mode 100644 index 0000000..3c4acd7 --- /dev/null +++ b/blog/management/commands/build_index.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand + +from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ + ELASTICSEARCH_ENABLED + + +# TODO 参数化 +class Command(BaseCommand): + help = 'build search index' + + def handle(self, *args, **options): + if ELASTICSEARCH_ENABLED: + ElaspedTimeDocumentManager.build_index() + manager = ElapsedTimeDocument() + manager.init() + manager = ArticleDocumentManager() + manager.delete_index() + manager.rebuild() diff --git a/blog/management/commands/build_search_words.py b/blog/management/commands/build_search_words.py new file mode 100644 index 0000000..cfe7e0d --- /dev/null +++ b/blog/management/commands/build_search_words.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from blog.models import Tag, Category + + +# TODO 参数化 +class Command(BaseCommand): + help = 'build search words' + + def handle(self, *args, **options): + datas = set([t.name for t in Tag.objects.all()] + + [t.name for t in Category.objects.all()]) + print('\n'.join(datas)) diff --git a/blog/management/commands/clear_cache.py b/blog/management/commands/clear_cache.py new file mode 100644 index 0000000..0d66172 --- /dev/null +++ b/blog/management/commands/clear_cache.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from djangoblog.utils import cache + + +class Command(BaseCommand): + help = 'clear the whole cache' + + def handle(self, *args, **options): + cache.clear() + self.stdout.write(self.style.SUCCESS('Cleared cache\n')) diff --git a/blog/management/commands/create_testdata.py b/blog/management/commands/create_testdata.py new file mode 100644 index 0000000..675d2ba --- /dev/null +++ b/blog/management/commands/create_testdata.py @@ -0,0 +1,40 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password +from django.core.management.base import BaseCommand + +from blog.models import Article, Tag, Category + + +class Command(BaseCommand): + help = 'create test datas' + + def handle(self, *args, **options): + user = get_user_model().objects.get_or_create( + email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + + pcategory = Category.objects.get_or_create( + name='我是父类目', parent_category=None)[0] + + category = Category.objects.get_or_create( + name='子类目', parent_category=pcategory)[0] + + category.save() + basetag = Tag() + basetag.name = "标签" + basetag.save() + for i in range(1, 20): + article = Article.objects.get_or_create( + category=category, + title='nice title ' + str(i), + body='nice content ' + str(i), + author=user)[0] + tag = Tag() + tag.name = "标签" + str(i) + tag.save() + article.tags.add(tag) + article.tags.add(basetag) + article.save() + + from djangoblog.utils import cache + cache.clear() + self.stdout.write(self.style.SUCCESS('created test datas \n')) diff --git a/blog/management/commands/ping_baidu.py b/blog/management/commands/ping_baidu.py new file mode 100644 index 0000000..2c7fbdd --- /dev/null +++ b/blog/management/commands/ping_baidu.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand + +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import get_current_site +from blog.models import Article, Tag, Category + +site = get_current_site().domain + + +class Command(BaseCommand): + help = 'notify baidu url' + + def add_arguments(self, parser): + parser.add_argument( + 'data_type', + type=str, + choices=[ + 'all', + 'article', + 'tag', + 'category'], + help='article : all article,tag : all tag,category: all category,all: All of these') + + def get_full_url(self, path): + url = "https://{site}{path}".format(site=site, path=path) + return url + + def handle(self, *args, **options): + type = options['data_type'] + self.stdout.write('start get %s' % type) + + urls = [] + if type == 'article' or type == 'all': + for article in Article.objects.filter(status='p'): + urls.append(article.get_full_url()) + if type == 'tag' or type == 'all': + for tag in Tag.objects.all(): + url = tag.get_absolute_url() + urls.append(self.get_full_url(url)) + if type == 'category' or type == 'all': + for category in Category.objects.all(): + url = category.get_absolute_url() + urls.append(self.get_full_url(url)) + + self.stdout.write( + self.style.SUCCESS( + 'start notify %d urls' % + len(urls))) + SpiderNotify.baidu_notify(urls) + self.stdout.write(self.style.SUCCESS('finish notify')) diff --git a/blog/management/commands/sync_user_avatar.py b/blog/management/commands/sync_user_avatar.py new file mode 100644 index 0000000..d0f4612 --- /dev/null +++ b/blog/management/commands/sync_user_avatar.py @@ -0,0 +1,47 @@ +import requests +from django.core.management.base import BaseCommand +from django.templatetags.static import static + +from djangoblog.utils import save_user_avatar +from oauth.models import OAuthUser +from oauth.oauthmanager import get_manager_by_type + + +class Command(BaseCommand): + help = 'sync user avatar' + + def test_picture(self, url): + try: + if requests.get(url, timeout=2).status_code == 200: + return True + except: + pass + + def handle(self, *args, **options): + static_url = static("../") + users = OAuthUser.objects.all() + self.stdout.write(f'开始同步{len(users)}个用户头像') + for u in users: + self.stdout.write(f'开始同步:{u.nickname}') + url = u.picture + if url: + if url.startswith(static_url): + if self.test_picture(url): + continue + else: + if u.metadata: + manage = get_manager_by_type(u.type) + url = manage.get_picture(u.metadata) + url = save_user_avatar(url) + else: + url = static('blog/img/avatar.png') + else: + url = save_user_avatar(url) + else: + url = static('blog/img/avatar.png') + if url: + self.stdout.write( + f'结束同步:{u.nickname}.url:{url}') + u.picture = url + u.save() + self.stdout.write('结束同步') diff --git a/blog/middleware.py b/blog/middleware.py new file mode 100644 index 0000000..94dd70c --- /dev/null +++ b/blog/middleware.py @@ -0,0 +1,42 @@ +import logging +import time + +from ipware import get_client_ip +from user_agents import parse + +from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager + +logger = logging.getLogger(__name__) + + +class OnlineMiddleware(object): + def __init__(self, get_response=None): + self.get_response = get_response + super().__init__() + + def __call__(self, request): + ''' page render time ''' + start_time = time.time() + response = self.get_response(request) + http_user_agent = request.META.get('HTTP_USER_AGENT', '') + ip, _ = get_client_ip(request) + user_agent = parse(http_user_agent) + if not response.streaming: + try: + cast_time = time.time() - start_time + if ELASTICSEARCH_ENABLED: + time_taken = round((cast_time) * 1000, 2) + url = request.path + from django.utils import timezone + ElaspedTimeDocumentManager.create( + url=url, + time_taken=time_taken, + log_datetime=timezone.now(), + useragent=user_agent, + ip=ip) + response.content = response.content.replace( + b'', str.encode(str(cast_time)[:5])) + except Exception as e: + logger.error("Error OnlineMiddleware: %s" % e) + + return response diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000..3d391b6 --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,137 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import mdeditor.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BlogSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), + ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), + ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), + ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), + ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), + ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), + ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), + ('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')), + ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')), + ('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')), + ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')), + ('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')), + ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), + ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), + ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), + ], + options={ + 'verbose_name': '网站配置', + 'verbose_name_plural': '网站配置', + }, + ), + migrations.CreateModel( + name='Links', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), + ('link', models.URLField(verbose_name='链接地址')), + ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '友情链接', + 'verbose_name_plural': '友情链接', + 'ordering': ['sequence'], + }, + ), + migrations.CreateModel( + name='SideBar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='标题')), + ('content', models.TextField(verbose_name='内容')), + ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '侧边栏', + 'verbose_name_plural': '侧边栏', + 'ordering': ['sequence'], + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ], + options={ + 'verbose_name': '标签', + 'verbose_name_plural': '标签', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), + ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), + ], + options={ + 'verbose_name': '分类', + 'verbose_name_plural': '分类', + 'ordering': ['-index'], + }, + ), + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), + ('body', mdeditor.fields.MDTextField(verbose_name='正文')), + ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), + ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), + ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), + ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')), + ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), + ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), + ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), + ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), + ], + options={ + 'verbose_name': '文章', + 'verbose_name_plural': '文章', + 'ordering': ['-article_order', '-pub_time'], + 'get_latest_by': 'id', + }, + ), + ] diff --git a/blog/migrations/0002_blogsettings_global_footer_and_more.py b/blog/migrations/0002_blogsettings_global_footer_and_more.py new file mode 100644 index 0000000..adbaa36 --- /dev/null +++ b/blog/migrations/0002_blogsettings_global_footer_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-03-29 06:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='blogsettings', + name='global_footer', + field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), + ), + migrations.AddField( + model_name='blogsettings', + name='global_header', + field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), + ), + ] diff --git a/blog/migrations/0003_blogsettings_comment_need_review.py b/blog/migrations/0003_blogsettings_comment_need_review.py new file mode 100644 index 0000000..e9f5502 --- /dev/null +++ b/blog/migrations/0003_blogsettings_comment_need_review.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-05-09 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('blog', '0002_blogsettings_global_footer_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='blogsettings', + name='comment_need_review', + field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), + ), + ] diff --git a/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py new file mode 100644 index 0000000..ceb1398 --- /dev/null +++ b/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.1 on 2023-05-09 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('blog', '0003_blogsettings_comment_need_review'), + ] + + operations = [ + migrations.RenameField( + model_name='blogsettings', + old_name='analyticscode', + new_name='analytics_code', + ), + migrations.RenameField( + model_name='blogsettings', + old_name='beiancode', + new_name='beian_code', + ), + migrations.RenameField( + model_name='blogsettings', + old_name='sitename', + new_name='site_name', + ), + ] diff --git a/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 new file mode 100644 index 0000000..d08e853 --- /dev/null +++ b/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py @@ -0,0 +1,300 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import mdeditor.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, + ), + migrations.AlterModelOptions( + name='category', + options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, + ), + migrations.AlterModelOptions( + name='links', + options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, + ), + migrations.AlterModelOptions( + name='sidebar', + options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, + ), + migrations.AlterModelOptions( + name='tag', + options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, + ), + migrations.RemoveField( + model_name='article', + name='created_time', + ), + migrations.RemoveField( + model_name='article', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='category', + name='created_time', + ), + migrations.RemoveField( + model_name='category', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='links', + name='created_time', + ), + migrations.RemoveField( + model_name='sidebar', + name='created_time', + ), + migrations.RemoveField( + model_name='tag', + name='created_time', + ), + migrations.RemoveField( + model_name='tag', + name='last_mod_time', + ), + migrations.AddField( + model_name='article', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='article', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AddField( + model_name='category', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='category', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AddField( + model_name='links', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='sidebar', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='tag', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='tag', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='article', + name='article_order', + field=models.IntegerField(default=0, verbose_name='order'), + ), + migrations.AlterField( + model_name='article', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + ), + migrations.AlterField( + model_name='article', + name='body', + field=mdeditor.fields.MDTextField(verbose_name='body'), + ), + migrations.AlterField( + model_name='article', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), + ), + migrations.AlterField( + model_name='article', + name='comment_status', + field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'), + ), + migrations.AlterField( + model_name='article', + name='pub_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), + ), + migrations.AlterField( + model_name='article', + name='show_toc', + field=models.BooleanField(default=False, verbose_name='show toc'), + ), + migrations.AlterField( + model_name='article', + name='status', + field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), + ), + migrations.AlterField( + model_name='article', + name='tags', + field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), + ), + migrations.AlterField( + model_name='article', + name='title', + field=models.CharField(max_length=200, unique=True, verbose_name='title'), + ), + migrations.AlterField( + model_name='article', + name='type', + field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), + ), + migrations.AlterField( + model_name='article', + name='views', + field=models.PositiveIntegerField(default=0, verbose_name='views'), + ), + migrations.AlterField( + model_name='blogsettings', + name='article_comment_count', + field=models.IntegerField(default=5, verbose_name='article comment count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='article_sub_length', + field=models.IntegerField(default=300, verbose_name='article sub length'), + ), + migrations.AlterField( + model_name='blogsettings', + name='google_adsense_codes', + field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), + ), + migrations.AlterField( + model_name='blogsettings', + name='open_site_comment', + field=models.BooleanField(default=True, verbose_name='open site comment'), + ), + migrations.AlterField( + model_name='blogsettings', + name='show_google_adsense', + field=models.BooleanField(default=False, verbose_name='show adsense'), + ), + migrations.AlterField( + model_name='blogsettings', + name='sidebar_article_count', + field=models.IntegerField(default=10, verbose_name='sidebar article count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='sidebar_comment_count', + field=models.IntegerField(default=5, verbose_name='sidebar comment count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_description', + field=models.TextField(default='', max_length=1000, verbose_name='site description'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_keywords', + field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_name', + field=models.CharField(default='', max_length=200, verbose_name='site name'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_seo_description', + field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), + ), + migrations.AlterField( + model_name='category', + name='index', + field=models.IntegerField(default=0, verbose_name='index'), + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='category name'), + ), + migrations.AlterField( + model_name='category', + name='parent_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), + ), + migrations.AlterField( + model_name='links', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is show'), + ), + migrations.AlterField( + model_name='links', + name='last_mod_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='links', + name='link', + field=models.URLField(verbose_name='link'), + ), + migrations.AlterField( + model_name='links', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='link name'), + ), + migrations.AlterField( + model_name='links', + name='sequence', + field=models.IntegerField(unique=True, verbose_name='order'), + ), + migrations.AlterField( + model_name='links', + name='show_type', + field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), + ), + migrations.AlterField( + model_name='sidebar', + name='content', + field=models.TextField(verbose_name='content'), + ), + migrations.AlterField( + model_name='sidebar', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is enable'), + ), + migrations.AlterField( + model_name='sidebar', + name='last_mod_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='sidebar', + name='name', + field=models.CharField(max_length=100, verbose_name='title'), + ), + migrations.AlterField( + model_name='sidebar', + name='sequence', + field=models.IntegerField(unique=True, verbose_name='order'), + ), + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), + ), + ] diff --git a/blog/migrations/0006_alter_blogsettings_options.py b/blog/migrations/0006_alter_blogsettings_options.py new file mode 100644 index 0000000..e36feb4 --- /dev/null +++ b/blog/migrations/0006_alter_blogsettings_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-01-26 02:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0005_alter_article_options_alter_category_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='blogsettings', + options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, + ), + ] diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000..083788b --- /dev/null +++ b/blog/models.py @@ -0,0 +1,376 @@ +import logging +import re +from abc import abstractmethod + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from mdeditor.fields import MDTextField +from uuslug import slugify + +from djangoblog.utils import cache_decorator, cache +from djangoblog.utils import get_current_site + +logger = logging.getLogger(__name__) + + +class LinkShowType(models.TextChoices): + I = ('i', _('index')) + L = ('l', _('list')) + P = ('p', _('post')) + A = ('a', _('all')) + S = ('s', _('slide')) + + +class BaseModel(models.Model): + id = models.AutoField(primary_key=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('modify time'), default=now) + + def save(self, *args, **kwargs): + is_update_views = isinstance( + self, + Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + if is_update_views: + Article.objects.filter(pk=self.pk).update(views=self.views) + else: + if 'slug' in self.__dict__: + slug = getattr( + self, 'title') if 'title' in self.__dict__ else getattr( + self, 'name') + setattr(self, 'slug', slugify(slug)) + super().save(*args, **kwargs) + + def get_full_url(self): + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + abstract = True + + @abstractmethod + def get_absolute_url(self): + pass + + +class Article(BaseModel): + """文章""" + STATUS_CHOICES = ( + ('d', _('Draft')), + ('p', _('Published')), + ) + COMMENT_STATUS = ( + ('o', _('Open')), + ('c', _('Close')), + ) + TYPE = ( + ('a', _('Article')), + ('p', _('Page')), + ) + title = models.CharField(_('title'), max_length=200, unique=True) + body = MDTextField(_('body')) + pub_time = models.DateTimeField( + _('publish time'), blank=False, null=False, default=now) + status = models.CharField( + _('status'), + max_length=1, + choices=STATUS_CHOICES, + default='p') + comment_status = models.CharField( + _('comment status'), + max_length=1, + choices=COMMENT_STATUS, + default='o') + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') + views = models.PositiveIntegerField(_('views'), default=0) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + blank=False, + null=False, + on_delete=models.CASCADE) + article_order = models.IntegerField( + _('order'), blank=False, null=False, default=0) + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + category = models.ForeignKey( + 'Category', + verbose_name=_('category'), + on_delete=models.CASCADE, + blank=False, + null=False) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + + def body_to_string(self): + return self.body + + def __str__(self): + return self.title + + class Meta: + ordering = ['-article_order', '-pub_time'] + verbose_name = _('article') + verbose_name_plural = verbose_name + get_latest_by = 'id' + + def get_absolute_url(self): + return reverse('blog:detailbyid', kwargs={ + 'article_id': self.id, + 'year': self.creation_time.year, + 'month': self.creation_time.month, + 'day': self.creation_time.day + }) + + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + tree = self.category.get_category_tree() + names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) + + return names + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + def viewed(self): + self.views += 1 + self.save(update_fields=['views']) + + def comment_list(self): + cache_key = 'article_comments_{id}'.format(id=self.id) + value = cache.get(cache_key) + if value: + logger.info('get article comments:{id}'.format(id=self.id)) + return value + else: + comments = self.comment_set.filter(is_enable=True).order_by('-id') + cache.set(cache_key, comments, 60 * 100) + logger.info('set article comments:{id}'.format(id=self.id)) + return comments + + def get_admin_url(self): + info = (self._meta.app_label, self._meta.model_name) + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + + @cache_decorator(expiration=60 * 100) + def next_article(self): + # 下一篇 + return Article.objects.filter( + id__gt=self.id, status='p').order_by('id').first() + + @cache_decorator(expiration=60 * 100) + def prev_article(self): + # 前一篇 + return Article.objects.filter(id__lt=self.id, status='p').first() + + def get_first_image_url(self): + """ + Get the first image url from article.body. + :return: + """ + match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + if match: + return match.group(1) + return "" + + +class Category(BaseModel): + """文章分类""" + name = models.CharField(_('category name'), max_length=30, unique=True) + parent_category = models.ForeignKey( + 'self', + verbose_name=_('parent category'), + blank=True, + null=True, + on_delete=models.CASCADE) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + index = models.IntegerField(default=0, verbose_name=_('index')) + + class Meta: + ordering = ['-index'] + verbose_name = _('category') + verbose_name_plural = verbose_name + + def get_absolute_url(self): + return reverse( + 'blog:category_detail', kwargs={ + 'category_name': self.slug}) + + def __str__(self): + return self.name + + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + """ + 递归获得分类目录的父级 + :return: + """ + categorys = [] + + def parse(category): + categorys.append(category) + if category.parent_category: + parse(category.parent_category) + + parse(self) + return categorys + + @cache_decorator(60 * 60 * 10) + def get_sub_categorys(self): + """ + 获得当前分类目录所有子集 + :return: + """ + categorys = [] + all_categorys = Category.objects.all() + + def parse(category): + if category not in categorys: + categorys.append(category) + childs = all_categorys.filter(parent_category=category) + for child in childs: + if category not in categorys: + categorys.append(child) + parse(child) + + parse(self) + return categorys + + +class Tag(BaseModel): + """文章标签""" + name = models.CharField(_('tag name'), max_length=30, unique=True) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) + + @cache_decorator(60 * 60 * 10) + def get_article_count(self): + return Article.objects.filter(tags__name=self.name).distinct().count() + + class Meta: + ordering = ['name'] + verbose_name = _('tag') + verbose_name_plural = verbose_name + + +class Links(models.Model): + """友情链接""" + + name = models.CharField(_('link name'), max_length=30, unique=True) + link = models.URLField(_('link')) + sequence = models.IntegerField(_('order'), unique=True) + is_enable = models.BooleanField( + _('is show'), default=True, blank=False, null=False) + show_type = models.CharField( + _('show type'), + max_length=1, + choices=LinkShowType.choices, + default=LinkShowType.I) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + class Meta: + ordering = ['sequence'] + verbose_name = _('link') + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class SideBar(models.Model): + """侧边栏,可以展示一些html内容""" + name = models.CharField(_('title'), max_length=100) + content = models.TextField(_('content')) + sequence = models.IntegerField(_('order'), unique=True) + is_enable = models.BooleanField(_('is enable'), default=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + class Meta: + ordering = ['sequence'] + verbose_name = _('sidebar') + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class BlogSettings(models.Model): + """blog的配置""" + site_name = models.CharField( + _('site name'), + max_length=200, + null=False, + blank=False, + default='') + site_description = models.TextField( + _('site description'), + max_length=1000, + null=False, + blank=False, + default='') + site_seo_description = models.TextField( + _('site seo description'), max_length=1000, null=False, blank=False, default='') + site_keywords = models.TextField( + _('site keywords'), + max_length=1000, + null=False, + blank=False, + default='') + article_sub_length = models.IntegerField(_('article sub length'), default=300) + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) + article_comment_count = models.IntegerField(_('article comment count'), default=5) + show_google_adsense = models.BooleanField(_('show adsense'), default=False) + google_adsense_codes = models.TextField( + _('adsense code'), max_length=2000, null=True, blank=True, default='') + open_site_comment = models.BooleanField(_('open site comment'), default=True) + global_header = models.TextField("公共头部", null=True, blank=True, default='') + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + beian_code = models.CharField( + '备案号', + max_length=2000, + null=True, + blank=True, + default='') + analytics_code = models.TextField( + "网站统计代码", + max_length=1000, + null=False, + blank=False, + default='') + show_gongan_code = models.BooleanField( + '是否显示公安备案号', default=False, null=False) + gongan_beiancode = models.TextField( + '公安备案号', + max_length=2000, + null=True, + blank=True, + default='') + comment_need_review = models.BooleanField( + '评论是否需要审核', default=False, null=False) + + class Meta: + verbose_name = _('Website configuration') + verbose_name_plural = verbose_name + + def __str__(self): + return self.site_name + + def clean(self): + if BlogSettings.objects.exclude(id=self.id).count(): + raise ValidationError(_('There can only be one configuration')) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + from djangoblog.utils import cache + cache.clear() diff --git a/blog/search_indexes.py b/blog/search_indexes.py new file mode 100644 index 0000000..7f1dfac --- /dev/null +++ b/blog/search_indexes.py @@ -0,0 +1,13 @@ +from haystack import indexes + +from blog.models import Article + + +class ArticleIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + + def get_model(self): + return Article + + def index_queryset(self, using=None): + return self.get_model().objects.filter(status='p') diff --git a/blog/templatetags/__init__.py b/blog/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py new file mode 100644 index 0000000..d6cd5d5 --- /dev/null +++ b/blog/templatetags/blog_tags.py @@ -0,0 +1,344 @@ +import hashlib +import logging +import random +import urllib + +from django import template +from django.conf import settings +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.template.defaultfilters import stringfilter +from django.templatetags.static import static +from django.urls import reverse +from django.utils.safestring import mark_safe + +from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType +from comments.models import Comment +from djangoblog.utils import CommonMarkdown, sanitize_html +from djangoblog.utils import cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser +from djangoblog.plugin_manage import hooks + +logger = logging.getLogger(__name__) + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def head_meta(context): + return mark_safe(hooks.apply_filters('head_meta', '', context)) + + +@register.simple_tag +def timeformat(data): + try: + return data.strftime(settings.TIME_FORMAT) + except Exception as e: + logger.error(e) + return "" + + +@register.simple_tag +def datetimeformat(data): + try: + return data.strftime(settings.DATE_TIME_FORMAT) + except Exception as e: + logger.error(e) + return "" + + +@register.filter() +@stringfilter +def custom_markdown(content): + return mark_safe(CommonMarkdown.get_markdown(content)) + + +@register.simple_tag +def get_markdown_toc(content): + from djangoblog.utils import CommonMarkdown + body, toc = CommonMarkdown.get_markdown_with_toc(content) + return mark_safe(toc) + + +@register.filter() +@stringfilter +def comment_markdown(content): + content = CommonMarkdown.get_markdown(content) + return mark_safe(sanitize_html(content)) + + +@register.filter(is_safe=True) +@stringfilter +def truncatechars_content(content): + """ + 获得文章内容的摘要 + :param content: + :return: + """ + from django.template.defaultfilters import truncatechars_html + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + return truncatechars_html(content, blogsetting.article_sub_length) + + +@register.filter(is_safe=True) +@stringfilter +def truncate(content): + from django.utils.html import strip_tags + + return strip_tags(content)[:150] + + +@register.inclusion_tag('blog/tags/breadcrumb.html') +def load_breadcrumb(article): + """ + 获得文章面包屑 + :param article: + :return: + """ + names = article.get_category_tree() + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + site = get_current_site().domain + names.append((blogsetting.site_name, '/')) + names = names[::-1] + + return { + 'names': names, + 'title': article.title, + 'count': len(names) + 1 + } + + +@register.inclusion_tag('blog/tags/article_tag_list.html') +def load_articletags(article): + """ + 文章标签 + :param article: + :return: + """ + tags = article.tags.all() + tags_list = [] + for tag in tags: + url = tag.get_absolute_url() + count = tag.get_article_count() + tags_list.append(( + url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) + )) + return { + 'article_tags_list': tags_list + } + + +@register.inclusion_tag('blog/tags/sidebar.html') +def load_sidebar(user, linktype): + """ + 加载侧边栏 + :return: + """ + value = cache.get("sidebar" + linktype) + if value: + value['user'] = user + return value + else: + logger.info('load sidebar') + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + recent_articles = Article.objects.filter( + status='p')[:blogsetting.sidebar_article_count] + sidebar_categorys = Category.objects.all() + extra_sidebars = SideBar.objects.filter( + is_enable=True).order_by('sequence') + most_read_articles = Article.objects.filter(status='p').order_by( + '-views')[:blogsetting.sidebar_article_count] + dates = Article.objects.datetimes('creation_time', 'month', order='DESC') + links = Links.objects.filter(is_enable=True).filter( + Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) + commment_list = Comment.objects.filter(is_enable=True).order_by( + '-id')[:blogsetting.sidebar_comment_count] + # 标签云 计算字体大小 + # 根据总数计算出平均值 大小为 (数目/平均值)*步长 + increment = 5 + tags = Tag.objects.all() + sidebar_tags = None + if tags and len(tags) > 0: + s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] + count = sum([t[1] for t in s]) + dd = 1 if (count == 0 or not len(tags)) else count / len(tags) + import random + sidebar_tags = list( + map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) + random.shuffle(sidebar_tags) + + value = { + 'recent_articles': recent_articles, + 'sidebar_categorys': sidebar_categorys, + 'most_read_articles': most_read_articles, + 'article_dates': dates, + 'sidebar_comments': commment_list, + 'sidabar_links': links, + 'show_google_adsense': blogsetting.show_google_adsense, + 'google_adsense_codes': blogsetting.google_adsense_codes, + 'open_site_comment': blogsetting.open_site_comment, + 'show_gongan_code': blogsetting.show_gongan_code, + 'sidebar_tags': sidebar_tags, + 'extra_sidebars': extra_sidebars + } + cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) + logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) + value['user'] = user + return value + + +@register.inclusion_tag('blog/tags/article_meta_info.html') +def load_article_metas(article, user): + """ + 获得文章meta信息 + :param article: + :return: + """ + return { + 'article': article, + 'user': user + } + + +@register.inclusion_tag('blog/tags/article_pagination.html') +def load_pagination_info(page_obj, page_type, tag_name): + previous_url = '' + next_url = '' + if page_type == '': + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse('blog:index_page', kwargs={'page': next_number}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:index_page', kwargs={ + 'page': previous_number}) + if page_type == '分类标签归档': + tag = get_object_or_404(Tag, name=tag_name) + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:tag_detail_page', + kwargs={ + 'page': next_number, + 'tag_name': tag.slug}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:tag_detail_page', + kwargs={ + 'page': previous_number, + 'tag_name': tag.slug}) + if page_type == '作者文章归档': + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:author_detail_page', + kwargs={ + 'page': next_number, + 'author_name': tag_name}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:author_detail_page', + kwargs={ + 'page': previous_number, + 'author_name': tag_name}) + + if page_type == '分类目录归档': + category = get_object_or_404(Category, name=tag_name) + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:category_detail_page', + kwargs={ + 'page': next_number, + 'category_name': category.slug}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:category_detail_page', + kwargs={ + 'page': previous_number, + 'category_name': category.slug}) + + return { + 'previous_url': previous_url, + 'next_url': next_url, + 'page_obj': page_obj + } + + +@register.inclusion_tag('blog/tags/article_info.html') +def load_article_detail(article, isindex, user): + """ + 加载文章详情 + :param article: + :param isindex:是否列表页,若是列表页只显示摘要 + :return: + """ + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + + return { + 'article': article, + 'isindex': isindex, + 'user': user, + 'open_site_comment': blogsetting.open_site_comment, + } + + +# return only the URL of the gravatar +# TEMPLATE USE: {{ email|gravatar_url:150 }} +@register.filter +def gravatar_url(email, size=40): + """获得gravatar头像""" + cachekey = 'gravatat/' + email + url = cache.get(cachekey) + if url: + return url + else: + usermodels = OAuthUser.objects.filter(email=email) + if usermodels: + o = list(filter(lambda x: x.picture is not None, usermodels)) + if o: + return o[0].picture + email = email.encode('utf-8') + + default = static('blog/img/avatar.png') + + url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5( + email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) + cache.set(cachekey, url, 60 * 60 * 10) + logger.info('set gravatar cache.key:{key}'.format(key=cachekey)) + return url + + +@register.filter +def gravatar(email, size=40): + """获得gravatar头像""" + url = gravatar_url(email, size) + return mark_safe( + '' % + (url, size, size)) + + +@register.simple_tag +def query(qs, **kwargs): + """ template tag which allows queryset filtering. Usage: + {% query books author=author as mybooks %} + {% for book in mybooks %} + ... + {% endfor %} + """ + return qs.filter(**kwargs) + + +@register.filter +def addstr(arg1, arg2): + """concatenate arg1 & arg2""" + return str(arg1) + str(arg2) diff --git a/blog/tests.py b/blog/tests.py new file mode 100644 index 0000000..ee13505 --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,232 @@ +import os + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management import call_command +from django.core.paginator import Paginator +from django.templatetags.static import static +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone + +from accounts.models import BlogUser +from blog.forms import BlogSearchForm +from blog.models import Article, Category, Tag, SideBar, Links +from blog.templatetags.blog_tags import load_pagination_info, load_articletags +from djangoblog.utils import get_current_site, get_sha256 +from oauth.models import OAuthUser, OAuthConfig + + +# Create your tests here. + +class ArticleTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_validate_article(self): + site = get_current_site().domain + user = BlogUser.objects.get_or_create( + email="liangliangyy@gmail.com", + username="liangliangyy")[0] + user.set_password("liangliangyy") + user.is_staff = True + user.is_superuser = True + user.save() + response = self.client.get(user.get_absolute_url()) + self.assertEqual(response.status_code, 200) + response = self.client.get('/admin/servermanager/emailsendlog/') + response = self.client.get('admin/admin/logentry/') + s = SideBar() + s.sequence = 1 + s.name = 'test' + s.content = 'test content' + s.is_enable = True + s.save() + + category = Category() + category.name = "category" + category.creation_time = timezone.now() + category.last_mod_time = timezone.now() + category.save() + + tag = Tag() + tag.name = "nicetag" + tag.save() + + article = Article() + article.title = "nicetitle" + article.body = "nicecontent" + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + + article.save() + self.assertEqual(0, article.tags.count()) + article.tags.add(tag) + article.save() + self.assertEqual(1, article.tags.count()) + + for i in range(20): + article = Article() + article.title = "nicetitle" + str(i) + article.body = "nicetitle" + str(i) + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + article.tags.add(tag) + article.save() + from blog.documents import ELASTICSEARCH_ENABLED + if ELASTICSEARCH_ENABLED: + call_command("build_index") + response = self.client.get('/search', {'q': 'nicetitle'}) + self.assertEqual(response.status_code, 200) + + response = self.client.get(article.get_absolute_url()) + self.assertEqual(response.status_code, 200) + from djangoblog.spider_notify import SpiderNotify + SpiderNotify.notify(article.get_absolute_url()) + response = self.client.get(tag.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get(category.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get('/search', {'q': 'django'}) + self.assertEqual(response.status_code, 200) + s = load_articletags(article) + self.assertIsNotNone(s) + + self.client.login(username='liangliangyy', password='liangliangyy') + + response = self.client.get(reverse('blog:archives')) + self.assertEqual(response.status_code, 200) + + p = Paginator(Article.objects.all(), settings.PAGINATE_BY) + self.check_pagination(p, '', '') + + p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) + self.check_pagination(p, '分类标签归档', tag.slug) + + p = Paginator( + Article.objects.filter( + author__username='liangliangyy'), settings.PAGINATE_BY) + self.check_pagination(p, '作者文章归档', 'liangliangyy') + + p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) + self.check_pagination(p, '分类目录归档', category.slug) + + f = BlogSearchForm() + f.search() + # self.client.login(username='liangliangyy', password='liangliangyy') + from djangoblog.spider_notify import SpiderNotify + SpiderNotify.baidu_notify([article.get_full_url()]) + + from blog.templatetags.blog_tags import gravatar_url, gravatar + u = gravatar_url('liangliangyy@gmail.com') + u = gravatar('liangliangyy@gmail.com') + + link = Links( + sequence=1, + name="lylinux", + link='https://wwww.lylinux.net') + link.save() + response = self.client.get('/links.html') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/feed/') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/sitemap.xml') + self.assertEqual(response.status_code, 200) + + self.client.get("/admin/blog/article/1/delete/") + self.client.get('/admin/servermanager/emailsendlog/') + self.client.get('/admin/admin/logentry/') + self.client.get('/admin/admin/logentry/1/change/') + + def check_pagination(self, p, type, value): + for page in range(1, p.num_pages + 1): + s = load_pagination_info(p.page(page), type, value) + self.assertIsNotNone(s) + if s['previous_url']: + response = self.client.get(s['previous_url']) + self.assertEqual(response.status_code, 200) + if s['next_url']: + response = self.client.get(s['next_url']) + self.assertEqual(response.status_code, 200) + + def test_image(self): + import requests + rsp = requests.get( + 'https://www.python.org/static/img/python-logo.png') + imagepath = os.path.join(settings.BASE_DIR, 'python.png') + with open(imagepath, 'wb') as file: + file.write(rsp.content) + rsp = self.client.post('/upload') + self.assertEqual(rsp.status_code, 403) + sign = get_sha256(get_sha256(settings.SECRET_KEY)) + with open(imagepath, 'rb') as file: + imgfile = SimpleUploadedFile( + 'python.png', file.read(), content_type='image/jpg') + form_data = {'python.png': imgfile} + rsp = self.client.post( + '/upload?sign=' + sign, form_data, follow=True) + self.assertEqual(rsp.status_code, 200) + os.remove(imagepath) + from djangoblog.utils import save_user_avatar, send_email + send_email(['qq@qq.com'], 'testTitle', 'testContent') + save_user_avatar( + 'https://www.python.org/static/img/python-logo.png') + + def test_errorpage(self): + rsp = self.client.get('/eee') + self.assertEqual(rsp.status_code, 404) + + def test_commands(self): + user = BlogUser.objects.get_or_create( + email="liangliangyy@gmail.com", + username="liangliangyy")[0] + user.set_password("liangliangyy") + user.is_staff = True + user.is_superuser = True + user.save() + + c = OAuthConfig() + c.type = 'qq' + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + + u = OAuthUser() + u.type = 'qq' + u.openid = 'openid' + u.user = user + u.picture = static("/blog/img/avatar.png") + u.metadata = ''' +{ +"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" +}''' + u.save() + + u = OAuthUser() + u.type = 'qq' + u.openid = 'openid1' + u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' + u.metadata = ''' + { + "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" + }''' + u.save() + + from blog.documents import ELASTICSEARCH_ENABLED + if ELASTICSEARCH_ENABLED: + call_command("build_index") + call_command("ping_baidu", "all") + call_command("create_testdata") + call_command("clear_cache") + call_command("sync_user_avatar") + call_command("build_search_words") diff --git a/blog/urls.py b/blog/urls.py new file mode 100644 index 0000000..adf2703 --- /dev/null +++ b/blog/urls.py @@ -0,0 +1,62 @@ +from django.urls import path +from django.views.decorators.cache import cache_page + +from . import views + +app_name = "blog" +urlpatterns = [ + path( + r'', + views.IndexView.as_view(), + name='index'), + path( + r'page//', + views.IndexView.as_view(), + name='index_page'), + path( + r'article////.html', + views.ArticleDetailView.as_view(), + name='detailbyid'), + path( + r'category/.html', + views.CategoryDetailView.as_view(), + name='category_detail'), + path( + r'category//.html', + views.CategoryDetailView.as_view(), + name='category_detail_page'), + path( + r'author/.html', + views.AuthorDetailView.as_view(), + name='author_detail'), + path( + r'author//.html', + views.AuthorDetailView.as_view(), + name='author_detail_page'), + path( + r'tag/.html', + views.TagDetailView.as_view(), + name='tag_detail'), + path( + r'tag//.html', + views.TagDetailView.as_view(), + name='tag_detail_page'), + path( + 'archives.html', + cache_page( + 60 * 60)( + views.ArchivesView.as_view()), + name='archives'), + path( + 'links.html', + views.LinkListView.as_view(), + name='links'), + path( + r'upload', + views.fileupload, + name='upload'), + path( + r'clean', + views.clean_cache_view, + name='clean'), +] diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000..d5dc7ec --- /dev/null +++ b/blog/views.py @@ -0,0 +1,379 @@ +import logging +import os +import uuid + +from django.conf import settings +from django.core.paginator import Paginator +from django.http import HttpResponse, HttpResponseForbidden +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.templatetags.static import static +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from haystack.views import SearchView + +from blog.models import Article, Category, LinkShowType, Links, Tag +from comments.forms import CommentForm +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME +from djangoblog.utils import cache, get_blog_setting, get_sha256 + +logger = logging.getLogger(__name__) + + +class ArticleListView(ListView): + # template_name属性用于指定使用哪个模板进行渲染 + template_name = 'blog/article_index.html' + + # context_object_name属性用于给上下文变量取名(在模板中使用该名字) + context_object_name = 'article_list' + + # 页面类型,分类目录或标签列表等 + page_type = '' + paginate_by = settings.PAGINATE_BY + page_kwarg = 'page' + link_type = LinkShowType.L + + def get_view_cache_key(self): + return self.request.get['pages'] + + @property + def page_number(self): + page_kwarg = self.page_kwarg + page = self.kwargs.get( + page_kwarg) or self.request.GET.get(page_kwarg) or 1 + return page + + def get_queryset_cache_key(self): + """ + 子类重写.获得queryset的缓存key + """ + raise NotImplementedError() + + def get_queryset_data(self): + """ + 子类重写.获取queryset的数据 + """ + raise NotImplementedError() + + def get_queryset_from_cache(self, cache_key): + ''' + 缓存页面数据 + :param cache_key: 缓存key + :return: + ''' + value = cache.get(cache_key) + if value: + logger.info('get view cache.key:{key}'.format(key=cache_key)) + return value + else: + article_list = self.get_queryset_data() + cache.set(cache_key, article_list) + logger.info('set view cache.key:{key}'.format(key=cache_key)) + return article_list + + def get_queryset(self): + ''' + 重写默认,从缓存获取数据 + :return: + ''' + key = self.get_queryset_cache_key() + value = self.get_queryset_from_cache(key) + return value + + def get_context_data(self, **kwargs): + kwargs['linktype'] = self.link_type + return super(ArticleListView, self).get_context_data(**kwargs) + + +class IndexView(ArticleListView): + ''' + 首页 + ''' + # 友情链接类型 + link_type = LinkShowType.I + + def get_queryset_data(self): + article_list = Article.objects.filter(type='a', status='p') + return article_list + + def get_queryset_cache_key(self): + cache_key = 'index_{page}'.format(page=self.page_number) + return cache_key + + +class ArticleDetailView(DetailView): + ''' + 文章详情页面 + ''' + template_name = 'blog/article_detail.html' + model = Article + pk_url_kwarg = 'article_id' + context_object_name = "article" + + def get_context_data(self, **kwargs): + comment_form = CommentForm() + + article_comments = self.object.comment_list() + parent_comments = article_comments.filter(parent_comment=None) + blog_setting = get_blog_setting() + paginator = Paginator(parent_comments, blog_setting.article_comment_count) + page = self.request.GET.get('comment_page', '1') + if not page.isnumeric(): + page = 1 + else: + page = int(page) + if page < 1: + page = 1 + if page > paginator.num_pages: + page = paginator.num_pages + + p_comments = paginator.page(page) + next_page = p_comments.next_page_number() if p_comments.has_next() else None + prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None + + if next_page: + kwargs[ + 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' + if prev_page: + kwargs[ + 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' + kwargs['form'] = comment_form + kwargs['article_comments'] = article_comments + kwargs['p_comments'] = p_comments + kwargs['comment_count'] = len( + article_comments) if article_comments else 0 + + kwargs['next_article'] = self.object.next_article + kwargs['prev_article'] = self.object.prev_article + + context = super(ArticleDetailView, self).get_context_data(**kwargs) + article = self.object + # Action Hook, 通知插件"文章详情已获取" + hooks.run_action('after_article_body_get', article=article, request=self.request) + # # Filter Hook, 允许插件修改文章正文 + article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, + request=self.request) + + return context + + +class CategoryDetailView(ArticleListView): + ''' + 分类目录列表 + ''' + page_type = "分类目录归档" + + def get_queryset_data(self): + slug = self.kwargs['category_name'] + category = get_object_or_404(Category, slug=slug) + + categoryname = category.name + self.categoryname = categoryname + categorynames = list( + map(lambda c: c.name, category.get_sub_categorys())) + article_list = Article.objects.filter( + category__name__in=categorynames, status='p') + return article_list + + def get_queryset_cache_key(self): + slug = self.kwargs['category_name'] + category = get_object_or_404(Category, slug=slug) + categoryname = category.name + self.categoryname = categoryname + cache_key = 'category_list_{categoryname}_{page}'.format( + categoryname=categoryname, page=self.page_number) + return cache_key + + def get_context_data(self, **kwargs): + + categoryname = self.categoryname + try: + categoryname = categoryname.split('/')[-1] + except BaseException: + pass + kwargs['page_type'] = CategoryDetailView.page_type + kwargs['tag_name'] = categoryname + return super(CategoryDetailView, self).get_context_data(**kwargs) + + +class AuthorDetailView(ArticleListView): + ''' + 作者详情页 + ''' + page_type = '作者文章归档' + + def get_queryset_cache_key(self): + from uuslug import slugify + author_name = slugify(self.kwargs['author_name']) + cache_key = 'author_{author_name}_{page}'.format( + author_name=author_name, page=self.page_number) + return cache_key + + def get_queryset_data(self): + author_name = self.kwargs['author_name'] + article_list = Article.objects.filter( + author__username=author_name, type='a', status='p') + return article_list + + def get_context_data(self, **kwargs): + author_name = self.kwargs['author_name'] + kwargs['page_type'] = AuthorDetailView.page_type + kwargs['tag_name'] = author_name + return super(AuthorDetailView, self).get_context_data(**kwargs) + + +class TagDetailView(ArticleListView): + ''' + 标签列表页面 + ''' + page_type = '分类标签归档' + + def get_queryset_data(self): + slug = self.kwargs['tag_name'] + tag = get_object_or_404(Tag, slug=slug) + tag_name = tag.name + self.name = tag_name + article_list = Article.objects.filter( + tags__name=tag_name, type='a', status='p') + return article_list + + def get_queryset_cache_key(self): + slug = self.kwargs['tag_name'] + tag = get_object_or_404(Tag, slug=slug) + tag_name = tag.name + self.name = tag_name + cache_key = 'tag_{tag_name}_{page}'.format( + tag_name=tag_name, page=self.page_number) + return cache_key + + def get_context_data(self, **kwargs): + # tag_name = self.kwargs['tag_name'] + tag_name = self.name + kwargs['page_type'] = TagDetailView.page_type + kwargs['tag_name'] = tag_name + return super(TagDetailView, self).get_context_data(**kwargs) + + +class ArchivesView(ArticleListView): + ''' + 文章归档页面 + ''' + page_type = '文章归档' + paginate_by = None + page_kwarg = None + template_name = 'blog/article_archives.html' + + def get_queryset_data(self): + return Article.objects.filter(status='p').all() + + def get_queryset_cache_key(self): + cache_key = 'archives' + return cache_key + + +class LinkListView(ListView): + model = Links + template_name = 'blog/links_list.html' + + def get_queryset(self): + return Links.objects.filter(is_enable=True) + + +class EsSearchView(SearchView): + def get_context(self): + paginator, page = self.build_page() + context = { + "query": self.query, + "form": self.form, + "page": page, + "paginator": paginator, + "suggestion": None, + } + if hasattr(self.results, "query") and self.results.query.backend.include_spelling: + context["suggestion"] = self.results.query.get_spelling_suggestion() + context.update(self.extra_context()) + + return context + + +@csrf_exempt +def fileupload(request): + """ + 该方法需自己写调用端来上传图片,该方法仅提供图床功能 + :param request: + :return: + """ + if request.method == 'POST': + sign = request.GET.get('sign', None) + if not sign: + return HttpResponseForbidden() + if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): + return HttpResponseForbidden() + response = [] + for filename in request.FILES: + timestr = timezone.now().strftime('%Y/%m/%d') + imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] + fname = u''.join(str(filename)) + isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 + base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) + if not os.path.exists(base_dir): + os.makedirs(base_dir) + savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) + if not savepath.startswith(base_dir): + return HttpResponse("only for post") + with open(savepath, 'wb+') as wfile: + for chunk in request.FILES[filename].chunks(): + wfile.write(chunk) + if isimage: + from PIL import Image + image = Image.open(savepath) + image.save(savepath, quality=20, optimize=True) + url = static(savepath) + response.append(url) + return HttpResponse(response) + + else: + return HttpResponse("only for post") + + +def page_not_found_view( + request, + exception, + template_name='blog/error_page.html'): + if exception: + logger.error(exception) + url = request.get_full_path() + return render(request, + template_name, + {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'), + 'statuscode': '404'}, + status=404) + + +def server_error_view(request, template_name='blog/error_page.html'): + return render(request, + template_name, + {'message': _('Sorry, the server is busy, please click the home page to see other?'), + 'statuscode': '500'}, + status=500) + + +def permission_denied_view( + request, + exception, + template_name='blog/error_page.html'): + if exception: + logger.error(exception) + return render( + request, template_name, { + 'message': _('Sorry, you do not have permission to access this page?'), + 'statuscode': '403'}, status=403) + + +def clean_cache_view(request): + cache.clear() + return HttpResponse('ok') diff --git a/comments/__init__.py b/comments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comments/admin.py b/comments/admin.py new file mode 100644 index 0000000..a814f3f --- /dev/null +++ b/comments/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + + +def disable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=False) + + +def enable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=True) + + +disable_commentstatus.short_description = _('Disable comments') +enable_commentstatus.short_description = _('Enable comments') + + +class CommentAdmin(admin.ModelAdmin): + list_per_page = 20 + list_display = ( + 'id', + 'body', + 'link_to_userinfo', + 'link_to_article', + 'is_enable', + 'creation_time') + list_display_links = ('id', 'body', 'is_enable') + list_filter = ('is_enable',) + exclude = ('creation_time', 'last_modify_time') + actions = [disable_commentstatus, enable_commentstatus] + + def link_to_userinfo(self, obj): + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def link_to_article(self, obj): + info = (obj.article._meta.app_label, obj.article._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + return format_html( + u'%s' % (link, obj.article.title)) + + link_to_userinfo.short_description = _('User') + link_to_article.short_description = _('Article') diff --git a/comments/apps.py b/comments/apps.py new file mode 100644 index 0000000..ff01b77 --- /dev/null +++ b/comments/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + name = 'comments' diff --git a/comments/forms.py b/comments/forms.py new file mode 100644 index 0000000..e83737d --- /dev/null +++ b/comments/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.forms import ModelForm + +from .models import Comment + + +class CommentForm(ModelForm): + parent_comment_id = forms.IntegerField( + widget=forms.HiddenInput, required=False) + + class Meta: + model = Comment + fields = ['body'] diff --git a/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py new file mode 100644 index 0000000..61d1e53 --- /dev/null +++ b/comments/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('blog', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField(max_length=300, verbose_name='正文')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + ], + options={ + 'verbose_name': '评论', + 'verbose_name_plural': '评论', + 'ordering': ['-id'], + 'get_latest_by': 'id', + }, + ), + ] diff --git a/comments/migrations/0002_alter_comment_is_enable.py b/comments/migrations/0002_alter_comment_is_enable.py new file mode 100644 index 0000000..17c44db --- /dev/null +++ b/comments/migrations/0002_alter_comment_is_enable.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-04-24 13:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='是否显示'), + ), + ] diff --git a/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 new file mode 100644 index 0000000..a1ca970 --- /dev/null +++ b/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0005_alter_article_options_alter_category_options_and_more'), + ('comments', '0002_alter_comment_is_enable'), + ] + + operations = [ + migrations.AlterModelOptions( + name='comment', + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, + ), + migrations.RemoveField( + model_name='comment', + name='created_time', + ), + migrations.RemoveField( + model_name='comment', + name='last_mod_time', + ), + migrations.AddField( + model_name='comment', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='comment', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='comment', + name='article', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), + ), + migrations.AlterField( + model_name='comment', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + ), + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='enable'), + ), + migrations.AlterField( + model_name='comment', + name='parent_comment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), + ), + ] diff --git a/comments/migrations/__init__.py b/comments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comments/models.py b/comments/models.py new file mode 100644 index 0000000..7c3bbc8 --- /dev/null +++ b/comments/models.py @@ -0,0 +1,39 @@ +from django.conf import settings +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + +from blog.models import Article + + +# Create your models here. + +class Comment(models.Model): + body = models.TextField('正文', max_length=300) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + on_delete=models.CASCADE) + article = models.ForeignKey( + Article, + verbose_name=_('article'), + on_delete=models.CASCADE) + parent_comment = models.ForeignKey( + 'self', + verbose_name=_('parent comment'), + blank=True, + null=True, + on_delete=models.CASCADE) + is_enable = models.BooleanField(_('enable'), + default=False, blank=False, null=False) + + class Meta: + ordering = ['-id'] + verbose_name = _('comment') + verbose_name_plural = verbose_name + get_latest_by = 'id' + + def __str__(self): + return self.body diff --git a/comments/templatetags/__init__.py b/comments/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comments/templatetags/comments_tags.py b/comments/templatetags/comments_tags.py new file mode 100644 index 0000000..fde02b4 --- /dev/null +++ b/comments/templatetags/comments_tags.py @@ -0,0 +1,30 @@ +from django import template + +register = template.Library() + + +@register.simple_tag +def parse_commenttree(commentlist, comment): + """获得当前评论子评论的列表 + 用法: {% parse_commenttree article_comments comment as childcomments %} + """ + datas = [] + + def parse(c): + childs = commentlist.filter(parent_comment=c, is_enable=True) + for child in childs: + datas.append(child) + parse(child) + + parse(comment) + return datas + + +@register.inclusion_tag('comments/tags/comment_item.html') +def show_comment_item(comment, ischild): + """评论""" + depth = 1 if ischild else 2 + return { + 'comment_item': comment, + 'depth': depth + } diff --git a/comments/tests.py b/comments/tests.py new file mode 100644 index 0000000..2a7f55f --- /dev/null +++ b/comments/tests.py @@ -0,0 +1,109 @@ +from django.test import Client, RequestFactory, TransactionTestCase +from django.urls import reverse + +from accounts.models import BlogUser +from blog.models import Category, Article +from comments.models import Comment +from comments.templatetags.comments_tags import * +from djangoblog.utils import get_max_articleid_commentid + + +# Create your tests here. + +class CommentsTest(TransactionTestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + from blog.models import BlogSettings + value = BlogSettings() + value.comment_need_review = True + value.save() + + self.user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + def update_article_comment_status(self, article): + comments = article.comment_set.all() + for comment in comments: + comment.is_enable = True + comment.save() + + def test_validate_comment(self): + self.client.login(username='liangliangyy1', password='liangliangyy1') + + category = Category() + category.name = "categoryccc" + category.save() + + article = Article() + article.title = "nicetitleccc" + article.body = "nicecontentccc" + article.author = self.user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + + comment_url = reverse( + 'comments:postcomment', kwargs={ + 'article_id': article.id}) + + response = self.client.post(comment_url, + { + 'body': '123ffffffffff' + }) + + self.assertEqual(response.status_code, 302) + + article = Article.objects.get(pk=article.pk) + self.assertEqual(len(article.comment_list()), 0) + self.update_article_comment_status(article) + + self.assertEqual(len(article.comment_list()), 1) + + response = self.client.post(comment_url, + { + 'body': '123ffffffffff', + }) + + self.assertEqual(response.status_code, 302) + + article = Article.objects.get(pk=article.pk) + self.update_article_comment_status(article) + self.assertEqual(len(article.comment_list()), 2) + parent_comment_id = article.comment_list()[0].id + + response = self.client.post(comment_url, + { + 'body': ''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''', + 'parent_comment_id': parent_comment_id + }) + + self.assertEqual(response.status_code, 302) + self.update_article_comment_status(article) + article = Article.objects.get(pk=article.pk) + self.assertEqual(len(article.comment_list()), 3) + comment = Comment.objects.get(id=parent_comment_id) + tree = parse_commenttree(article.comment_list(), comment) + self.assertEqual(len(tree), 1) + data = show_comment_item(comment, True) + self.assertIsNotNone(data) + s = get_max_articleid_commentid() + self.assertIsNotNone(s) + + from comments.utils import send_comment_email + send_comment_email(comment) diff --git a/comments/urls.py b/comments/urls.py new file mode 100644 index 0000000..7df3fab --- /dev/null +++ b/comments/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "comments" +urlpatterns = [ + path( + 'article//postcomment', + views.CommentPostView.as_view(), + name='postcomment'), +] diff --git a/comments/utils.py b/comments/utils.py new file mode 100644 index 0000000..f01dba7 --- /dev/null +++ b/comments/utils.py @@ -0,0 +1,38 @@ +import logging + +from django.utils.translation import gettext_lazy as _ + +from djangoblog.utils import get_current_site +from djangoblog.utils import send_email + +logger = logging.getLogger(__name__) + + +def send_comment_email(comment): + site = get_current_site().domain + subject = _('Thanks for your comment') + article_url = f"https://{site}{comment.article.get_absolute_url()}" + html_content = _("""

Thank you very much for your comments on this site

+ You can visit %(article_title)s + to review your comments, + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} + tomail = comment.author.email + send_email([tomail], subject, html_content) + try: + if comment.parent_comment: + html_content = _("""Your comment on %(article_title)s
has + received a reply.
%(comment_body)s +
+ go check it out! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s + """) % {'article_url': article_url, 'article_title': comment.article.title, + 'comment_body': comment.parent_comment.body} + tomail = comment.parent_comment.author.email + send_email([tomail], subject, html_content) + except Exception as e: + logger.error(e) diff --git a/comments/views.py b/comments/views.py new file mode 100644 index 0000000..ad9b2b9 --- /dev/null +++ b/comments/views.py @@ -0,0 +1,63 @@ +# Create your views here. +from django.core.exceptions import ValidationError +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect +from django.views.generic.edit import FormView + +from accounts.models import BlogUser +from blog.models import Article +from .forms import CommentForm +from .models import Comment + + +class CommentPostView(FormView): + form_class = CommentForm + template_name = 'blog/article_detail.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(CommentPostView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + url = article.get_absolute_url() + return HttpResponseRedirect(url + "#comments") + + def form_invalid(self, form): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + return self.render_to_response({ + 'form': form, + 'article': article + }) + + def form_valid(self, form): + """提交的数据验证合法后的逻辑""" + user = self.request.user + author = BlogUser.objects.get(pk=user.pk) + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + if article.comment_status == 'c' or article.status == 'c': + raise ValidationError("该文章评论已关闭.") + comment = form.save(False) + comment.article = article + from djangoblog.utils import get_blog_setting + settings = get_blog_setting() + if not settings.comment_need_review: + comment.is_enable = True + comment.author = author + + if form.cleaned_data['parent_comment_id']: + parent_comment = Comment.objects.get( + pk=form.cleaned_data['parent_comment_id']) + comment.parent_comment = parent_comment + + comment.save(True) + return HttpResponseRedirect( + "%s#div-comment-%d" % + (article.get_absolute_url(), comment.pk)) diff --git a/deploy/docker-compose/docker-compose.es.yml b/deploy/docker-compose/docker-compose.es.yml new file mode 100644 index 0000000..83e35ff --- /dev/null +++ b/deploy/docker-compose/docker-compose.es.yml @@ -0,0 +1,48 @@ +version: '3' + +services: + es: + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + container_name: es + restart: always + environment: + - discovery.type=single-node + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - 9200:9200 + volumes: + - ./bin/datas/es/:/usr/share/elasticsearch/data/ + + kibana: + image: kibana:8.6.1 + restart: always + container_name: kibana + ports: + - 5601:5601 + environment: + - ELASTICSEARCH_HOSTS=http://es:9200 + + djangoblog: + build: . + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_MEMCACHED_LOCATION=memcached:11211 + - DJANGO_ELASTICSEARCH_HOST=es:9200 + links: + - db + - memcached + depends_on: + - db + container_name: djangoblog + diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml new file mode 100644 index 0000000..9609af3 --- /dev/null +++ b/deploy/docker-compose/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3' + +services: + db: + image: mysql:latest + restart: always + environment: + - MYSQL_DATABASE=djangoblog + - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E + ports: + - 3306:3306 + volumes: + - ./bin/datas/mysql/:/var/lib/mysql + depends_on: + - redis + container_name: db + + djangoblog: + build: + context: ../../ + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./logs:/code/djangoblog/logs + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_REDIS_URL=redis:6379 + links: + - db + - redis + depends_on: + - db + container_name: djangoblog + nginx: + restart: always + image: nginx:latest + ports: + - "80:80" + - "443:443" + volumes: + - ./bin/nginx.conf:/etc/nginx/nginx.conf + - ./collectedstatic:/code/djangoblog/collectedstatic + links: + - djangoblog:djangoblog + container_name: nginx + + redis: + restart: always + image: redis:latest + container_name: redis + ports: + - "6379:6379" diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100644 index 0000000..2fb6491 --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +NAME="djangoblog" +DJANGODIR=/code/djangoblog +USER=root +GROUP=root +NUM_WORKERS=1 +DJANGO_WSGI_MODULE=djangoblog.wsgi + + +echo "Starting $NAME as `whoami`" + +cd $DJANGODIR + +export PYTHONPATH=$DJANGODIR:$PYTHONPATH + +python manage.py makemigrations && \ + python manage.py migrate && \ + python manage.py collectstatic --noinput && \ + python manage.py compress --force && \ + python manage.py build_index && \ + python manage.py compilemessages || exit 1 + +exec gunicorn ${DJANGO_WSGI_MODULE}:application \ +--name $NAME \ +--workers $NUM_WORKERS \ +--user=$USER --group=$GROUP \ +--bind 0.0.0.0:8000 \ +--log-level=debug \ +--log-file=- \ +--worker-class gevent \ +--threads 4 diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml new file mode 100644 index 0000000..835d4ad --- /dev/null +++ b/deploy/k8s/configmap.yaml @@ -0,0 +1,119 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: web-nginx-config + namespace: djangoblog +data: + nginx.conf: | + user nginx; + worker_processes auto; + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + multi_accept on; + use epoll; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + gzip on; + gzip_disable "msie6"; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 8; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; + + # Include server configurations + include /etc/nginx/conf.d/*.conf; + } + djangoblog.conf: | + server { + server_name lylinux.net; + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + + location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ { + root /resource/djangopub; + expires 1d; + access_log off; + error_log off; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } + server { + server_name www.lylinux.net; + listen 80; + return 301 https://lylinux.net$request_uri; + } + resource.lylinux.net.conf: | + server { + index index.html index.htm; + server_name resource.lylinux.net; + root /resource/; + + location /djangoblog/ { + alias /code/djangoblog/collectedstatic/; + } + + access_log off; + error_log off; + include lylinux/resource.conf; + } + lylinux.resource.conf: | + expires max; + access_log off; + log_not_found off; + add_header Pragma public; + add_header Cache-Control "public"; + add_header "Access-Control-Allow-Origin" "*"; + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: djangoblog-env + namespace: djangoblog +data: + DJANGO_MYSQL_DATABASE: djangoblog + DJANGO_MYSQL_USER: db_user + DJANGO_MYSQL_PASSWORD: db_password + DJANGO_MYSQL_HOST: db_host + DJANGO_MYSQL_PORT: db_port + DJANGO_REDIS_URL: "redis:6379" + DJANGO_DEBUG: "False" + MYSQL_ROOT_PASSWORD: db_password + MYSQL_DATABASE: djangoblog + MYSQL_PASSWORD: db_password + DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..414fdcc --- /dev/null +++ b/deploy/k8s/deployment.yaml @@ -0,0 +1,274 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + replicas: 3 + selector: + matchLabels: + app: djangoblog + template: + metadata: + labels: + app: djangoblog + spec: + containers: + - name: djangoblog + image: liangliangyy/djangoblog:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: djangoblog + mountPath: /code/djangoblog/collectedstatic + - name: resource + mountPath: /resource + volumes: + - name: djangoblog + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: 200m + memory: 2Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + replicas: 1 + selector: + matchLabels: + app: db + template: + metadata: + labels: + app: db + spec: + containers: + - name: db + image: mysql:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3306 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: db-data + mountPath: /var/lib/mysql + volumes: + - name: db-data + persistentVolumeClaim: + claimName: db-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: djangoblog.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf + subPath: resource.lylinux.net.conf + - name: nginx-config + mountPath: /etc/nginx/lylinux/resource.conf + subPath: lylinux.resource.conf + - name: djangoblog-pvc + mountPath: /code/djangoblog/collectedstatic + - name: resource-pvc + mountPath: /resource + volumes: + - name: nginx-config + configMap: + name: web-nginx-config + - name: djangoblog-pvc + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource-pvc + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + replicas: 1 + selector: + matchLabels: + app: elasticsearch + template: + metadata: + labels: + app: elasticsearch + spec: + containers: + - name: elasticsearch + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + imagePullPolicy: IfNotPresent + env: + - name: discovery.type + value: single-node + - name: ES_JAVA_OPTS + value: "-Xms256m -Xmx256m" + - name: xpack.security.enabled + value: "false" + - name: xpack.monitoring.templates.enabled + value: "false" + ports: + - containerPort: 9200 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + readinessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + volumeMounts: + - name: elasticsearch-data + mountPath: /usr/share/elasticsearch/data/ + volumes: + - name: elasticsearch-data + persistentVolumeClaim: + claimName: elasticsearch-pvc diff --git a/deploy/k8s/gateway.yaml b/deploy/k8s/gateway.yaml new file mode 100644 index 0000000..a8de073 --- /dev/null +++ b/deploy/k8s/gateway.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + namespace: djangoblog +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + number: 80 \ No newline at end of file diff --git a/deploy/k8s/pv.yaml b/deploy/k8s/pv.yaml new file mode 100644 index 0000000..874b72f --- /dev/null +++ b/deploy/k8s/pv.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-db +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-db + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-djangoblog +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-djangoblog + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-resource +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/resource/ + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-elasticsearch +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-elasticsearch + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master \ No newline at end of file diff --git a/deploy/k8s/pvc.yaml b/deploy/k8s/pvc.yaml new file mode 100644 index 0000000..ef238c5 --- /dev/null +++ b/deploy/k8s/pvc.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: db-pvc + namespace: djangoblog +spec: + storageClassName: local-storage + volumeName: local-pv-db + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: djangoblog-pvc + namespace: djangoblog +spec: + volumeName: local-pv-djangoblog + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: resource-pvc + namespace: djangoblog +spec: + volumeName: local-pv-resource + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elasticsearch-pvc + namespace: djangoblog +spec: + volumeName: local-pv-elasticsearch + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + \ No newline at end of file diff --git a/deploy/k8s/service.yaml b/deploy/k8s/service.yaml new file mode 100644 index 0000000..4ef2931 --- /dev/null +++ b/deploy/k8s/service.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + selector: + app: djangoblog + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + selector: + app: nginx + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + selector: + app: db + ports: + - protocol: TCP + port: 3306 + targetPort: 3306 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + selector: + app: elasticsearch + ports: + - protocol: TCP + port: 9200 + targetPort: 9200 + type: ClusterIP + diff --git a/deploy/k8s/storageclass.yaml b/deploy/k8s/storageclass.yaml new file mode 100644 index 0000000..5d5a14c --- /dev/null +++ b/deploy/k8s/storageclass.yaml @@ -0,0 +1,10 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: local-storage + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: Immediate + + diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..32161d8 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,50 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + server { + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } +} diff --git a/djangoblog/__init__.py b/djangoblog/__init__.py new file mode 100644 index 0000000..1e205f4 --- /dev/null +++ b/djangoblog/__init__.py @@ -0,0 +1 @@ +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/djangoblog/admin_site.py b/djangoblog/admin_site.py new file mode 100644 index 0000000..f120405 --- /dev/null +++ b/djangoblog/admin_site.py @@ -0,0 +1,64 @@ +from django.contrib.admin import AdminSite +from django.contrib.admin.models import LogEntry +from django.contrib.sites.admin import SiteAdmin +from django.contrib.sites.models import Site + +from accounts.admin import * +from blog.admin import * +from blog.models import * +from comments.admin import * +from comments.models import * +from djangoblog.logentryadmin import LogEntryAdmin +from oauth.admin import * +from oauth.models import * +from owntracks.admin import * +from owntracks.models import * +from servermanager.admin import * +from servermanager.models import * + + +class DjangoBlogAdminSite(AdminSite): + site_header = 'djangoblog administration' + site_title = 'djangoblog site admin' + + def __init__(self, name='admin'): + super().__init__(name) + + def has_permission(self, request): + return request.user.is_superuser + + # def get_urls(self): + # urls = super().get_urls() + # from django.urls import path + # from blog.views import refresh_memcache + # + # my_urls = [ + # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), + # ] + # return urls + my_urls + + +admin_site = DjangoBlogAdminSite(name='admin') + +admin_site.register(Article, ArticlelAdmin) +admin_site.register(Category, CategoryAdmin) +admin_site.register(Tag, TagAdmin) +admin_site.register(Links, LinksAdmin) +admin_site.register(SideBar, SideBarAdmin) +admin_site.register(BlogSettings, BlogSettingsAdmin) + +admin_site.register(commands, CommandsAdmin) +admin_site.register(EmailSendLog, EmailSendLogAdmin) + +admin_site.register(BlogUser, BlogUserAdmin) + +admin_site.register(Comment, CommentAdmin) + +admin_site.register(OAuthUser, OAuthUserAdmin) +admin_site.register(OAuthConfig, OAuthConfigAdmin) + +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) + +admin_site.register(Site, SiteAdmin) + +admin_site.register(LogEntry, LogEntryAdmin) diff --git a/djangoblog/apps.py b/djangoblog/apps.py new file mode 100644 index 0000000..d29e318 --- /dev/null +++ b/djangoblog/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + +class DjangoblogAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'djangoblog' + + def ready(self): + super().ready() + # Import and load plugins here + from .plugin_manage.loader import load_plugins + load_plugins() \ No newline at end of file diff --git a/djangoblog/blog_signals.py b/djangoblog/blog_signals.py new file mode 100644 index 0000000..393f441 --- /dev/null +++ b/djangoblog/blog_signals.py @@ -0,0 +1,122 @@ +import _thread +import logging + +import django.dispatch +from django.conf import settings +from django.contrib.admin.models import LogEntry +from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.core.mail import EmailMultiAlternatives +from django.db.models.signals import post_save +from django.dispatch import receiver + +from comments.models import Comment +from comments.utils import send_comment_email +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser + +logger = logging.getLogger(__name__) + +oauth_user_login_signal = django.dispatch.Signal(['id']) +send_email_signal = django.dispatch.Signal( + ['emailto', 'title', 'content']) + + +@receiver(send_email_signal) +def send_email_signal_handler(sender, **kwargs): + emailto = kwargs['emailto'] + title = kwargs['title'] + content = kwargs['content'] + + msg = EmailMultiAlternatives( + title, + content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=emailto) + msg.content_subtype = "html" + + from servermanager.models import EmailSendLog + log = EmailSendLog() + log.title = title + log.content = content + log.emailto = ','.join(emailto) + + try: + result = msg.send() + log.send_result = result > 0 + except Exception as e: + logger.error(f"失败邮箱号: {emailto}, {e}") + log.send_result = False + log.save() + + +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + id = kwargs['id'] + oauthuser = OAuthUser.objects.get(id=id) + site = get_current_site().domain + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: + from djangoblog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) + oauthuser.save() + + delete_sidebar_cache() + + +@receiver(post_save) +def model_post_save_callback( + sender, + instance, + created, + raw, + using, + update_fields, + **kwargs): + clearcache = False + if isinstance(instance, LogEntry): + return + if 'get_full_url' in dir(instance): + is_update_views = update_fields == {'views'} + if not settings.TESTING and not is_update_views: + try: + notify_url = instance.get_full_url() + SpiderNotify.baidu_notify([notify_url]) + except Exception as ex: + logger.error("notify sipder", ex) + if not is_update_views: + clearcache = True + + if isinstance(instance, Comment): + if instance.is_enable: + path = instance.article.get_absolute_url() + site = get_current_site().domain + if site.find(':') > 0: + site = site[0:site.find(':')] + + expire_view_cache( + path, + servername=site, + serverport=80, + key_prefix='blogdetail') + if cache.get('seo_processor'): + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format( + id=instance.article.id) + cache.delete(comment_cache_key) + delete_sidebar_cache() + delete_view_cache('article_comments', [str(instance.article.pk)]) + + _thread.start_new_thread(send_comment_email, (instance,)) + + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: + logger.info(user) + delete_sidebar_cache() + # cache.clear() diff --git a/djangoblog/elasticsearch_backend.py b/djangoblog/elasticsearch_backend.py new file mode 100644 index 0000000..4afe498 --- /dev/null +++ b/djangoblog/elasticsearch_backend.py @@ -0,0 +1,183 @@ +from django.utils.encoding import force_str +from elasticsearch_dsl import Q +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +from haystack.forms import ModelSearchForm +from haystack.models import SearchResult +from haystack.utils import log as logging + +from blog.documents import ArticleDocument, ArticleDocumentManager +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ElasticSearchBackend(BaseSearchBackend): + def __init__(self, connection_alias, **connection_options): + super( + ElasticSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.manager = ArticleDocumentManager() + self.include_spelling = True + + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() + docs = self.manager.convert_to_doc(models) + return docs + + def _create(self, models): + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) + + def _delete(self, models): + for m in models: + m.delete() + return True + + def _rebuild(self, models): + models = models if models else Article.objects.all() + docs = self.manager.convert_to_doc(models) + self.manager.update_docs(docs) + + def update(self, index, iterable, commit=True): + + models = self._get_models(iterable) + self.manager.update_docs(models) + + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) + self._delete(models) + + def clear(self, models=None, commit=True): + self.remove(None) + + @staticmethod + def get_suggestion(query: str) -> str: + """获取推荐词, 如果没有找到添加原搜索词""" + + search = ArticleDocument.search() \ + .query("match", body=query) \ + .suggest('suggest_search', query, term={'field': 'body'}) \ + .execute() + + keywords = [] + for suggest in search.suggest.suggest_search: + if suggest["options"]: + keywords.append(suggest["options"][0]["text"]) + else: + keywords.append(suggest["text"]) + + return ' '.join(keywords) + + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) + + start_offset = kwargs.get('start_offset') + end_offset = kwargs.get('end_offset') + + # 推荐词搜索 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) + else: + suggestion = query_string + + q = Q('bool', + should=[Q('match', body=suggestion), Q('match', title=suggestion)], + minimum_should_match="70%") + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] + + results = search.execute() + hits = results['hits'].total + raw_results = [] + for raw_result in results['hits']['hits']: + app_label = 'blog' + model_name = 'Article' + additional_fields = {} + + result_class = SearchResult + + result = result_class( + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields) + raw_results.append(result) + facets = {} + spelling_suggestion = None if query_string == suggestion else suggestion + + return { + 'results': raw_results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + +class ElasticSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + + +class ElasticSearchModelSearchForm(ModelSearchForm): + + def search(self): + # 是否建议搜索 + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + sqs = super().search() + return sqs + + +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/djangoblog/feeds.py b/djangoblog/feeds.py new file mode 100644 index 0000000..8c4e851 --- /dev/null +++ b/djangoblog/feeds.py @@ -0,0 +1,40 @@ +from django.contrib.auth import get_user_model +from django.contrib.syndication.views import Feed +from django.utils import timezone +from django.utils.feedgenerator import Rss201rev2Feed + +from blog.models import Article +from djangoblog.utils import CommonMarkdown + + +class DjangoBlogFeed(Feed): + feed_type = Rss201rev2Feed + + description = '大巧无工,重剑无锋.' + title = "且听风吟 大巧无工,重剑无锋. " + link = "/feed/" + + def author_name(self): + return get_user_model().objects.first().nickname + + def author_link(self): + return get_user_model().objects.first().get_absolute_url() + + def items(self): + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + + def item_title(self, item): + return item.title + + def item_description(self, item): + return CommonMarkdown.get_markdown(item.body) + + def feed_copyright(self): + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) + + def item_link(self, item): + return item.get_absolute_url() + + def item_guid(self, item): + return diff --git a/djangoblog/logentryadmin.py b/djangoblog/logentryadmin.py new file mode 100644 index 0000000..2f6a535 --- /dev/null +++ b/djangoblog/logentryadmin.py @@ -0,0 +1,91 @@ +from django.contrib import admin +from django.contrib.admin.models import DELETION +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse, NoReverseMatch +from django.utils.encoding import force_str +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + + +class LogEntryAdmin(admin.ModelAdmin): + list_filter = [ + 'content_type' + ] + + search_fields = [ + 'object_repr', + 'change_message' + ] + + list_display_links = [ + 'action_time', + 'get_change_message', + ] + list_display = [ + 'action_time', + 'user_link', + 'content_type', + 'object_link', + 'get_change_message', + ] + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return ( + request.user.is_superuser or + request.user.has_perm('admin.change_logentry') + ) and request.method != 'POST' + + def has_delete_permission(self, request, obj=None): + return False + + def object_link(self, obj): + object_link = escape(obj.object_repr) + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: + # try returning an actual link instead of object repr string + try: + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) + except NoReverseMatch: + pass + return mark_safe(object_link) + + object_link.admin_order_field = 'object_repr' + object_link.short_description = _('object') + + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) + user_link = escape(force_str(obj.user)) + try: + # try returning an actual link instead of object repr string + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) + except NoReverseMatch: + pass + return mark_safe(user_link) + + user_link.admin_order_field = 'user' + user_link.short_description = _('user') + + def get_queryset(self, request): + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') + + def get_actions(self, request): + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions diff --git a/djangoblog/plugin_manage/base_plugin.py b/djangoblog/plugin_manage/base_plugin.py new file mode 100644 index 0000000..2b4be5c --- /dev/null +++ b/djangoblog/plugin_manage/base_plugin.py @@ -0,0 +1,41 @@ +import logging + +logger = logging.getLogger(__name__) + + +class BasePlugin: + # 插件元数据 + PLUGIN_NAME = None + PLUGIN_DESCRIPTION = None + PLUGIN_VERSION = None + + def __init__(self): + if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): + raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") + self.init_plugin() + self.register_hooks() + + def init_plugin(self): + """ + 插件初始化逻辑 + 子类可以重写此方法来实现特定的初始化操作 + """ + logger.info(f'{self.PLUGIN_NAME} initialized.') + + def register_hooks(self): + """ + 注册插件钩子 + 子类可以重写此方法来注册特定的钩子 + """ + pass + + def get_plugin_info(self): + """ + 获取插件信息 + :return: 包含插件元数据的字典 + """ + return { + 'name': self.PLUGIN_NAME, + 'description': self.PLUGIN_DESCRIPTION, + 'version': self.PLUGIN_VERSION + } diff --git a/djangoblog/plugin_manage/hook_constants.py b/djangoblog/plugin_manage/hook_constants.py new file mode 100644 index 0000000..6685b7c --- /dev/null +++ b/djangoblog/plugin_manage/hook_constants.py @@ -0,0 +1,7 @@ +ARTICLE_DETAIL_LOAD = 'article_detail_load' +ARTICLE_CREATE = 'article_create' +ARTICLE_UPDATE = 'article_update' +ARTICLE_DELETE = 'article_delete' + +ARTICLE_CONTENT_HOOK_NAME = "the_content" + diff --git a/djangoblog/plugin_manage/hooks.py b/djangoblog/plugin_manage/hooks.py new file mode 100644 index 0000000..d712540 --- /dev/null +++ b/djangoblog/plugin_manage/hooks.py @@ -0,0 +1,44 @@ +import logging + +logger = logging.getLogger(__name__) + +_hooks = {} + + +def register(hook_name: str, callback: callable): + """ + 注册一个钩子回调。 + """ + if hook_name not in _hooks: + _hooks[hook_name] = [] + _hooks[hook_name].append(callback) + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") + + +def run_action(hook_name: str, *args, **kwargs): + """ + 执行一个 Action Hook。 + 它会按顺序执行所有注册到该钩子上的回调函数。 + """ + if hook_name in _hooks: + logger.debug(f"Running action hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + callback(*args, **kwargs) + except Exception as e: + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + + +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行一个 Filter Hook。 + 它会把 value 依次传递给所有注册的回调函数进行处理。 + """ + if hook_name in _hooks: + logger.debug(f"Applying filter hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + value = callback(value, *args, **kwargs) + except Exception as e: + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + return value diff --git a/djangoblog/plugin_manage/loader.py b/djangoblog/plugin_manage/loader.py new file mode 100644 index 0000000..12e824b --- /dev/null +++ b/djangoblog/plugin_manage/loader.py @@ -0,0 +1,19 @@ +import os +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + +def load_plugins(): + """ + Dynamically loads and initializes plugins from the 'plugins' directory. + This function is intended to be called when the Django app registry is ready. + """ + for plugin_name in settings.ACTIVE_PLUGINS: + plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): + try: + __import__(f'plugins.{plugin_name}.plugin') + logger.info(f"Successfully loaded plugin: {plugin_name}") + except ImportError as e: + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/djangoblog/settings.py b/djangoblog/settings.py new file mode 100644 index 0000000..d076bb6 --- /dev/null +++ b/djangoblog/settings.py @@ -0,0 +1,343 @@ +""" +Django settings for djangoblog project. + +Generated by 'django-admin startproject' using Django 1.10.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" +import os +import sys +from pathlib import Path + +from django.utils.translation import gettext_lazy as _ + + +def env_to_bool(env, default): + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get( + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# DEBUG = False +TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' + +# ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# django 4.0新增配置 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# Application definition + + +INSTALLED_APPS = [ + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.sitemaps', + 'mdeditor', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + 'servermanager', + 'owntracks', + 'compressor', + 'djangoblog' +] + +MIDDLEWARE = [ + + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' +] + +ROOT_URLCONF = 'djangoblog.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' + ], + }, + }, +] + +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', + 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', + 'PORT': int( + os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'OPTIONS': { + 'charset': 'utf8mb4'}, + }} + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGES = ( + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), +) +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) + +LANGUAGE_CODE = 'zh-hans' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = False + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + + +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, +} +# Automatically update searching index +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +# Allow user login with username and password +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] + +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') + +STATIC_URL = '/static/' +STATICFILES = os.path.join(BASE_DIR, 'static') + +AUTH_USER_MODEL = 'accounts.BlogUser' +LOGIN_URL = '/login/' + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_TIME_FORMAT = '%Y-%m-%d' + +# bootstrap color styles +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + +# paginate +PAGINATE_BY = 10 +# http cache timeout +CACHE_CONTROL_MAX_AGE = 2592000 +# cache setting +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +# 使用redis作为缓存 +if os.environ.get("DJANGO_REDIS_URL"): + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + } + } + +SITE_ID = 1 +BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ + or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' + +# Email: +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) +EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' +EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = EMAIL_HOST_USER +# Setting debug=false did NOT handle except email notifications +ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] +# WX ADMIN password(Two times md5) +WXADMIN = os.environ.get( + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + +LOG_PATH = os.path.join(BASE_DIR, 'logs') +if not os.path.exists(LOG_PATH): + os.makedirs(LOG_PATH, exist_ok=True) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['console', 'log_file'], + }, + 'formatters': { + 'verbose': { + 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), + 'when': 'D', + 'formatter': 'verbose', + 'interval': 1, + 'delay': True, + 'backupCount': 5, + 'encoding': 'utf-8' + }, + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'null': { + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'djangoblog': { + 'handlers': ['log_file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + } + } +} + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) +COMPRESS_ENABLED = True +# COMPRESS_OFFLINE = True + + +COMPRESS_CSS_FILTERS = [ + # creates absolute urls from relative ones + 'compressor.filters.css_default.CssAbsoluteFilter', + # css minimizer + 'compressor.filters.cssmin.CSSMinFilter' +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter' +] + +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' +X_FRAME_OPTIONS = 'SAMEORIGIN' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') + }, + } + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, + } + +# Plugin System +PLUGINS_DIR = BASE_DIR / 'plugins' +ACTIVE_PLUGINS = [ + 'article_copyright', + 'reading_time', + 'external_links', + 'view_count', + 'seo_optimizer' +] \ No newline at end of file diff --git a/djangoblog/sitemap.py b/djangoblog/sitemap.py new file mode 100644 index 0000000..8b7d446 --- /dev/null +++ b/djangoblog/sitemap.py @@ -0,0 +1,59 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + + +class StaticViewSitemap(Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['blog:index', ] + + def location(self, item): + return reverse(item) + + +class ArticleSiteMap(Sitemap): + changefreq = "monthly" + priority = "0.6" + + def items(self): + return Article.objects.filter(status='p') + + def lastmod(self, obj): + return obj.last_modify_time + + +class CategorySiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.6" + + def items(self): + return Category.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class TagSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return Tag.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class UserSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + return obj.date_joined diff --git a/djangoblog/spider_notify.py b/djangoblog/spider_notify.py new file mode 100644 index 0000000..7b909e9 --- /dev/null +++ b/djangoblog/spider_notify.py @@ -0,0 +1,21 @@ +import logging + +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class SpiderNotify(): + @staticmethod + def baidu_notify(urls): + try: + data = '\n'.join(urls) + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + logger.info(result.text) + except Exception as e: + logger.error(e) + + @staticmethod + def notify(url): + SpiderNotify.baidu_notify(url) diff --git a/djangoblog/tests.py b/djangoblog/tests.py new file mode 100644 index 0000000..01237d9 --- /dev/null +++ b/djangoblog/tests.py @@ -0,0 +1,32 @@ +from django.test import TestCase + +from djangoblog.utils import * + + +class DjangoBlogTest(TestCase): + def setUp(self): + pass + + def test_utils(self): + md5 = get_sha256('test') + self.assertIsNotNone(md5) + c = CommonMarkdown.get_markdown(''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) + self.assertIsNotNone(data) diff --git a/djangoblog/urls.py b/djangoblog/urls.py new file mode 100644 index 0000000..4aae58a --- /dev/null +++ b/djangoblog/urls.py @@ -0,0 +1,64 @@ +"""djangoblog URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory + +from blog.views import EsSearchView +from djangoblog.admin_site import admin_site +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm +from djangoblog.feeds import DjangoBlogFeed +from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap + +sitemaps = { + + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), +] +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), + re_path(r'', include('blog.urls', namespace='blog')), + re_path(r'mdeditor/', include('mdeditor.urls')), + re_path(r'', include('comments.urls', namespace='comment')), + re_path(r'', include('accounts.urls', namespace='account')), + re_path(r'', include('oauth.urls', namespace='oauth')), + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), + re_path(r'^rss/$', DjangoBlogFeed()), + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), + re_path(r'', include('owntracks.urls', namespace='owntracks')) + , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/djangoblog/utils.py b/djangoblog/utils.py new file mode 100644 index 0000000..57f63dc --- /dev/null +++ b/djangoblog/utils.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +import logging +import os +import random +import string +import uuid +from hashlib import sha256 + +import bleach +import markdown +import requests +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.cache import cache +from django.templatetags.static import static + +logger = logging.getLogger(__name__) + + +def get_max_articleid_commentid(): + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) + + +def get_sha256(str): + m = sha256(str.encode('utf-8')) + return m.hexdigest() + + +def cache_decorator(expiration=3 * 60): + def wrapper(func): + def news(*args, **kwargs): + try: + view = args[0] + key = view.get_cache_key() + except: + key = None + if not key: + unique_str = repr((func, args, kwargs)) + + m = sha256(unique_str.encode('utf-8')) + key = m.hexdigest() + value = cache.get(key) + if value is not None: + # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) + if str(value) == '__default_cache_value__': + return None + else: + return value + else: + logger.debug( + 'cache_decorator set cache:%s key:%s' % + (func.__name__, key)) + value = func(*args, **kwargs) + if value is None: + cache.set(key, '__default_cache_value__', expiration) + else: + cache.set(key, value, expiration) + return value + + return news + + return wrapper + + +def expire_view_cache(path, servername, serverport, key_prefix=None): + ''' + 刷新视图缓存 + :param path:url路径 + :param servername:host + :param serverport:端口 + :param key_prefix:前缀 + :return:是否成功 + ''' + from django.http import HttpRequest + from django.utils.cache import get_cache_key + + request = HttpRequest() + request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.path = path + + key = get_cache_key(request, key_prefix=key_prefix, cache=cache) + if key: + logger.info('expire_view_cache:get key:{path}'.format(path=path)) + if cache.get(key): + cache.delete(key) + return True + return False + + +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + + +class CommonMarkdown: + @staticmethod + def _convert_markdown(value): + md = markdown.Markdown( + extensions=[ + 'extra', + 'codehilite', + 'toc', + 'tables', + ] + ) + body = md.convert(value) + toc = md.toc + return body, toc + + @staticmethod + def get_markdown_with_toc(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body + + +def send_email(emailto, title, content): + from djangoblog.blog_signals import send_email_signal + send_email_signal.send( + send_email.__class__, + emailto=emailto, + title=title, + content=content) + + +def generate_code() -> str: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + +def parse_dict_to_url(dict): + from urllib.parse import quote + url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) + for k, v in dict.items()]) + return url + + +def get_blog_setting(): + value = cache.get('get_blog_setting') + if value: + return value + else: + from blog.models import BlogSettings + if not BlogSettings.objects.count(): + setting = BlogSettings() + setting.site_name = 'djangoblog' + setting.site_description = '基于Django的博客系统' + setting.site_seo_description = '基于Django的博客系统' + setting.site_keywords = 'Django,Python' + setting.article_sub_length = 300 + setting.sidebar_article_count = 10 + setting.sidebar_comment_count = 5 + setting.show_google_adsense = False + setting.open_site_comment = True + setting.analytics_code = '' + setting.beian_code = '' + setting.show_gongan_code = False + setting.comment_need_review = False + setting.save() + value = BlogSettings.objects.first() + logger.info('set cache get_blog_setting') + cache.set('get_blog_setting', value) + return value + + +def save_user_avatar(url): + ''' + 保存用户头像 + :param url:头像url + :return: 本地路径 + ''' + logger.info(url) + + try: + basedir = os.path.join(settings.STATICFILES, 'avatar') + rsp = requests.get(url, timeout=2) + if rsp.status_code == 200: + if not os.path.exists(basedir): + os.makedirs(basedir) + + image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] + isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 + ext = os.path.splitext(url)[1] if isimage else '.jpg' + save_filename = str(uuid.uuid4().hex) + ext + logger.info('保存用户头像:' + basedir + save_filename) + with open(os.path.join(basedir, save_filename), 'wb+') as file: + file.write(rsp.content) + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + + +def delete_sidebar_cache(): + from blog.models import LinkShowType + keys = ["sidebar" + x for x in LinkShowType.values] + for k in keys: + logger.info('delete sidebar key:' + k) + cache.delete(k) + + +def delete_view_cache(prefix, keys): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) + + +def get_resource_url(): + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + + +ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', + 'h2', 'p'] +ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + + +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) diff --git a/djangoblog/whoosh_cn_backend.py b/djangoblog/whoosh_cn_backend.py new file mode 100644 index 0000000..04e3f7f --- /dev/null +++ b/djangoblog/whoosh_cn_backend.py @@ -0,0 +1,1044 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +import json +import os +import re +import shutil +import threading +import warnings + +import six +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from datetime import datetime +from django.utils.encoding import force_str +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query +from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument +from haystack.inputs import Clean, Exact, PythonData, Raw +from haystack.models import SearchResult +from haystack.utils import get_identifier, get_model_ct +from haystack.utils import log as logging +from haystack.utils.app_loading import haystack_get_model +from jieba.analyse import ChineseAnalyzer +from whoosh import index +from whoosh.analysis import StemmingAnalyzer +from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT +from whoosh.fields import ID as WHOOSH_ID +from whoosh.filedb.filestore import FileStorage, RamStorage +from whoosh.highlight import ContextFragmenter, HtmlFormatter +from whoosh.highlight import highlight as whoosh_highlight +from whoosh.qparser import QueryParser +from whoosh.searching import ResultsPage +from whoosh.writing import AsyncWriter + +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +# Handle minimum requirement. +if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): + raise MissingDependency( + "The 'whoosh' backend requires version 2.5.0 or greater.") + +# Bubble up the correct error. + +DATETIME_REGEX = re.compile( + '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') +LOCALS = threading.local() +LOCALS.RAM_STORE = None + + +class WhooshHtmlFormatter(HtmlFormatter): + """ + This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. + We use it to have consistent results across backends. Specifically, + Solr, Xapian and Elasticsearch are using this formatting. + """ + template = '<%(tag)s>%(t)s' + + +class WhooshSearchBackend(BaseSearchBackend): + # Word reserved by Whoosh for special use. + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + # Characters reserved by Whoosh for special use. + # The '\\' must come first, so as not to overwrite the other slash + # replacements. + RESERVED_CHARACTERS = ( + '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', + '[', ']', '^', '"', '~', '*', '?', ':', '.', + ) + + def __init__(self, connection_alias, **connection_options): + super( + WhooshSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.setup_complete = False + self.use_file_storage = True + self.post_limit = getattr( + connection_options, + 'POST_LIMIT', + 128 * 1024 * 1024) + self.path = connection_options.get('PATH') + + if connection_options.get('STORAGE', 'file') != 'file': + self.use_file_storage = False + + if self.use_file_storage and not self.path: + raise ImproperlyConfigured( + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + self.log = logging.getLogger('haystack') + + def setup(self): + """ + Defers loading until needed. + """ + from haystack import connections + new_index = False + + # Make sure the index is there. + if self.use_file_storage and not os.path.exists(self.path): + os.makedirs(self.path) + new_index = True + + if self.use_file_storage and not os.access(self.path, os.W_OK): + raise IOError( + "The path to your Whoosh index '%s' is not writable for the current user/group." % + self.path) + + if self.use_file_storage: + self.storage = FileStorage(self.path) + else: + global LOCALS + + if getattr(LOCALS, 'RAM_STORE', None) is None: + LOCALS.RAM_STORE = RamStorage() + + self.storage = LOCALS.RAM_STORE + + self.content_field_name, self.schema = self.build_schema( + connections[self.connection_alias].get_unified_index().all_searchfields()) + self.parser = QueryParser(self.content_field_name, schema=self.schema) + + if new_index is True: + self.index = self.storage.create_index(self.schema) + else: + try: + self.index = self.storage.open_index(schema=self.schema) + except index.EmptyIndexError: + self.index = self.storage.create_index(self.schema) + + self.setup_complete = True + + def build_schema(self, fields): + schema_fields = { + ID: WHOOSH_ID(stored=True, unique=True), + DJANGO_CT: WHOOSH_ID(stored=True), + DJANGO_ID: WHOOSH_ID(stored=True), + } + # Grab the number of keys that are hard-coded into Haystack. + # We'll use this to (possibly) fail slightly more gracefully later. + initial_key_count = len(schema_fields) + content_field_name = '' + + for field_name, field_class in fields.items(): + if field_class.is_multivalued: + if field_class.indexed is False: + schema_fields[field_class.index_fieldname] = IDLIST( + stored=True, field_boost=field_class.boost) + else: + schema_fields[field_class.index_fieldname] = KEYWORD( + stored=True, commas=True, scorable=True, field_boost=field_class.boost) + elif field_class.field_type in ['date', 'datetime']: + schema_fields[field_class.index_fieldname] = DATETIME( + stored=field_class.stored, sortable=True) + elif field_class.field_type == 'integer': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=int, field_boost=field_class.boost) + elif field_class.field_type == 'float': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) + elif field_class.field_type == 'boolean': + # Field boost isn't supported on BOOLEAN as of 1.8.2. + schema_fields[field_class.index_fieldname] = BOOLEAN( + stored=field_class.stored) + elif field_class.field_type == 'ngram': + schema_fields[field_class.index_fieldname] = NGRAM( + minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + elif field_class.field_type == 'edge_ngram': + schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', + stored=field_class.stored, + field_boost=field_class.boost) + else: + # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) + schema_fields[field_class.index_fieldname] = TEXT( + stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) + if field_class.document is True: + content_field_name = field_class.index_fieldname + schema_fields[field_class.index_fieldname].spelling = True + + # Fail more gracefully than relying on the backend to die if no fields + # are found. + if len(schema_fields) <= initial_key_count: + raise SearchBackendError( + "No fields were found in any search_indexes. Please correct this before attempting to search.") + + return (content_field_name, Schema(**schema_fields)) + + def update(self, index, iterable, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + writer = AsyncWriter(self.index) + + for obj in iterable: + try: + doc = index.full_prepare(obj) + except SkipDocument: + self.log.debug(u"Indexing for object `%s` skipped", obj) + else: + # Really make sure it's unicode, because Whoosh won't have it any + # other way. + for key in doc: + doc[key] = self._from_python(doc[key]) + + # Document boosts aren't supported in Whoosh 2.5.0+. + if 'boost' in doc: + del doc['boost'] + + try: + writer.update_document(**doc) + except Exception as e: + if not self.silently_fail: + raise + + # We'll log the object identifier but won't include the actual object + # to avoid the possibility of that generating encoding errors while + # processing the log message: + self.log.error( + u"%s while preparing object for update" % + e.__class__.__name__, + exc_info=True, + extra={ + "data": { + "index": index, + "object": get_identifier(obj)}}) + + if len(iterable) > 0: + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + + def remove(self, obj_or_string, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + whoosh_id = get_identifier(obj_or_string) + + try: + self.index.delete_by_query( + q=self.parser.parse( + u'%s:"%s"' % + (ID, whoosh_id))) + except Exception as e: + if not self.silently_fail: + raise + + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + + def clear(self, models=None, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + + if models is not None: + assert isinstance(models, (list, tuple)) + + try: + if models is None: + self.delete_index() + else: + models_to_delete = [] + + for model in models: + models_to_delete.append( + u"%s:%s" % + (DJANGO_CT, get_model_ct(model))) + + self.index.delete_by_query( + q=self.parser.parse( + u" OR ".join(models_to_delete))) + except Exception as e: + if not self.silently_fail: + raise + + if models is not None: + self.log.error( + "Failed to clear Whoosh index of models '%s': %s", + ','.join(models_to_delete), + e, + exc_info=True) + else: + self.log.error( + "Failed to clear Whoosh index: %s", e, exc_info=True) + + def delete_index(self): + # Per the Whoosh mailing list, if wiping out everything from the index, + # it's much more efficient to simply delete the index files. + if self.use_file_storage and os.path.exists(self.path): + shutil.rmtree(self.path) + elif not self.use_file_storage: + self.storage.clean() + + # Recreate everything. + self.setup() + + def optimize(self): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + self.index.optimize() + + def calculate_page(self, start_offset=0, end_offset=None): + # Prevent against Whoosh throwing an error. Requires an end_offset + # greater than 0. + if end_offset is not None and end_offset <= 0: + end_offset = 1 + + # Determine the page. + page_num = 0 + + if end_offset is None: + end_offset = 1000000 + + if start_offset is None: + start_offset = 0 + + page_length = end_offset - start_offset + + if page_length and page_length > 0: + page_num = int(start_offset / page_length) + + # Increment because Whoosh uses 1-based page numbers. + page_num += 1 + return page_num, page_length + + @log_query + def search( + self, + query_string, + sort_by=None, + start_offset=0, + end_offset=None, + fields='', + highlight=False, + facets=None, + date_facets=None, + query_facets=None, + narrow_queries=None, + spelling_query=None, + within=None, + dwithin=None, + distance_point=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # A zero length query should return no results. + if len(query_string) == 0: + return { + 'results': [], + 'hits': 0, + } + + query_string = force_str(query_string) + + # A one-character query (non-wildcard) gets nabbed by a stopwords + # filter and should yield zero results. + if len(query_string) <= 1 and query_string != u'*': + return { + 'results': [], + 'hits': 0, + } + + reverse = False + + if sort_by is not None: + # Determine if we need to reverse the results and if Whoosh can + # handle what it's being asked to sort by. Reversing is an + # all-or-nothing action, unfortunately. + sort_by_list = [] + reverse_counter = 0 + + for order_by in sort_by: + if order_by.startswith('-'): + reverse_counter += 1 + + if reverse_counter and reverse_counter != len(sort_by): + raise SearchBackendError("Whoosh requires all order_by fields" + " to use the same sort direction") + + for order_by in sort_by: + if order_by.startswith('-'): + sort_by_list.append(order_by[1:]) + + if len(sort_by_list) == 1: + reverse = True + else: + sort_by_list.append(order_by) + + if len(sort_by_list) == 1: + reverse = False + + sort_by = sort_by_list[0] + + if facets is not None: + warnings.warn( + "Whoosh does not handle faceting.", + Warning, + stacklevel=2) + + if date_facets is not None: + warnings.warn( + "Whoosh does not handle date faceting.", + Warning, + stacklevel=2) + + if query_facets is not None: + warnings.warn( + "Whoosh does not handle query faceting.", + Warning, + stacklevel=2) + + narrowed_results = None + self.index = self.index.refresh() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Using narrow queries, limit the results to only models handled + # with the current routers. + model_choices = self.build_models_list() + else: + model_choices = [] + + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + narrow_searcher = None + + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # Whoosh... + narrow_searcher = self.index.searcher() + + for nq in narrow_queries: + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + self.index = self.index.refresh() + + if self.index.doc_count(): + searcher = self.index.searcher() + parsed_query = self.parser.parse(query_string) + + # In the event of an invalid/stopworded query, recover gracefully. + if parsed_query is None: + return { + 'results': [], + 'hits': 0, + } + + page_num, page_length = self.calculate_page( + start_offset, end_offset) + + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + + # Handle the case where the results have been narrowed. + if narrowed_results is not None: + search_kwargs['filter'] = narrowed_results + + try: + raw_page = searcher.search_page( + parsed_query, + page_num, + **search_kwargs + ) + except ValueError: + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + results = self._process_results( + raw_page, + highlight=highlight, + query_string=query_string, + spelling_query=spelling_query, + result_class=result_class) + searcher.close() + + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + else: + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + else: + spelling_suggestion = None + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + def more_like_this( + self, + model_instance, + additional_query_string=None, + start_offset=0, + end_offset=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # Deferred models will have a different class ("RealClass_Deferred_fieldname") + # which won't be in our registry: + model_klass = model_instance._meta.concrete_model + + field_name = self.content_field_name + narrow_queries = set() + narrowed_results = None + self.index = self.index.refresh() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Using narrow queries, limit the results to only models handled + # with the current routers. + model_choices = self.build_models_list() + else: + model_choices = [] + + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + if additional_query_string and additional_query_string != '*': + narrow_queries.add(additional_query_string) + + narrow_searcher = None + + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # Whoosh... + narrow_searcher = self.index.searcher() + + for nq in narrow_queries: + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + page_num, page_length = self.calculate_page(start_offset, end_offset) + + self.index = self.index.refresh() + raw_results = EmptyResults() + + if self.index.doc_count(): + query = "%s:%s" % (ID, get_identifier(model_instance)) + searcher = self.index.searcher() + parsed_query = self.parser.parse(query) + results = searcher.search(parsed_query) + + if len(results): + raw_results = results[0].more_like_this( + field_name, top=end_offset) + + # Handle the case where the results have been narrowed. + if narrowed_results is not None and hasattr(raw_results, 'filter'): + raw_results.filter(narrowed_results) + + try: + raw_page = ResultsPage(raw_results, page_num, page_length) + except ValueError: + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + results = self._process_results(raw_page, result_class=result_class) + searcher.close() + + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + + def _process_results( + self, + raw_page, + highlight=False, + query_string='', + spelling_query=None, + result_class=None): + from haystack import connections + results = [] + + # It's important to grab the hits first before slicing. Otherwise, this + # can cause pagination failures. + hits = len(raw_page) + + if result_class is None: + result_class = SearchResult + + facets = {} + spelling_suggestion = None + unified_index = connections[self.connection_alias].get_unified_index() + indexed_models = unified_index.get_indexed_models() + + for doc_offset, raw_result in enumerate(raw_page): + score = raw_page.score(doc_offset) or 0 + app_label, model_name = raw_result[DJANGO_CT].split('.') + additional_fields = {} + model = haystack_get_model(app_label, model_name) + + if model and model in indexed_models: + for key, value in raw_result.items(): + index = unified_index.get_index(model) + string_key = str(key) + + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Special-cased due to the nature of KEYWORD fields. + if index.fields[string_key].is_multivalued: + if value is None or len(value) == 0: + additional_fields[string_key] = [] + else: + additional_fields[string_key] = value.split( + ',') + else: + additional_fields[string_key] = index.fields[string_key].convert( + value) + else: + additional_fields[string_key] = self._to_python(value) + + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + + if highlight: + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + terms = [token.text for token in sa(query_string)] + + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + hits -= 1 + + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + def create_spelling_suggestion(self, query_string): + spelling_suggestion = None + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + cleaned_query = force_str(query_string) + + if not query_string: + return spelling_suggestion + + # Clean the string. + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + + # Break it down. + query_words = cleaned_query.split() + suggested_words = [] + + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + + def _from_python(self, value): + """ + Converts Python values to a string for Whoosh. + + Code courtesy of pysolr. + """ + if hasattr(value, 'strftime'): + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + elif isinstance(value, bool): + if value: + value = 'true' + else: + value = 'false' + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + elif isinstance(value, (six.integer_types, float)): + # Leave it alone. + pass + else: + value = force_str(value) + return value + + def _to_python(self, value): + """ + Converts values from Whoosh to native Python values. + + A port of the same method in pysolr, as they deal with data the same way. + """ + if value == 'true': + return True + elif value == 'false': + return False + + if value and isinstance(value, six.string_types): + possible_datetime = DATETIME_REGEX.search(value) + + if possible_datetime: + date_values = possible_datetime.groupdict() + + for dk, dv in date_values.items(): + date_values[dk] = int(dv) + + return datetime( + date_values['year'], + date_values['month'], + date_values['day'], + date_values['hour'], + date_values['minute'], + date_values['second']) + + try: + # Attempt to use json to load the values. + converted_value = json.loads(value) + + # Try to handle most built-in types. + if isinstance( + converted_value, + (list, + tuple, + set, + dict, + six.integer_types, + float, + complex)): + return converted_value + except BaseException: + # If it fails (SyntaxError or its ilk) or we don't trust it, + # continue on. + pass + + return value + + +class WhooshSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + from haystack import connections + query_frag = '' + is_datetime = False + + if not hasattr(value, 'input_type_name'): + # Handle when we've got a ``ValuesListQuerySet``... + if hasattr(value, 'values_list'): + value = list(value) + + if hasattr(value, 'strftime'): + is_datetime = True + + if isinstance(value, six.string_types) and value != ' ': + # It's not an ``InputType``. Assume ``Clean``. + value = Clean(value) + else: + value = PythonData(value) + + # Prepare the query using the InputType. + prepared_value = value.prepare(self) + + if not isinstance(prepared_value, (set, list, tuple)): + # Then convert whatever we get back to what pysolr wants if needed. + prepared_value = self.backend._from_python(prepared_value) + + # 'content' is a special reserved word, much like 'pk' in + # Django's ORM layer. It indicates 'no special field'. + if field == 'content': + index_fieldname = '' + else: + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + + filter_types = { + 'content': '%s', + 'contains': '*%s*', + 'endswith': "*%s", + 'startswith': "%s*", + 'exact': '%s', + 'gt': "{%s to}", + 'gte': "[%s to]", + 'lt': "{to %s}", + 'lte': "[to %s]", + 'fuzzy': u'%s~', + } + + if value.post_process is False: + query_frag = prepared_value + else: + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + # Iterate over terms & incorportate the converted form of + # each into the query. + terms = [] + + if isinstance(prepared_value, six.string_types): + possible_values = prepared_value.split(' ') + else: + if is_datetime is True: + prepared_value = self._convert_datetime( + prepared_value) + + possible_values = [prepared_value] + + for possible_value in possible_values: + terms.append( + filter_types[filter_type] % + self.backend._from_python(possible_value)) + + if len(terms) == 1: + query_frag = terms[0] + else: + query_frag = u"(%s)" % " AND ".join(terms) + elif filter_type == 'in': + in_options = [] + + for possible_value in prepared_value: + is_datetime = False + + if hasattr(possible_value, 'strftime'): + is_datetime = True + + pv = self.backend._from_python(possible_value) + + if is_datetime is True: + pv = self._convert_datetime(pv) + + if isinstance(pv, six.string_types) and not is_datetime: + in_options.append('"%s"' % pv) + else: + in_options.append('%s' % pv) + + query_frag = "(%s)" % " OR ".join(in_options) + elif filter_type == 'range': + start = self.backend._from_python(prepared_value[0]) + end = self.backend._from_python(prepared_value[1]) + + if hasattr(prepared_value[0], 'strftime'): + start = self._convert_datetime(start) + + if hasattr(prepared_value[1], 'strftime'): + end = self._convert_datetime(end) + + query_frag = u"[%s to %s]" % (start, end) + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value + else: + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) + + query_frag = filter_types[filter_type] % prepared_value + + if len(query_frag) and not isinstance(value, Raw): + if not query_frag.startswith('(') and not query_frag.endswith(')'): + query_frag = "(%s)" % query_frag + + return u"%s%s" % (index_fieldname, query_frag) + + # if not filter_type in ('in', 'range'): + # # 'in' is a bit of a special case, as we don't want to + # # convert a valid list/tuple to string. Defer handling it + # # until later... + # value = self.backend._from_python(value) + + +class WhooshEngine(BaseEngine): + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/djangoblog/wsgi.py b/djangoblog/wsgi.py new file mode 100644 index 0000000..2295efd --- /dev/null +++ b/djangoblog/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for djangoblog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +application = get_wsgi_application() diff --git a/doc/.gitkeep b/doc/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/README-en.md b/docs/README-en.md new file mode 100644 index 0000000..37ea069 --- /dev/null +++ b/docs/README-en.md @@ -0,0 +1,158 @@ +# DjangoBlog + +

+ Django CI + CodeQL + codecov + license +

+ +

+ 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. + +

+ Alipay Sponsorship + WeChat Sponsorship +

+

+ (Left) Alipay / (Right) WeChat +

+ +## 🙏 Acknowledgements + +A special thanks to **JetBrains** for providing a free open-source license for this project. + +

+ + JetBrains Logo + +

+ +--- +> 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 new file mode 100644 index 0000000..b877efb --- /dev/null +++ b/docs/config-en.md @@ -0,0 +1,64 @@ +# Introduction to main features settings + +## Cache: +Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`. +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', + 'TIMEOUT': 60 * 60 * 10 + }, + 'locmemcache': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +``` + +## OAuth Login: +QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration. + +### Callback address examples: +QQ: http://your-domain-name/oauth/authorize?type=qq +Weibo: http://your-domain-name/oauth/authorize?type=weibo +type is in the type field of `oauthmanager`. + +## owntracks: +owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap. + +## Email feature: +Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +with your email account information. + +## WeChat Official Account +Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account. + +## Introduction to website configuration +You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc. +OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default. + +## Source code highlighting +If the code block in your article didn't show hightlight, please write the code blocks as following: + +![](https://resource.lylinux.net/image/codelang.png) + +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 new file mode 100644 index 0000000..24673a3 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,58 @@ +# 主要功能配置介绍: + +## 缓存: +缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。 +https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199 + + +## oauth登录: + +现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 +**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 +### 回调地址示例: +qq:http://你的域名/oauth/authorize?type=qq +微博:http://你的域名/oauth/authorize?type=weibo +type对应在`oauthmanager`中的type字段。 + +## owntracks: +owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: +`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。 + +## 邮件功能: +同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +为你自己的邮箱配置。 + +## 微信公众号 +集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。 +然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。 +## 网站配置介绍 +在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 +其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 +## 代码高亮 +如果你发现你文章的代码没有高亮,请这样书写代码块: + +![](https://resource.lylinux.net/image/codelang.png) + + +也就是说,需要在代码块开始位置加入这段代码对应的语言。 + +## 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 new file mode 100644 index 0000000..8d5d59e --- /dev/null +++ b/docs/docker-en.md @@ -0,0 +1,114 @@ +# Deploying DjangoBlog with Docker + +![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog) +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date) +![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/djangoblog) + +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 new file mode 100644 index 0000000..e7c255a --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,114 @@ +# 使用 Docker 部署 DjangoBlog + +![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog) +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date) +![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/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 new file mode 100644 index 0000000..97226c5 --- /dev/null +++ b/docs/es.md @@ -0,0 +1,28 @@ +# 集成Elasticsearch +如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单, +首先需要注意如下几点: +1. 你的`Elasticsearch`支持`ik`中文分词 +2. 你的`Elasticsearch`版本>=7.3.0 + +接下来在`settings.py`做如下改动即可: +- 增加es链接,如下所示: +```python +ELASTICSEARCH_DSL = { + 'default': { + 'hosts': '127.0.0.1:9200' + }, +} +``` +- 修改`HAYSTACK`配置: +```python +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, +} +``` +然后终端执行: +```shell script +./manage.py build_index +``` +这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。 \ No newline at end of file diff --git a/docs/imgs/alipay.jpg b/docs/imgs/alipay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..424d70a2ffbb629b481e0c27d72d6076727e8041 GIT binary patch literal 17961 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⁡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 literal 0 HcmV?d00001 diff --git a/docs/imgs/pycharm_logo.png b/docs/imgs/pycharm_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2a4b0ea66469bd218774de8cb3027a9c18b84d GIT binary patch literal 132045 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 z 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

*$hI3G#u%WdV|9<`IE_VBVx2~ei~?p& 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


AewDOjh!MMu(vjQJBsiX3ZCmAui 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 literal 0 HcmV?d00001 diff --git a/docs/k8s-en.md b/docs/k8s-en.md new file mode 100644 index 0000000..20e9527 --- /dev/null +++ b/docs/k8s-en.md @@ -0,0 +1,141 @@ +# Deploying DjangoBlog with Kubernetes + +This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch. + +## Architecture Overview + +This deployment utilizes a microservices-based, cloud-native architecture: + +- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`. +- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.** +- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names. +- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application. +- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC). + +## 1. Prerequisites + +Before you begin, please ensure you have the following: + +- A running Kubernetes cluster. +- The `kubectl` command-line tool configured to connect to your cluster. +- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster. +- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories. + +## 2. Deployment Steps + +### Step 1: Create a Namespace + +We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management. + +```bash +# Create a namespace named 'djangoblog' +kubectl create namespace djangoblog +``` + +### Step 2: Configure Persistent Storage + +This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`). + +```bash +# Log in to your master node +ssh user@master-node + +# Create the required storage directories +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# Log out from the node +exit +``` +**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file. + +After creating the directories, apply the storage-related configurations: + +```bash +# Apply the StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# Apply the PersistentVolumes (PVs) +kubectl apply -f deploy/k8s/pv.yaml + +# Apply the PersistentVolumeClaims (PVCs) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### Step 3: Configure the Application + +Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings. + +**It is strongly recommended to change the following fields:** +- `DJANGO_SECRET_KEY`: Change to a random, complex string. +- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password. + +```bash +# Edit the ConfigMap file +vim deploy/k8s/configmap.yaml + +# Apply the configuration +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### Step 4: Deploy the Application Stack + +Now, we can deploy all the core services. + +```bash +# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# Deploy the Services (to create internal endpoints for the Deployments) +kubectl apply -f deploy/k8s/service.yaml +``` + +The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### Step 5: Expose the Application Externally + +Finally, expose the Nginx service to external traffic by applying the `Ingress` rule. + +```bash +# Apply the Ingress rule +kubectl apply -f deploy/k8s/gateway.yaml +``` + +Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address: + +```bash +kubectl get ingress -n djangoblog +``` + +### Step 6: First-Time Initialization + +Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run. + +```bash +# First, get the name of a djangoblog pod +kubectl get pods -n djangoblog | grep djangoblog + +# Exec into one of the Pods (replace [pod-name] with the name from the previous step) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# Inside the Pod, run the following commands: +# Create a superuser account (follow the prompts) +python manage.py createsuperuser + +# (Optional) Create some test data +python manage.py create_testdata + +# (Optional, if ES is enabled) Create the search index +python manage.py rebuild_index + +# Exit the Pod +exit +``` + +Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster. \ No newline at end of file diff --git a/docs/k8s.md b/docs/k8s.md new file mode 100644 index 0000000..9da3c28 --- /dev/null +++ b/docs/k8s.md @@ -0,0 +1,141 @@ +# 使用 Kubernetes 部署 DjangoBlog + +本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。 + +## 架构概览 + +本次部署采用的是微服务化的云原生架构: + +- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。 +- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。** +- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。 +- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。 +- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。 + +## 1. 环境准备 + +在开始之前,请确保您已具备以下环境: + +- 一个正在运行的 Kubernetes 集群。 +- `kubectl` 命令行工具已配置并能够连接到您的集群。 +- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。 +- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。 + +## 2. 部署步骤 + +### 步骤 1: 创建命名空间 + +我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。 + +```bash +# 创建一个名为 djangoblog 的命名空间 +kubectl create namespace djangoblog +``` + +### 步骤 2: 配置持久化存储 + +此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。 + +```bash +# 登录到您的 master 节点 +ssh user@master-node + +# 创建所需的存储目录 +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# 退出节点 +exit +``` +**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。 + +创建目录后,应用存储相关的配置文件: + +```bash +# 应用 StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# 应用 PersistentVolume (PV) +kubectl apply -f deploy/k8s/pv.yaml + +# 应用 PersistentVolumeClaim (PVC) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### 步骤 3: 配置应用 + +在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。 + +**强烈建议修改以下字段:** +- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。 +- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。 + +```bash +# 编辑 ConfigMap 文件 +vim deploy/k8s/configmap.yaml + +# 应用配置 +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### 步骤 4: 部署应用服务栈 + +现在,我们可以部署所有的核心服务了。 + +```bash +# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# 部署 Services (为 Deployments 创建内部访问端点) +kubectl apply -f deploy/k8s/service.yaml +``` + +部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### 步骤 5: 暴露应用到外部 + +最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。 + +```bash +# 应用 Ingress 规则 +kubectl apply -f deploy/k8s/gateway.yaml +``` + +部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址: + +```bash +kubectl get ingress -n djangoblog +``` + +### 步骤 6: 首次运行的初始化操作 + +与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。 + +```bash +# 首先,获取 djangoblog pod 的名称 +kubectl get pods -n djangoblog | grep djangoblog + +# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# 在 Pod 内部执行以下命令: +# 创建超级管理员账户 (请按照提示操作) +python manage.py createsuperuser + +# (可选) 创建测试数据 +python manage.py create_testdata + +# (可选,如果启用了 ES) 创建索引 +python manage.py rebuild_index + +# 退出 Pod +exit +``` + +至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署! \ No newline at end of file diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f63669f46b3283a84e04098a7338b55f204e7b9d GIT binary patch literal 11097 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 literal 0 HcmV?d00001 diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..c80b30a --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,685 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "password" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "Enter password again" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "passwords do not match" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "email already exists" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "New password" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "Confirm password" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "Email" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "Code" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "email does not exist" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "nick name" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "creation time" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "last modify time" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "create source" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "user" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "Verification code error" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "Verify Email" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "author" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "Publish selected articles" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "Draft selected articles" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "Close article comments" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "Open article comments" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "category" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "index" + +#: .\blog\models.py:21 +msgid "list" +msgstr "list" + +#: .\blog\models.py:22 +msgid "post" +msgstr "post" + +#: .\blog\models.py:23 +msgid "all" +msgstr "all" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "slide" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "modify time" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "Draft" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "Published" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "Open" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "Close" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "Article" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "Page" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "title" + +#: .\blog\models.py:75 +msgid "body" +msgstr "body" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "publish time" + +#: .\blog\models.py:79 +msgid "status" +msgstr "status" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "comment status" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "type" + +#: .\blog\models.py:89 +msgid "views" +msgstr "views" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "order" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "show toc" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "tag" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "article" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "category name" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "parent category" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "tag name" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "link name" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "link" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "is show" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "show type" + +#: .\blog\models.py:281 +msgid "content" +msgstr "content" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "is enable" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "sidebar" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "site name" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "site description" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "site seo description" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "site keywords" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "article sub length" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "sidebar article count" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "sidebar comment count" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "article comment count" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "show adsense" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "adsense code" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "open site comment" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "Website configuration" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "There can only be one configuration" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "Sorry, the server is busy, please click the home page to see other?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "Sorry, you do not have permission to access this page?" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "Disable comments" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "Enable comments" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "User" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "parent comment" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "enable" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "comment" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "Thanks for your comment" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit
%(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "object" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "English" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "Simplified Chinese" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "Traditional Chinese" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "oauth user" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "weibo" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "google" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "callback url" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "already exists" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "Congratulations on your successful binding!" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "Bind your email" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "Binding successful" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "forget the password" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "get verification code" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "submit" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "Create Account" + +#: .\templates\account\login.html:42 +#, fuzzy +#| msgid "forget the password" +msgid "Forget Password" +msgstr "forget the password" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "login" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "back to the homepage" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "article archive" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "year" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "month" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "pin to top" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "comments" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "toc" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "posted in" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "and tagged" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "by" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "on" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "edit" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "article navigation" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "earlier articles" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "newer articles" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "tags" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "search" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "recent comments" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "published on" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "recent articles" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "bookmark" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "Tag Cloud" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "Welcome to star or fork the source code of this site" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "Function" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "management site" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "logout" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "Track record" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "Click me to return to the top" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "quick login" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "Article archive" diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..a2d36e98a180a2d9f413841d0cfa0c5f85654f63 GIT binary patch literal 10321 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, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "密码" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "再次输入密码" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "密码不匹配" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "邮箱已存在" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "新密码" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "确认密码" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "邮箱" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "验证码" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "邮箱不存在" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "昵称" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "创建时间" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "最后修改时间" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "来源" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "用户" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "验证码错误" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "验证邮箱" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "您正在重置密码,验证码为:%(code)s,5分钟内有效 请妥善保管." + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "作者" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "发布选中的文章" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "选中文章设为草稿" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "关闭文章评论" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "打开文章评论" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "分类目录" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "首页" + +#: .\blog\models.py:21 +msgid "list" +msgstr "列表" + +#: .\blog\models.py:22 +msgid "post" +msgstr "文章" + +#: .\blog\models.py:23 +msgid "all" +msgstr "所有" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "侧边栏" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "修改时间" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "草稿" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "发布" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "打开" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "关闭" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "文章" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "页面" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "标题" + +#: .\blog\models.py:75 +msgid "body" +msgstr "内容" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "发布时间" + +#: .\blog\models.py:79 +msgid "status" +msgstr "状态" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "评论状态" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "类型" + +#: .\blog\models.py:89 +msgid "views" +msgstr "阅读量" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "排序" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "显示目录" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "标签" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "文章" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "分类名" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "上级分类" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "标签名" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "链接名" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "链接" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "是否显示" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "显示类型" + +#: .\blog\models.py:281 +msgid "content" +msgstr "内容" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "是否启用" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "侧边栏" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "站点名称" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "站点描述" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "站点SEO描述" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "关键字" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "文章摘要长度" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "侧边栏文章数目" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "侧边栏评论数目" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "文章页面默认显示评论数目" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "是否显示广告" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "广告内容" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "公共头部" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "网站配置" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "只能有一个配置" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "抱歉,你所访问的页面找不到,请点击首页看看别的?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "抱歉,服务出错了,请点击首页看看别的?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "抱歉,你没用权限访问此页面。" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "禁用评论" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "启用评论" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "用户" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "上级评论" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "启用" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "评论" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "感谢你的评论" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

非常感谢您对此网站的评论

\n" +" 您可以访问%(article_title)s\n" +"查看您的评论,\n" +"再次感谢您!\n" +"
\n" +" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n" +"%(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"您对 %(article_title)s
" +"的评论有\n" +" 收到回复。
%(comment_body)s\n" +"
\n" +"快去看看吧!\n" +"
\n" +" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "对象" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "英文" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "简体中文" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "繁体中文" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "第三方用户" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "微博" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "谷歌" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "回调地址" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "已经存在" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

恭喜你已经绑定成功 你可以使用\n" +" %(oauthuser_type)s 来免密登录本站

\n" +" 欢迎继续关注本站, 地址是\n" +" %(site)s\n" +" 再次感谢你\n" +"
\n" +" 如果上面链接无法打开,请复制此链接到你的浏览器 \n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "恭喜你绑定成功" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

请点击下面的链接绑定您的邮箱

\n" +"\n" +" %(url)s\n" +"\n" +"再次感谢您!\n" +"
\n" +"如果上面的链接打不开,请复制此链接到您的浏览器。\n" +"%(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "绑定邮箱" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "恭喜您,还差一步就绑定成功了,请登录您的邮箱查看邮件完成绑定,谢谢。" + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "绑定成功" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"恭喜您绑定成功,您以后可以使用%(oauthuser_type)s来直接免密码登录本站啦,感谢" +"您对本站对关注。" + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "忘记密码" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "获取验证码" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "提交" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "创建账号" + +#: .\templates\account\login.html:42 +#| msgid "forget the password" +msgid "Forget Password" +msgstr "忘记密码" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "登录" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "返回首页吧" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "文章归档" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "年" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "月" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "置顶" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "评论" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "目录" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "发布于" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "并标记为" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "由" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"查看所有由 %(article.author.username)s\"发布的文章\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "在" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "编辑" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "文章导航" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "早期文章" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "较新文章" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "标签" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "搜索" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "近期评论" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "发表于" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "近期文章" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "书签" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "标签云" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "欢迎您STAR或者FORK本站源代码" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "功能" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "管理站点" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "登出" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "运动轨迹记录" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "点我返回顶部" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "快捷登录" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "文章归档" diff --git a/locale/zh_Hant/LC_MESSAGES/django.mo b/locale/zh_Hant/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fe2ea17dc2636742b9c3b8d4eddb6293ee3b7290 GIT binary patch literal 10268 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 literal 0 HcmV?d00001 diff --git a/locale/zh_Hant/LC_MESSAGES/django.po b/locale/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 0000000..a2920ce --- /dev/null +++ b/locale/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,668 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "密碼" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "再次輸入密碼" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "密碼不匹配" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "郵箱已存在" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "新密碼" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "確認密碼" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "郵箱" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "驗證碼" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "郵箱不存在" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "昵稱" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "創建時間" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "最後修改時間" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "來源" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "用戶" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "驗證碼錯誤" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "驗證郵箱" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "您正在重置密碼,驗證碼為:%(code)s,5分鐘內有效 請妥善保管." + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "作者" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "發布選中的文章" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "選中文章設為草稿" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "關閉文章評論" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "打開文章評論" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "分類目錄" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "首頁" + +#: .\blog\models.py:21 +msgid "list" +msgstr "列表" + +#: .\blog\models.py:22 +msgid "post" +msgstr "文章" + +#: .\blog\models.py:23 +msgid "all" +msgstr "所有" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "側邊欄" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "修改時間" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "草稿" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "發布" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "打開" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "關閉" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "文章" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "頁面" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "標題" + +#: .\blog\models.py:75 +msgid "body" +msgstr "內容" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "發布時間" + +#: .\blog\models.py:79 +msgid "status" +msgstr "狀態" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "評論狀態" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "類型" + +#: .\blog\models.py:89 +msgid "views" +msgstr "閱讀量" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "排序" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "顯示目錄" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "標簽" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "文章" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "分類名" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "上級分類" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "標簽名" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "鏈接名" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "鏈接" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "是否顯示" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "顯示類型" + +#: .\blog\models.py:281 +msgid "content" +msgstr "內容" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "是否啟用" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "側邊欄" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "站點名稱" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "站點描述" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "站點SEO描述" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "關鍵字" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "文章摘要長度" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "側邊欄文章數目" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "側邊欄評論數目" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "文章頁面默認顯示評論數目" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "是否顯示廣告" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "廣告內容" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "公共頭部" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "網站配置" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "只能有一個配置" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "抱歉,你所訪問的頁面找不到,請點擊首頁看看別的?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "抱歉,服務出錯了,請點擊首頁看看別的?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "抱歉,你沒用權限訪問此頁面。" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "禁用評論" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "啟用評論" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "用戶" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "上級評論" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "啟用" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "評論" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "感謝你的評論" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

非常感謝您對此網站的評論

\n" +" 您可以訪問%(article_title)s\n" +"查看您的評論,\n" +"再次感謝您!\n" +"
\n" +" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n" +"%(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"您對 %(article_title)s
" +"的評論有\n" +" 收到回復。
%(comment_body)s\n" +"
\n" +"快去看看吧!\n" +"
\n" +" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "對象" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "英文" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "簡體中文" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "繁體中文" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "第三方用戶" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "微博" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "谷歌" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "回調地址" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "已經存在" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

恭喜你已經綁定成功 你可以使用\n" +" %(oauthuser_type)s 來免密登錄本站

\n" +" 歡迎繼續關註本站, 地址是\n" +" %(site)s\n" +" 再次感謝你\n" +"
\n" +" 如果上面鏈接無法打開,請復製此鏈接到你的瀏覽器 \n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "恭喜你綁定成功" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

請點擊下面的鏈接綁定您的郵箱

\n" +"\n" +" %(url)s\n" +"\n" +"再次感謝您!\n" +"
\n" +"如果上面的鏈接打不開,請復製此鏈接到您的瀏覽器。\n" +"%(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "綁定郵箱" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "恭喜您,還差一步就綁定成功了,請登錄您的郵箱查看郵件完成綁定,謝謝。" + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "綁定成功" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"恭喜您綁定成功,您以後可以使用%(oauthuser_type)s來直接免密碼登錄本站啦,感謝" +"您對本站對關註。" + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "忘記密碼" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "獲取驗證碼" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "提交" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "創建賬號" + +#: .\templates\account\login.html:42 +#, fuzzy +#| msgid "forget the password" +msgid "Forget Password" +msgstr "忘記密碼" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "登錄" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "返回首頁吧" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "文章歸檔" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "年" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "月" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "置頂" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "評論" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "目錄" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "發布於" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "並標記為" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "由" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"查看所有由 %(article.author.username)s\"發布的文章\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "在" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "編輯" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "文章導航" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "早期文章" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "較新文章" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "標簽" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "搜索" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "近期評論" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "發表於" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "近期文章" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "書簽" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "標簽雲" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "歡迎您STAR或者FORK本站源代碼" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "功能" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "管理站點" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "登出" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "運動軌跡記錄" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "點我返回頂部" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "快捷登錄" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "文章歸檔" diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..919ba74 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/oauth/__init__.py b/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth/admin.py b/oauth/admin.py new file mode 100644 index 0000000..57eab5f --- /dev/null +++ b/oauth/admin.py @@ -0,0 +1,54 @@ +import logging + +from django.contrib import admin +# Register your models here. +from django.urls import reverse +from django.utils.html import format_html + +logger = logging.getLogger(__name__) + + +class OAuthUserAdmin(admin.ModelAdmin): + search_fields = ('nickname', 'email') + list_per_page = 20 + list_display = ( + 'id', + 'nickname', + 'link_to_usermodel', + 'show_user_image', + 'type', + 'email', + ) + list_display_links = ('id', 'nickname') + list_filter = ('author', 'type',) + readonly_fields = [] + + def get_readonly_fields(self, request, obj=None): + return list(self.readonly_fields) + \ + [field.name for field in obj._meta.fields] + \ + [field.name for field in obj._meta.many_to_many] + + def has_add_permission(self, request): + return False + + def link_to_usermodel(self, obj): + if obj.author: + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def show_user_image(self, obj): + img = obj.picture + return format_html( + u'' % + (img)) + + link_to_usermodel.short_description = '用户' + show_user_image.short_description = '用户头像' + + +class OAuthConfigAdmin(admin.ModelAdmin): + list_display = ('type', 'appkey', 'appsecret', 'is_enable') + list_filter = ('type',) diff --git a/oauth/apps.py b/oauth/apps.py new file mode 100644 index 0000000..17fcea2 --- /dev/null +++ b/oauth/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OauthConfig(AppConfig): + name = 'oauth' diff --git a/oauth/forms.py b/oauth/forms.py new file mode 100644 index 0000000..0e4ede3 --- /dev/null +++ b/oauth/forms.py @@ -0,0 +1,12 @@ +from django.contrib.auth.forms import forms +from django.forms import widgets + + +class RequireEmailForm(forms.Form): + email = forms.EmailField(label='电子邮箱', required=True) + oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) + + def __init__(self, *args, **kwargs): + super(RequireEmailForm, self).__init__(*args, **kwargs) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) diff --git a/oauth/migrations/0001_initial.py b/oauth/migrations/0001_initial.py new file mode 100644 index 0000000..3aa3e03 --- /dev/null +++ b/oauth/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.7 on 2023-03-07 09:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='OAuthConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), + ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), + ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), + ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': 'oauth配置', + 'verbose_name_plural': 'oauth配置', + 'ordering': ['-created_time'], + }, + ), + migrations.CreateModel( + name='OAuthUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('openid', models.CharField(max_length=50)), + ('nickname', models.CharField(max_length=50, verbose_name='昵称')), + ('token', models.CharField(blank=True, max_length=150, null=True)), + ('picture', models.CharField(blank=True, max_length=350, null=True)), + ('type', models.CharField(max_length=50)), + ('email', models.CharField(blank=True, max_length=50, null=True)), + ('metadata', models.TextField(blank=True, null=True)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': 'oauth用户', + 'verbose_name_plural': 'oauth用户', + 'ordering': ['-created_time'], + }, + ), + ] diff --git a/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py new file mode 100644 index 0000000..d5cc70e --- /dev/null +++ b/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py @@ -0,0 +1,86 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('oauth', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='oauthconfig', + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'}, + ), + migrations.AlterModelOptions( + name='oauthuser', + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'}, + ), + migrations.RemoveField( + model_name='oauthconfig', + name='created_time', + ), + migrations.RemoveField( + model_name='oauthconfig', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='oauthuser', + name='created_time', + ), + migrations.RemoveField( + model_name='oauthuser', + name='last_mod_time', + ), + migrations.AddField( + model_name='oauthconfig', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='oauthconfig', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AddField( + model_name='oauthuser', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='oauthuser', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='callback_url', + field=models.CharField(default='', max_length=200, verbose_name='callback url'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is enable'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='type', + field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'), + ), + migrations.AlterField( + model_name='oauthuser', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + ), + migrations.AlterField( + model_name='oauthuser', + name='nickname', + field=models.CharField(max_length=50, verbose_name='nickname'), + ), + ] diff --git a/oauth/migrations/0003_alter_oauthuser_nickname.py b/oauth/migrations/0003_alter_oauthuser_nickname.py new file mode 100644 index 0000000..6af08eb --- /dev/null +++ b/oauth/migrations/0003_alter_oauthuser_nickname.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-01-26 02:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oauthuser', + name='nickname', + field=models.CharField(max_length=50, verbose_name='nick name'), + ), + ] diff --git a/oauth/migrations/__init__.py b/oauth/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth/models.py b/oauth/models.py new file mode 100644 index 0000000..be838ed --- /dev/null +++ b/oauth/models.py @@ -0,0 +1,67 @@ +# Create your models here. +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + + +class OAuthUser(models.Model): + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + blank=True, + null=True, + on_delete=models.CASCADE) + openid = models.CharField(max_length=50) + nickname = models.CharField(max_length=50, verbose_name=_('nick name')) + token = models.CharField(max_length=150, null=True, blank=True) + picture = models.CharField(max_length=350, blank=True, null=True) + type = models.CharField(blank=False, null=False, max_length=50) + email = models.CharField(max_length=50, null=True, blank=True) + metadata = models.TextField(null=True, blank=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + def __str__(self): + return self.nickname + + class Meta: + verbose_name = _('oauth user') + verbose_name_plural = verbose_name + ordering = ['-creation_time'] + + +class OAuthConfig(models.Model): + TYPE = ( + ('weibo', _('weibo')), + ('google', _('google')), + ('github', 'GitHub'), + ('facebook', 'FaceBook'), + ('qq', 'QQ'), + ) + type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') + appkey = models.CharField(max_length=200, verbose_name='AppKey') + appsecret = models.CharField(max_length=200, verbose_name='AppSecret') + callback_url = models.CharField( + max_length=200, + verbose_name=_('callback url'), + blank=False, + default='') + is_enable = models.BooleanField( + _('is enable'), default=True, blank=False, null=False) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + def clean(self): + if OAuthConfig.objects.filter( + type=self.type).exclude(id=self.id).count(): + raise ValidationError(_(self.type + _('already exists'))) + + def __str__(self): + return self.type + + class Meta: + verbose_name = 'oauth配置' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py new file mode 100644 index 0000000..2e7ceef --- /dev/null +++ b/oauth/oauthmanager.py @@ -0,0 +1,504 @@ +import json +import logging +import os +import urllib.parse +from abc import ABCMeta, abstractmethod + +import requests + +from djangoblog.utils import cache_decorator +from oauth.models import OAuthUser, OAuthConfig + +logger = logging.getLogger(__name__) + + +class OAuthAccessTokenException(Exception): + ''' + oauth授权失败异常 + ''' + + +class BaseOauthManager(metaclass=ABCMeta): + """获取用户授权""" + AUTH_URL = None + """获取token""" + TOKEN_URL = None + """获取用户信息""" + API_URL = None + '''icon图标名''' + ICON_NAME = None + + def __init__(self, access_token=None, openid=None): + self.access_token = access_token + self.openid = openid + + @property + def is_access_token_set(self): + return self.access_token is not None + + @property + def is_authorized(self): + return self.is_access_token_set and self.access_token is not None and self.openid is not None + + @abstractmethod + def get_authorization_url(self, nexturl='/'): + pass + + @abstractmethod + def get_access_token_by_code(self, code): + pass + + @abstractmethod + def get_oauth_userinfo(self): + pass + + @abstractmethod + def get_picture(self, metadata): + pass + + def do_get(self, url, params, headers=None): + rsp = requests.get(url=url, params=params, headers=headers) + logger.info(rsp.text) + return rsp.text + + def do_post(self, url, params, headers=None): + rsp = requests.post(url, params, headers=headers) + logger.info(rsp.text) + return rsp.text + + def get_config(self): + value = OAuthConfig.objects.filter(type=self.ICON_NAME) + return value[0] if value else None + + +class WBOauthManager(BaseOauthManager): + AUTH_URL = 'https://api.weibo.com/oauth2/authorize' + TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' + API_URL = 'https://api.weibo.com/2/users/show.json' + ICON_NAME = 'weibo' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + WBOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, nexturl='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url + '&next_url=' + nexturl + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + if 'access_token' in obj: + self.access_token = str(obj['access_token']) + self.openid = str(obj['uid']) + return self.get_oauth_userinfo() + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + if not self.is_authorized: + return None + params = { + 'uid': self.openid, + 'access_token': self.access_token + } + rsp = self.do_get(self.API_URL, params) + try: + datas = json.loads(rsp) + user = OAuthUser() + user.metadata = rsp + user.picture = datas['avatar_large'] + user.nickname = datas['screen_name'] + user.openid = datas['id'] + user.type = 'weibo' + user.token = self.access_token + if 'email' in datas and datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('weibo oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['avatar_large'] + + +class ProxyManagerMixin: + def __init__(self, *args, **kwargs): + if os.environ.get("HTTP_PROXY"): + self.proxies = { + "http": os.environ.get("HTTP_PROXY"), + "https": os.environ.get("HTTP_PROXY") + } + else: + self.proxies = None + + def do_get(self, url, params, headers=None): + rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) + logger.info(rsp.text) + return rsp.text + + def do_post(self, url, params, headers=None): + rsp = requests.post(url, params, headers=headers, proxies=self.proxies) + logger.info(rsp.text) + return rsp.text + + +class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' + TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' + API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' + ICON_NAME = 'google' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + GoogleOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, nexturl='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url, + 'scope': 'openid email', + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + + if 'access_token' in obj: + self.access_token = str(obj['access_token']) + self.openid = str(obj['id_token']) + logger.info(self.ICON_NAME + ' oauth ' + rsp) + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + if not self.is_authorized: + return None + params = { + 'access_token': self.access_token + } + rsp = self.do_get(self.API_URL, params) + try: + + datas = json.loads(rsp) + user = OAuthUser() + user.metadata = rsp + user.picture = datas['picture'] + user.nickname = datas['name'] + user.openid = datas['sub'] + user.token = self.access_token + user.type = 'google' + if datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('google oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['picture'] + + +class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://github.com/login/oauth/authorize' + TOKEN_URL = 'https://github.com/login/oauth/access_token' + API_URL = 'https://api.github.com/user' + ICON_NAME = 'github' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + GitHubOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': f'{self.callback_url}&next_url={next_url}', + 'scope': 'user' + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + from urllib import parse + r = parse.parse_qs(rsp) + if 'access_token' in r: + self.access_token = (r['access_token'][0]) + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + + rsp = self.do_get(self.API_URL, params={}, headers={ + "Authorization": "token " + self.access_token + }) + try: + datas = json.loads(rsp) + user = OAuthUser() + user.picture = datas['avatar_url'] + user.nickname = datas['name'] + user.openid = datas['id'] + user.type = 'github' + user.token = self.access_token + user.metadata = rsp + if 'email' in datas and datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('github oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['avatar_url'] + + +class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' + TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' + API_URL = 'https://graph.facebook.com/me' + ICON_NAME = 'facebook' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + FaceBookOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url, + 'scope': 'email,public_profile' + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + # 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + if 'access_token' in obj: + token = str(obj['access_token']) + self.access_token = token + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + params = { + 'access_token': self.access_token, + 'fields': 'id,name,picture,email' + } + try: + rsp = self.do_get(self.API_URL, params) + datas = json.loads(rsp) + user = OAuthUser() + user.nickname = datas['name'] + user.openid = datas['id'] + user.type = 'facebook' + user.token = self.access_token + user.metadata = rsp + if 'email' in datas and datas['email']: + user.email = datas['email'] + if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: + user.picture = str(datas['picture']['data']['url']) + return user + except Exception as e: + logger.error(e) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return str(datas['picture']['data']['url']) + + +class QQOauthManager(BaseOauthManager): + AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' + TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' + API_URL = 'https://graph.qq.com/user/get_user_info' + OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' + ICON_NAME = 'qq' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + QQOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'response_type': 'code', + 'client_id': self.client_id, + 'redirect_uri': self.callback_url + '&next_url=' + next_url, + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code': code, + 'redirect_uri': self.callback_url + } + rsp = self.do_get(self.TOKEN_URL, params) + if rsp: + d = urllib.parse.parse_qs(rsp) + if 'access_token' in d: + token = d['access_token'] + self.access_token = token[0] + return token + else: + raise OAuthAccessTokenException(rsp) + + def get_open_id(self): + if self.is_access_token_set: + params = { + 'access_token': self.access_token + } + rsp = self.do_get(self.OPEN_ID_URL, params) + if rsp: + rsp = rsp.replace( + 'callback(', '').replace( + ')', '').replace( + ';', '') + obj = json.loads(rsp) + openid = str(obj['openid']) + self.openid = openid + return openid + + def get_oauth_userinfo(self): + openid = self.get_open_id() + if openid: + params = { + 'access_token': self.access_token, + 'oauth_consumer_key': self.client_id, + 'openid': self.openid + } + rsp = self.do_get(self.API_URL, params) + logger.info(rsp) + obj = json.loads(rsp) + user = OAuthUser() + user.nickname = obj['nickname'] + user.openid = openid + user.type = 'qq' + user.token = self.access_token + user.metadata = rsp + if 'email' in obj: + user.email = obj['email'] + if 'figureurl' in obj: + user.picture = str(obj['figureurl']) + return user + + def get_picture(self, metadata): + datas = json.loads(metadata) + return str(datas['figureurl']) + + +@cache_decorator(expiration=100 * 60) +def get_oauth_apps(): + configs = OAuthConfig.objects.filter(is_enable=True).all() + if not configs: + return [] + configtypes = [x.type for x in configs] + applications = BaseOauthManager.__subclasses__() + apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] + return apps + + +def get_manager_by_type(type): + applications = get_oauth_apps() + if applications: + finds = list( + filter( + lambda x: x.ICON_NAME.lower() == type.lower(), + applications)) + if finds: + return finds[0] + return None diff --git a/oauth/templatetags/__init__.py b/oauth/templatetags/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/oauth/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/oauth/templatetags/oauth_tags.py b/oauth/templatetags/oauth_tags.py new file mode 100644 index 0000000..7b687d5 --- /dev/null +++ b/oauth/templatetags/oauth_tags.py @@ -0,0 +1,22 @@ +from django import template +from django.urls import reverse + +from oauth.oauthmanager import get_oauth_apps + +register = template.Library() + + +@register.inclusion_tag('oauth/oauth_applications.html') +def load_oauth_applications(request): + applications = get_oauth_apps() + if applications: + baseurl = reverse('oauth:oauthlogin') + path = request.get_full_path() + + apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format( + baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) + else: + apps = [] + return { + 'apps': apps + } diff --git a/oauth/tests.py b/oauth/tests.py new file mode 100644 index 0000000..bb23b9b --- /dev/null +++ b/oauth/tests.py @@ -0,0 +1,249 @@ +import json +from unittest.mock import patch + +from django.conf import settings +from django.contrib import auth +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse + +from djangoblog.utils import get_sha256 +from oauth.models import OAuthConfig +from oauth.oauthmanager import BaseOauthManager + + +# Create your tests here. +class OAuthConfigTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_oauth_login_test(self): + c = OAuthConfig() + c.type = 'weibo' + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + +class OauthLoginTest(TestCase): + def setUp(self) -> None: + self.client = Client() + self.factory = RequestFactory() + self.apps = self.init_apps() + + def init_apps(self): + applications = [p() for p in BaseOauthManager.__subclasses__()] + for application in applications: + c = OAuthConfig() + c.type = application.ICON_NAME.lower() + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + return applications + + def get_app_by_type(self, type): + for app in self.apps: + if app.ICON_NAME.lower() == type: + return app + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_login(self, mock_do_get, mock_do_post): + weibo_app = self.get_app_by_type('weibo') + assert weibo_app + url = weibo_app.get_authorization_url() + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_do_get.return_value = json.dumps({ + "avatar_large": "avatar_large", + "screen_name": "screen_name", + "id": "id", + "email": "email", + }) + userinfo = weibo_app.get_access_token_by_code('code') + self.assertEqual(userinfo.token, 'access_token') + self.assertEqual(userinfo.openid, 'id') + + @patch("oauth.oauthmanager.GoogleOauthManager.do_post") + @patch("oauth.oauthmanager.GoogleOauthManager.do_get") + def test_google_login(self, mock_do_get, mock_do_post): + google_app = self.get_app_by_type('google') + assert google_app + url = google_app.get_authorization_url() + mock_do_post.return_value = json.dumps({ + "access_token": "access_token", + "id_token": "id_token", + }) + mock_do_get.return_value = json.dumps({ + "picture": "picture", + "name": "name", + "sub": "sub", + "email": "email", + }) + token = google_app.get_access_token_by_code('code') + userinfo = google_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + self.assertEqual(userinfo.openid, 'sub') + + @patch("oauth.oauthmanager.GitHubOauthManager.do_post") + @patch("oauth.oauthmanager.GitHubOauthManager.do_get") + def test_github_login(self, mock_do_get, mock_do_post): + github_app = self.get_app_by_type('github') + assert github_app + url = github_app.get_authorization_url() + self.assertTrue("github.com" in url) + self.assertTrue("client_id" in url) + mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" + mock_do_get.return_value = json.dumps({ + "avatar_url": "avatar_url", + "name": "name", + "id": "id", + "email": "email", + }) + token = github_app.get_access_token_by_code('code') + userinfo = github_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') + self.assertEqual(userinfo.openid, 'id') + + @patch("oauth.oauthmanager.FaceBookOauthManager.do_post") + @patch("oauth.oauthmanager.FaceBookOauthManager.do_get") + def test_facebook_login(self, mock_do_get, mock_do_post): + facebook_app = self.get_app_by_type('facebook') + assert facebook_app + url = facebook_app.get_authorization_url() + self.assertTrue("facebook.com" in url) + mock_do_post.return_value = json.dumps({ + "access_token": "access_token", + }) + mock_do_get.return_value = json.dumps({ + "name": "name", + "id": "id", + "email": "email", + "picture": { + "data": { + "url": "url" + } + } + }) + token = facebook_app.get_access_token_by_code('code') + userinfo = facebook_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + + @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ + 'access_token=access_token&expires_in=3600', + 'callback({"client_id":"appid","openid":"openid"} );', + json.dumps({ + "nickname": "nickname", + "email": "email", + "figureurl": "figureurl", + "openid": "openid", + }) + ]) + def test_qq_login(self, mock_do_get): + qq_app = self.get_app_by_type('qq') + assert qq_app + url = qq_app.get_authorization_url() + self.assertTrue("qq.com" in url) + token = qq_app.get_access_token_by_code('code') + userinfo = qq_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): + + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_user_info = { + "avatar_large": "avatar_large", + "screen_name": "screen_name1", + "id": "id", + "email": "email", + } + mock_do_get.return_value = json.dumps(mock_user_info) + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + user = auth.get_user(self.client) + assert user.is_authenticated + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, mock_user_info['email']) + self.client.logout() + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + user = auth.get_user(self.client) + assert user.is_authenticated + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, mock_user_info['email']) + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): + + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_user_info = { + "avatar_large": "avatar_large", + "screen_name": "screen_name1", + "id": "id", + } + mock_do_get.return_value = json.dumps(mock_user_info) + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + + self.assertEqual(response.status_code, 302) + + oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) + self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') + + response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) + + self.assertEqual(response.status_code, 302) + sign = get_sha256(settings.SECRET_KEY + + str(oauth_user_id) + settings.SECRET_KEY) + + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': oauth_user_id, + }) + self.assertEqual(response.url, f'{url}?type=email') + + path = reverse('oauth:email_confirm', kwargs={ + 'id': oauth_user_id, + 'sign': sign + }) + response = self.client.get(path) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') + user = auth.get_user(self.client) + from oauth.models import OAuthUser + oauth_user = OAuthUser.objects.get(author=user) + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, 'test@gmail.com') + self.assertEqual(oauth_user.pk, oauth_user_id) diff --git a/oauth/urls.py b/oauth/urls.py new file mode 100644 index 0000000..c4a12a0 --- /dev/null +++ b/oauth/urls.py @@ -0,0 +1,25 @@ +from django.urls import path + +from . import views + +app_name = "oauth" +urlpatterns = [ + path( + r'oauth/authorize', + views.authorize), + path( + r'oauth/requireemail/.html', + views.RequireEmailView.as_view(), + name='require_email'), + path( + r'oauth/emailconfirm//.html', + views.emailconfirm, + name='email_confirm'), + path( + r'oauth/bindsuccess/.html', + views.bindsuccess, + name='bindsuccess'), + path( + r'oauth/oauthlogin', + views.oauthlogin, + name='oauthlogin')] diff --git a/oauth/views.py b/oauth/views.py new file mode 100644 index 0000000..12e3a6e --- /dev/null +++ b/oauth/views.py @@ -0,0 +1,253 @@ +import logging +# Create your views here. +from urllib.parse import urlparse + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth import login +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.http import HttpResponseForbidden +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView + +from djangoblog.blog_signals import oauth_user_login_signal +from djangoblog.utils import get_current_site +from djangoblog.utils import send_email, get_sha256 +from oauth.forms import RequireEmailForm +from .models import OAuthUser +from .oauthmanager import get_manager_by_type, OAuthAccessTokenException + +logger = logging.getLogger(__name__) + + +def get_redirecturl(request): + nexturl = request.GET.get('next_url', None) + if not nexturl or nexturl == '/login/' or nexturl == '/login': + nexturl = '/' + return nexturl + p = urlparse(nexturl) + if p.netloc: + site = get_current_site().domain + if not p.netloc.replace('www.', '') == site.replace('www.', ''): + logger.info('非法url:' + nexturl) + return "/" + return nexturl + + +def oauthlogin(request): + type = request.GET.get('type', None) + if not type: + return HttpResponseRedirect('/') + manager = get_manager_by_type(type) + if not manager: + return HttpResponseRedirect('/') + nexturl = get_redirecturl(request) + authorizeurl = manager.get_authorization_url(nexturl) + return HttpResponseRedirect(authorizeurl) + + +def authorize(request): + type = request.GET.get('type', None) + if not type: + return HttpResponseRedirect('/') + manager = get_manager_by_type(type) + if not manager: + return HttpResponseRedirect('/') + code = request.GET.get('code', None) + try: + rsp = manager.get_access_token_by_code(code) + except OAuthAccessTokenException as e: + logger.warning("OAuthAccessTokenException:" + str(e)) + return HttpResponseRedirect('/') + except Exception as e: + logger.error(e) + rsp = None + nexturl = get_redirecturl(request) + if not rsp: + return HttpResponseRedirect(manager.get_authorization_url(nexturl)) + user = manager.get_oauth_userinfo() + if user: + if not user.nickname or not user.nickname.strip(): + user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + try: + temp = OAuthUser.objects.get(type=type, openid=user.openid) + temp.picture = user.picture + temp.metadata = user.metadata + temp.nickname = user.nickname + user = temp + except ObjectDoesNotExist: + pass + # facebook的token过长 + if type == 'facebook': + user.token = '' + if user.email: + with transaction.atomic(): + author = None + try: + author = get_user_model().objects.get(id=user.author_id) + except ObjectDoesNotExist: + pass + if not author: + result = get_user_model().objects.get_or_create(email=user.email) + author = result[0] + if result[1]: + try: + get_user_model().objects.get(username=user.nickname) + except ObjectDoesNotExist: + author.username = user.nickname + else: + author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + author.source = 'authorize' + author.save() + + user.author = author + user.save() + + oauth_user_login_signal.send( + sender=authorize.__class__, id=user.id) + login(request, author) + return HttpResponseRedirect(nexturl) + else: + user.save() + url = reverse('oauth:require_email', kwargs={ + 'oauthid': user.id + }) + + return HttpResponseRedirect(url) + else: + return HttpResponseRedirect(nexturl) + + +def emailconfirm(request, id, sign): + if not sign: + return HttpResponseForbidden() + if not get_sha256(settings.SECRET_KEY + + str(id) + + settings.SECRET_KEY).upper() == sign.upper(): + return HttpResponseForbidden() + oauthuser = get_object_or_404(OAuthUser, pk=id) + with transaction.atomic(): + if oauthuser.author: + author = get_user_model().objects.get(pk=oauthuser.author_id) + else: + result = get_user_model().objects.get_or_create(email=oauthuser.email) + author = result[0] + if result[1]: + author.source = 'emailconfirm' + author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( + ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + author.save() + oauthuser.author = author + oauthuser.save() + oauth_user_login_signal.send( + sender=emailconfirm.__class__, + id=oauthuser.id) + login(request, author) + + site = 'http://' + get_current_site().domain + content = _(''' +

Congratulations, you have successfully bound your email address. You can use + %(oauthuser_type)s to directly log in to this website without a password.

+ You are welcome to continue to follow this site, the address is + %(site)s + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(site)s + ''') % {'oauthuser_type': oauthuser.type, 'site': site} + + send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': id + }) + url = url + '?type=success' + return HttpResponseRedirect(url) + + +class RequireEmailView(FormView): + form_class = RequireEmailForm + template_name = 'oauth/require_email.html' + + def get(self, request, *args, **kwargs): + oauthid = self.kwargs['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if oauthuser.email: + pass + # return HttpResponseRedirect('/') + + return super(RequireEmailView, self).get(request, *args, **kwargs) + + def get_initial(self): + oauthid = self.kwargs['oauthid'] + return { + 'email': '', + 'oauthid': oauthid + } + + def get_context_data(self, **kwargs): + oauthid = self.kwargs['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if oauthuser.picture: + kwargs['picture'] = oauthuser.picture + return super(RequireEmailView, self).get_context_data(**kwargs) + + def form_valid(self, form): + email = form.cleaned_data['email'] + oauthid = form.cleaned_data['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + oauthuser.email = email + oauthuser.save() + sign = get_sha256(settings.SECRET_KEY + + str(oauthuser.id) + settings.SECRET_KEY) + site = get_current_site().domain + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('oauth:email_confirm', kwargs={ + 'id': oauthid, + 'sign': sign + }) + url = "http://{site}{path}".format(site=site, path=path) + + content = _(""" +

Please click the link below to bind your email

+ + %(url)s + + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. +
+ %(url)s + """) % {'url': url} + send_email(emailto=[email, ], title=_('Bind your email'), content=content) + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': oauthid + }) + url = url + '?type=email' + return HttpResponseRedirect(url) + + +def bindsuccess(request, oauthid): + type = request.GET.get('type', None) + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if type == 'email': + title = _('Bind your email') + content = _( + 'Congratulations, the binding is just one step away. ' + 'Please log in to your email to check the email to complete the binding. Thank you.') + else: + title = _('Binding successful') + content = _( + "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" + " to directly log in to this website without a password. You are welcome to continue to follow this site." % { + 'oauthuser_type': oauthuser.type}) + return render(request, 'oauth/bindsuccess.html', { + 'title': title, + 'content': content + }) diff --git a/owntracks/__init__.py b/owntracks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/owntracks/admin.py b/owntracks/admin.py new file mode 100644 index 0000000..655b535 --- /dev/null +++ b/owntracks/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +# Register your models here. + + +class OwnTrackLogsAdmin(admin.ModelAdmin): + pass diff --git a/owntracks/apps.py b/owntracks/apps.py new file mode 100644 index 0000000..1bc5f12 --- /dev/null +++ b/owntracks/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OwntracksConfig(AppConfig): + name = 'owntracks' diff --git a/owntracks/migrations/0001_initial.py b/owntracks/migrations/0001_initial.py new file mode 100644 index 0000000..9eee55c --- /dev/null +++ b/owntracks/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OwnTrackLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tid', models.CharField(max_length=100, verbose_name='用户')), + ('lat', models.FloatField(verbose_name='纬度')), + ('lon', models.FloatField(verbose_name='经度')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ], + options={ + 'verbose_name': 'OwnTrackLogs', + 'verbose_name_plural': 'OwnTrackLogs', + 'ordering': ['created_time'], + 'get_latest_by': 'created_time', + }, + ), + ] diff --git a/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/owntracks/migrations/0002_alter_owntracklog_options_and_more.py new file mode 100644 index 0000000..b4f8dec --- /dev/null +++ b/owntracks/migrations/0002_alter_owntracklog_options_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('owntracks', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='owntracklog', + options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'}, + ), + migrations.RenameField( + model_name='owntracklog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/owntracks/migrations/__init__.py b/owntracks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/owntracks/models.py b/owntracks/models.py new file mode 100644 index 0000000..760942c --- /dev/null +++ b/owntracks/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.utils.timezone import now + + +# Create your models here. + +class OwnTrackLog(models.Model): + tid = models.CharField(max_length=100, null=False, verbose_name='用户') + lat = models.FloatField(verbose_name='纬度') + lon = models.FloatField(verbose_name='经度') + creation_time = models.DateTimeField('创建时间', default=now) + + def __str__(self): + return self.tid + + class Meta: + ordering = ['creation_time'] + verbose_name = "OwnTrackLogs" + verbose_name_plural = verbose_name + get_latest_by = 'creation_time' diff --git a/owntracks/tests.py b/owntracks/tests.py new file mode 100644 index 0000000..3b4b9d8 --- /dev/null +++ b/owntracks/tests.py @@ -0,0 +1,64 @@ +import json + +from django.test import Client, RequestFactory, TestCase + +from accounts.models import BlogUser +from .models import OwnTrackLog + + +# Create your tests here. + +class OwnTrackLogTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_own_track_log(self): + o = { + 'tid': 12, + 'lat': 123.123, + 'lon': 134.341 + } + + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) + + o = { + 'tid': 12, + 'lat': 123.123 + } + + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) + + rsp = self.client.get('/owntracks/show_maps') + self.assertEqual(rsp.status_code, 302) + + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + self.client.login(username='liangliangyy1', password='liangliangyy1') + s = OwnTrackLog() + s.tid = 12 + s.lon = 123.234 + s.lat = 34.234 + s.save() + + rsp = self.client.get('/owntracks/show_dates') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/show_maps') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/get_datas') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') + self.assertEqual(rsp.status_code, 200) diff --git a/owntracks/urls.py b/owntracks/urls.py new file mode 100644 index 0000000..c19ada8 --- /dev/null +++ b/owntracks/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from . import views + +app_name = "owntracks" + +urlpatterns = [ + path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), + path('owntracks/show_maps', views.show_maps, name='show_maps'), + path('owntracks/get_datas', views.get_datas, name='get_datas'), + path('owntracks/show_dates', views.show_log_dates, name='show_dates') +] diff --git a/owntracks/views.py b/owntracks/views.py new file mode 100644 index 0000000..4c72bdd --- /dev/null +++ b/owntracks/views.py @@ -0,0 +1,127 @@ +# Create your views here. +import datetime +import itertools +import json +import logging +from datetime import timezone +from itertools import groupby + +import django +import requests +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.http import JsonResponse +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt + +from .models import OwnTrackLog + +logger = logging.getLogger(__name__) + + +@csrf_exempt +def manage_owntrack_log(request): + try: + s = json.loads(request.read().decode('utf-8')) + tid = s['tid'] + lat = s['lat'] + lon = s['lon'] + + logger.info( + 'tid:{tid}.lat:{lat}.lon:{lon}'.format( + tid=tid, lat=lat, lon=lon)) + if tid and lat and lon: + m = OwnTrackLog() + m.tid = tid + m.lat = lat + m.lon = lon + m.save() + return HttpResponse('ok') + else: + return HttpResponse('data error') + except Exception as e: + logger.error(e) + return HttpResponse('error') + + +@login_required +def show_maps(request): + if request.user.is_superuser: + defaultdate = str(datetime.datetime.now(timezone.utc).date()) + date = request.GET.get('date', defaultdate) + context = { + 'date': date + } + return render(request, 'owntracks/show_maps.html', context) + else: + from django.http import HttpResponseForbidden + return HttpResponseForbidden() + + +@login_required +def show_log_dates(request): + dates = OwnTrackLog.objects.values_list('creation_time', flat=True) + results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) + + context = { + 'results': results + } + return render(request, 'owntracks/show_log_dates.html', context) + + +def convert_to_amap(locations): + convert_result = [] + it = iter(locations) + + item = list(itertools.islice(it, 30)) + while item: + datas = ';'.join( + set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) + + key = '8440a376dfc9743d8924bf0ad141f28e' + api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' + query = { + 'key': key, + 'locations': datas, + 'coordsys': 'gps' + } + rsp = requests.get(url=api, params=query) + result = json.loads(rsp.text) + if "locations" in result: + convert_result.append(result['locations']) + item = list(itertools.islice(it, 30)) + + return ";".join(convert_result) + + +@login_required +def get_datas(request): + now = django.utils.timezone.now().replace(tzinfo=timezone.utc) + querydate = django.utils.timezone.datetime( + now.year, now.month, now.day, 0, 0, 0) + if request.GET.get('date', None): + date = list(map(lambda x: int(x), request.GET.get('date').split('-'))) + querydate = django.utils.timezone.datetime( + date[0], date[1], date[2], 0, 0, 0) + nextdate = querydate + datetime.timedelta(days=1) + models = OwnTrackLog.objects.filter( + creation_time__range=(querydate, nextdate)) + result = list() + if models and len(models): + for tid, item in groupby( + sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): + + d = dict() + d["name"] = tid + paths = list() + # 使用高德转换后的经纬度 + # locations = convert_to_amap( + # sorted(item, key=lambda x: x.creation_time)) + # for i in locations.split(';'): + # paths.append(i.split(',')) + # 使用GPS原始经纬度 + for location in sorted(item, key=lambda x: x.creation_time): + paths.append([str(location.lon), str(location.lat)]) + d["path"] = paths + result.append(d) + return JsonResponse(result, safe=False) diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/plugins/article_copyright/__init__.py b/plugins/article_copyright/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/plugins/article_copyright/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/plugins/article_copyright/plugin.py b/plugins/article_copyright/plugin.py new file mode 100644 index 0000000..317fed2 --- /dev/null +++ b/plugins/article_copyright/plugin.py @@ -0,0 +1,32 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ArticleCopyrightPlugin(BasePlugin): + PLUGIN_NAME = '文章结尾版权声明' + PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' + PLUGIN_VERSION = '0.2.0' + PLUGIN_AUTHOR = 'liangliangyy' + + # 2. 实现 register_hooks 方法,专门用于注册钩子 + def register_hooks(self): + # 在这里将插件的方法注册到指定的钩子上 + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content) + + def add_copyright_to_content(self, content, *args, **kwargs): + """ + 这个方法会被注册到 'the_content' 过滤器钩子上。 + 它接收原始内容,并返回添加了版权信息的新内容。 + """ + article = kwargs.get('article') + if not article: + return content + + copyright_info = f"\n

本文由 {article.author.username} 原创,转载请注明出处。

" + return content + copyright_info + + +# 3. 实例化插件。 +# 这会自动调用 BasePlugin.__init__,然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。 +plugin = ArticleCopyrightPlugin() diff --git a/plugins/external_links/__init__.py b/plugins/external_links/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/plugins/external_links/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/plugins/external_links/plugin.py b/plugins/external_links/plugin.py new file mode 100644 index 0000000..5b2ef14 --- /dev/null +++ b/plugins/external_links/plugin.py @@ -0,0 +1,48 @@ +import re +from urllib.parse import urlparse +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ExternalLinksPlugin(BasePlugin): + PLUGIN_NAME = '外部链接处理器' + PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links) + + def process_external_links(self, content, *args, **kwargs): + from djangoblog.utils import get_current_site + site_domain = get_current_site().domain + + # 正则表达式查找所有 标签 + link_pattern = re.compile(r'(]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE) + + def replacer(match): + # match.group(1) 是 ... + href = match.group(2) + + # 如果链接已经有 target 属性,则不处理 + if 'target=' in match.group(0).lower(): + return match.group(0) + + # 解析链接 + parsed_url = urlparse(href) + + # 如果链接是外部的 (有域名且域名不等于当前网站域名) + if parsed_url.netloc and parsed_url.netloc != site_domain: + # 添加 target 和 rel 属性 + return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}' + + # 否则返回原样 + return match.group(0) + + return link_pattern.sub(replacer, content) + + +plugin = ExternalLinksPlugin() diff --git a/plugins/reading_time/__init__.py b/plugins/reading_time/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/plugins/reading_time/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/plugins/reading_time/plugin.py b/plugins/reading_time/plugin.py new file mode 100644 index 0000000..35f9db1 --- /dev/null +++ b/plugins/reading_time/plugin.py @@ -0,0 +1,43 @@ +import math +import re +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ReadingTimePlugin(BasePlugin): + PLUGIN_NAME = '阅读时间预测' + PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time) + + def add_reading_time(self, content, *args, **kwargs): + """ + 计算阅读时间并添加到内容开头。 + """ + # 移除HTML标签和空白字符,以获得纯文本 + clean_content = re.sub(r'<[^>]*>', '', content) + clean_content = clean_content.strip() + + # 中文和英文单词混合计数的一个简单方法 + # 匹配中文字符或连续的非中文字符(视为单词) + words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content) + word_count = len(words) + + # 按平均每分钟200字的速度计算 + reading_speed = 200 + reading_minutes = math.ceil(word_count / reading_speed) + + # 如果阅读时间少于1分钟,则显示为1分钟 + if reading_minutes < 1: + reading_minutes = 1 + + reading_time_html = f'

预计阅读时间:{reading_minutes} 分钟

' + + return reading_time_html + content + + +plugin = ReadingTimePlugin() \ No newline at end of file diff --git a/plugins/seo_optimizer/__init__.py b/plugins/seo_optimizer/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/plugins/seo_optimizer/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/plugins/seo_optimizer/plugin.py b/plugins/seo_optimizer/plugin.py new file mode 100644 index 0000000..b5b19a3 --- /dev/null +++ b/plugins/seo_optimizer/plugin.py @@ -0,0 +1,142 @@ +import json +from django.utils.html import strip_tags +from django.template.defaultfilters import truncatewords +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from blog.models import Article, Category, Tag +from djangoblog.utils import get_blog_setting + + +class SeoOptimizerPlugin(BasePlugin): + PLUGIN_NAME = 'SEO 优化器' + PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。' + PLUGIN_VERSION = '0.2.0' + PLUGIN_AUTHOR = 'liuangliangyy' + + def register_hooks(self): + hooks.register('head_meta', self.dispatch_seo_generation) + + def _get_article_seo_data(self, context, request, blog_setting): + article = context.get('article') + if not isinstance(article, Article): + return None + + description = strip_tags(article.body)[:150] + keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords + + meta_tags = f''' + + + + + + + + + ''' + for tag in article.tags.all(): + meta_tags += f'' + meta_tags += f'' + + structured_data = { + "@context": "https://schema.org", + "@type": "Article", + "mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()}, + "headline": article.title, + "description": description, + "image": request.build_absolute_uri(article.get_first_image_url()), + "datePublished": article.pub_time.isoformat(), + "dateModified": article.last_modify_time.isoformat(), + "author": {"@type": "Person", "name": article.author.username}, + "publisher": {"@type": "Organization", "name": blog_setting.site_name} + } + if not structured_data.get("image"): + del structured_data["image"] + + return { + "title": f"{article.title} | {blog_setting.site_name}", + "description": description, + "keywords": keywords, + "meta_tags": meta_tags, + "json_ld": structured_data + } + + def _get_category_seo_data(self, context, request, blog_setting): + category_name = context.get('tag_name') + if not category_name: + return None + + category = Category.objects.filter(name=category_name).first() + if not category: + return None + + title = f"{category.name} | {blog_setting.site_name}" + description = strip_tags(category.name) or blog_setting.site_description + keywords = category.name + + # BreadcrumbList structured data for category page + breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}] + breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()}) + + structured_data = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": breadcrumb_items + } + + return { + "title": title, + "description": description, + "keywords": keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def _get_default_seo_data(self, context, request, blog_setting): + # Homepage and other default pages + structured_data = { + "@context": "https://schema.org", + "@type": "WebSite", + "url": request.build_absolute_uri('/'), + "potentialAction": { + "@type": "SearchAction", + "target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}", + "query-input": "required name=search_term_string" + } + } + return { + "title": f"{blog_setting.site_name} | {blog_setting.site_description}", + "description": blog_setting.site_description, + "keywords": blog_setting.site_keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def dispatch_seo_generation(self, metas, context): + request = context.get('request') + if not request: + return metas + + view_name = request.resolver_match.view_name + blog_setting = get_blog_setting() + + seo_data = None + if view_name == 'blog:detailbyid': + seo_data = self._get_article_seo_data(context, request, blog_setting) + elif view_name == 'blog:category_detail': + seo_data = self._get_category_seo_data(context, request, blog_setting) + + if not seo_data: + seo_data = self._get_default_seo_data(context, request, blog_setting) + + json_ld_script = f'' + + return f""" + {seo_data.get("title", "")} + + + {seo_data.get("meta_tags", "")} + {json_ld_script} + """ + +plugin = SeoOptimizerPlugin() diff --git a/plugins/view_count/__init__.py b/plugins/view_count/__init__.py new file mode 100644 index 0000000..8804fdf --- /dev/null +++ b/plugins/view_count/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package \ No newline at end of file diff --git a/plugins/view_count/plugin.py b/plugins/view_count/plugin.py new file mode 100644 index 0000000..15e9d94 --- /dev/null +++ b/plugins/view_count/plugin.py @@ -0,0 +1,18 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks + + +class ViewCountPlugin(BasePlugin): + PLUGIN_NAME = '文章浏览次数统计' + PLUGIN_DESCRIPTION = '统计文章的浏览次数' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register('after_article_body_get', self.record_view) + + def record_view(self, article, *args, **kwargs): + article.viewed() + + +plugin = ViewCountPlugin() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..9dc5c935191f166db408850beb747475a262f65f GIT binary patch literal 2554 zcmZ{mOK)0H41~|RQhy3jxa83;x~aOVRHt-$MBCMc>m$SHk)3NsCx724O z&svob%XMU|V{iQC@?R zZ~0G!W+z_|B@^!EYb0|a8|zFJJ(1fNe%^|{En8uk>3b5kw*sb!x=F1+XweA^2Zpom&^VXcC#kB7{YUtyk1AW8lRCsH7kc&9|2FJZDW6fPxwcK<4UuCUBdsXLVs`_5F zp~0oR=sQS>?<%PjW*zBU@cbYY3-!J}L^}_3Y27N;PkG)pk*uJsc&?t#R3o*J(X1SA zka;NaQGO#GYHJxbBbSkJp-k9@@&PlNs$w3EcPR@sF|TltTB6Tpl&WX?P!AFM`gAq# ziDP5!b%x?N`7!_PG~o%JQRKX9Y6p=$>EBXOM*7a}6;HOi-71ev8|^_li^@^eSj@w__0A3aLAaD3lW$cY-+ejf+0D0n;c*g?m`=Y9 z#D{NVKaQOf&bHqN|K6p!G41^vmi8TR=TIe^a2}m??@)Ypmc4|+%#Zt&bBpa!Y_Lna z95ZK`d!>na86B&$`)xt#Oe-&k^ISIS%h_8C0ec(O{{Ln{w6Q!`KVv#@hd&K=)9UJoM~W6el-zM7!{x**T=1X z^wa1z+L2b5chAc%ZKb?k2fY03=7b3E0}!J+D^ z`Om~yc~RJ~yuIGH-QLq%`|ZTuPLXcJX{%xE{f6^yk|#Rdx2k=` SS`X?#O)JeKo3nk{t@?jbHFwMa literal 0 HcmV?d00001 diff --git a/servermanager/MemcacheStorage.py b/servermanager/MemcacheStorage.py new file mode 100644 index 0000000..38a7990 --- /dev/null +++ b/servermanager/MemcacheStorage.py @@ -0,0 +1,32 @@ +from werobot.session import SessionStorage +from werobot.utils import json_loads, json_dumps + +from djangoblog.utils import cache + + +class MemcacheStorage(SessionStorage): + def __init__(self, prefix='ws_'): + self.prefix = prefix + self.cache = cache + + @property + def is_available(self): + value = "1" + self.set('checkavaliable', value=value) + return value == self.get('checkavaliable') + + def key_name(self, s): + return '{prefix}{s}'.format(prefix=self.prefix, s=s) + + def get(self, id): + id = self.key_name(id) + session_json = self.cache.get(id) or '{}' + return json_loads(session_json) + + def set(self, id, value): + id = self.key_name(id) + self.cache.set(id, json_dumps(value)) + + def delete(self, id): + id = self.key_name(id) + self.cache.delete(id) diff --git a/servermanager/__init__.py b/servermanager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/servermanager/admin.py b/servermanager/admin.py new file mode 100644 index 0000000..f26f4f6 --- /dev/null +++ b/servermanager/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +# Register your models here. + + +class CommandsAdmin(admin.ModelAdmin): + list_display = ('title', 'command', 'describe') + + +class EmailSendLogAdmin(admin.ModelAdmin): + list_display = ('title', 'emailto', 'send_result', 'creation_time') + readonly_fields = ( + 'title', + 'emailto', + 'send_result', + 'creation_time', + 'content') + + def has_add_permission(self, request): + return False diff --git a/servermanager/api/__init__.py b/servermanager/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/servermanager/api/__init__.py @@ -0,0 +1 @@ + diff --git a/servermanager/api/blogapi.py b/servermanager/api/blogapi.py new file mode 100644 index 0000000..8a4d6ac --- /dev/null +++ b/servermanager/api/blogapi.py @@ -0,0 +1,27 @@ +from haystack.query import SearchQuerySet + +from blog.models import Article, Category + + +class BlogApi: + def __init__(self): + self.searchqueryset = SearchQuerySet() + self.searchqueryset.auto_query('') + self.__max_takecount__ = 8 + + def search_articles(self, query): + sqs = self.searchqueryset.auto_query(query) + sqs = sqs.load_all() + return sqs[:self.__max_takecount__] + + def get_category_lists(self): + return Category.objects.all() + + def get_category_articles(self, categoryname): + articles = Article.objects.filter(category__name=categoryname) + if articles: + return articles[:self.__max_takecount__] + return None + + def get_recent_articles(self): + return Article.objects.all()[:self.__max_takecount__] diff --git a/servermanager/api/commonapi.py b/servermanager/api/commonapi.py new file mode 100644 index 0000000..83ad9ff --- /dev/null +++ b/servermanager/api/commonapi.py @@ -0,0 +1,64 @@ +import logging +import os + +import openai + +from servermanager.models import commands + +logger = logging.getLogger(__name__) + +openai.api_key = os.environ.get('OPENAI_API_KEY') +if os.environ.get('HTTP_PROXY'): + openai.proxy = os.environ.get('HTTP_PROXY') + + +class ChatGPT: + + @staticmethod + def chat(prompt): + try: + completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}]) + return completion.choices[0].message.content + except Exception as e: + logger.error(e) + return "服务器出错了" + + +class CommandHandler: + def __init__(self): + self.commands = commands.objects.all() + + def run(self, title): + """ + 运行命令 + :param title: 命令 + :return: 返回命令执行结果 + """ + cmd = list( + filter( + lambda x: x.title.upper() == title.upper(), + self.commands)) + if cmd: + return self.__run_command__(cmd[0].command) + else: + return "未找到相关命令,请输入hepme获得帮助。" + + def __run_command__(self, cmd): + try: + res = os.popen(cmd).read() + return res + except BaseException: + return '命令执行出错!' + + def get_help(self): + rsp = '' + for cmd in self.commands: + rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) + return rsp + + +if __name__ == '__main__': + chatbot = ChatGPT() + prompt = "写一篇1000字关于AI的论文" + print(chatbot.chat(prompt)) diff --git a/servermanager/apps.py b/servermanager/apps.py new file mode 100644 index 0000000..03cc38d --- /dev/null +++ b/servermanager/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ServermanagerConfig(AppConfig): + name = 'servermanager' diff --git a/servermanager/migrations/0001_initial.py b/servermanager/migrations/0001_initial.py new file mode 100644 index 0000000..bbdbf77 --- /dev/null +++ b/servermanager/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='commands', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=300, verbose_name='命令标题')), + ('command', models.CharField(max_length=2000, verbose_name='命令')), + ('describe', models.CharField(max_length=300, verbose_name='命令描述')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '命令', + 'verbose_name_plural': '命令', + }, + ), + migrations.CreateModel( + name='EmailSendLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('emailto', models.CharField(max_length=300, verbose_name='收件人')), + ('title', models.CharField(max_length=2000, verbose_name='邮件标题')), + ('content', models.TextField(verbose_name='邮件内容')), + ('send_result', models.BooleanField(default=False, verbose_name='结果')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '邮件发送log', + 'verbose_name_plural': '邮件发送log', + 'ordering': ['-created_time'], + }, + ), + ] diff --git a/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py new file mode 100644 index 0000000..4858857 --- /dev/null +++ b/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servermanager', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='emailsendlog', + options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'}, + ), + migrations.RenameField( + model_name='commands', + old_name='created_time', + new_name='creation_time', + ), + migrations.RenameField( + model_name='commands', + old_name='last_mod_time', + new_name='last_modify_time', + ), + migrations.RenameField( + model_name='emailsendlog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/servermanager/migrations/__init__.py b/servermanager/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/servermanager/models.py b/servermanager/models.py new file mode 100644 index 0000000..4326c65 --- /dev/null +++ b/servermanager/models.py @@ -0,0 +1,33 @@ +from django.db import models + + +# Create your models here. +class commands(models.Model): + title = models.CharField('命令标题', max_length=300) + command = models.CharField('命令', max_length=2000) + describe = models.CharField('命令描述', max_length=300) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + last_modify_time = models.DateTimeField('修改时间', auto_now=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '命令' + verbose_name_plural = verbose_name + + +class EmailSendLog(models.Model): + emailto = models.CharField('收件人', max_length=300) + title = models.CharField('邮件标题', max_length=2000) + content = models.TextField('邮件内容') + send_result = models.BooleanField('结果', default=False) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '邮件发送log' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/servermanager/robot.py b/servermanager/robot.py new file mode 100644 index 0000000..7b45736 --- /dev/null +++ b/servermanager/robot.py @@ -0,0 +1,187 @@ +import logging +import os +import re + +import jsonpickle +from django.conf import settings +from werobot import WeRoBot +from werobot.replies import ArticlesReply, Article +from werobot.session.filestorage import FileStorage + +from djangoblog.utils import get_sha256 +from servermanager.api.blogapi import BlogApi +from servermanager.api.commonapi import ChatGPT, CommandHandler +from .MemcacheStorage import MemcacheStorage + +robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN') + or 'lylinux', enable_session=True) +memstorage = MemcacheStorage() +if memstorage.is_available: + robot.config['SESSION_STORAGE'] = memstorage +else: + if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')): + os.remove(os.path.join(settings.BASE_DIR, 'werobot_session')) + robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session') + +blogapi = BlogApi() +cmd_handler = CommandHandler() +logger = logging.getLogger(__name__) + + +def convert_to_article_reply(articles, message): + reply = ArticlesReply(message=message) + from blog.templatetags.blog_tags import truncatechars_content + for post in articles: + imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body) + imgurl = '' + if imgs: + imgurl = imgs[0] + article = Article( + title=post.title, + description=truncatechars_content(post.body), + img=imgurl, + url=post.get_full_url() + ) + reply.add_article(article) + return reply + + +@robot.filter(re.compile(r"^\?.*")) +def search(message, session): + s = message.content + searchstr = str(s).replace('?', '') + result = blogapi.search_articles(searchstr) + if result: + articles = list(map(lambda x: x.object, result)) + reply = convert_to_article_reply(articles, message) + return reply + else: + return '没有找到相关文章。' + + +@robot.filter(re.compile(r'^category\s*$', re.I)) +def category(message, session): + categorys = blogapi.get_category_lists() + content = ','.join(map(lambda x: x.name, categorys)) + return '所有文章分类目录:' + content + + +@robot.filter(re.compile(r'^recent\s*$', re.I)) +def recents(message, session): + articles = blogapi.get_recent_articles() + if articles: + reply = convert_to_article_reply(articles, message) + return reply + else: + return "暂时还没有文章" + + +@robot.filter(re.compile('^help$', re.I)) +def help(message, session): + return '''欢迎关注! + 默认会与图灵机器人聊天~~ + 你可以通过下面这些命令来获得信息 + ?关键字搜索文章. + 如?python. + category获得文章分类目录及文章数. + category-***获得该分类目录文章 + 如category-python + recent获得最新文章 + help获得帮助. + weather:获得天气 + 如weather:西安 + idcard:获得身份证信息 + 如idcard:61048119xxxxxxxxxx + music:音乐搜索 + 如music:阴天快乐 + PS:以上标点符号都不支持中文标点~~ + ''' + + +@robot.filter(re.compile(r'^weather\:.*$', re.I)) +def weather(message, session): + return "建设中..." + + +@robot.filter(re.compile(r'^idcard\:.*$', re.I)) +def idcard(message, session): + return "建设中..." + + +@robot.handler +def echo(message, session): + handler = MessageHandler(message, session) + return handler.handler() + + +class MessageHandler: + def __init__(self, message, session): + userid = message.source + self.message = message + self.session = session + self.userid = userid + try: + info = session[userid] + self.userinfo = jsonpickle.decode(info) + except Exception as e: + userinfo = WxUserInfo() + self.userinfo = userinfo + + @property + def is_admin(self): + return self.userinfo.isAdmin + + @property + def is_password_set(self): + return self.userinfo.isPasswordSet + + def save_session(self): + info = jsonpickle.encode(self.userinfo) + self.session[self.userid] = info + + def handler(self): + info = self.message.content + + if self.userinfo.isAdmin and info.upper() == 'EXIT': + self.userinfo = WxUserInfo() + self.save_session() + return "退出成功" + if info.upper() == 'ADMIN': + self.userinfo.isAdmin = True + self.save_session() + return "输入管理员密码" + if self.userinfo.isAdmin and not self.userinfo.isPasswordSet: + passwd = settings.WXADMIN + if settings.TESTING: + passwd = '123' + if passwd.upper() == get_sha256(get_sha256(info)).upper(): + self.userinfo.isPasswordSet = True + self.save_session() + return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" + else: + if self.userinfo.Count >= 3: + self.userinfo = WxUserInfo() + self.save_session() + return "超过验证次数" + self.userinfo.Count += 1 + self.save_session() + return "验证失败,请重新输入管理员密码:" + if self.userinfo.isAdmin and self.userinfo.isPasswordSet: + if self.userinfo.Command != '' and info.upper() == 'Y': + return cmd_handler.run(self.userinfo.Command) + else: + if info.upper() == 'HELPME': + return cmd_handler.get_help() + self.userinfo.Command = info + self.save_session() + return "确认执行: " + info + " 命令?" + + return ChatGPT.chat(info) + + +class WxUserInfo(): + def __init__(self): + self.isAdmin = False + self.isPasswordSet = False + self.Count = 0 + self.Command = '' diff --git a/servermanager/tests.py b/servermanager/tests.py new file mode 100644 index 0000000..22a6689 --- /dev/null +++ b/servermanager/tests.py @@ -0,0 +1,79 @@ +from django.test import Client, RequestFactory, TestCase +from django.utils import timezone +from werobot.messages.messages import TextMessage + +from accounts.models import BlogUser +from blog.models import Category, Article +from servermanager.api.commonapi import ChatGPT +from .models import commands +from .robot import MessageHandler, CommandHandler +from .robot import search, category, recents + + +# Create your tests here. +class ServerManagerTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_chat_gpt(self): + content = ChatGPT.chat("你好") + self.assertIsNotNone(content) + + def test_validate_comment(self): + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + self.client.login(username='liangliangyy1', password='liangliangyy1') + + c = Category() + c.name = "categoryccc" + c.save() + + article = Article() + article.title = "nicetitleccc" + article.body = "nicecontentccc" + article.author = user + article.category = c + article.type = 'a' + article.status = 'p' + article.save() + s = TextMessage([]) + s.content = "nice" + rsp = search(s, None) + rsp = category(None, None) + self.assertIsNotNone(rsp) + rsp = recents(None, None) + self.assertTrue(rsp != '暂时还没有文章') + + cmd = commands() + cmd.title = "test" + cmd.command = "ls" + cmd.describe = "test" + cmd.save() + + cmdhandler = CommandHandler() + rsp = cmdhandler.run('test') + self.assertIsNotNone(rsp) + s.source = 'u' + s.content = 'test' + msghandler = MessageHandler(s, {}) + + # msghandler.userinfo.isPasswordSet = True + # msghandler.userinfo.isAdmin = True + msghandler.handler() + s.content = 'y' + msghandler.handler() + s.content = 'idcard:12321233' + msghandler.handler() + s.content = 'weather:上海' + msghandler.handler() + s.content = 'admin' + msghandler.handler() + s.content = '123' + msghandler.handler() + + s.content = 'exit' + msghandler.handler() diff --git a/servermanager/urls.py b/servermanager/urls.py new file mode 100644 index 0000000..8d134d2 --- /dev/null +++ b/servermanager/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from werobot.contrib.django import make_view + +from .robot import robot + +app_name = "servermanager" +urlpatterns = [ + path(r'robot', make_view(robot)), + +] diff --git a/servermanager/views.py b/servermanager/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/servermanager/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/forget_password.html b/templates/account/forget_password.html new file mode 100644 index 0000000..3384531 --- /dev/null +++ b/templates/account/forget_password.html @@ -0,0 +1,30 @@ +{% extends 'share_layout/base_account.html' %} +{% load i18n %} +{% load static %} +{% block content %} +
+ + + + + +

+ Home Page + | + login page +

+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/account/login.html b/templates/account/login.html new file mode 100644 index 0000000..cff8d33 --- /dev/null +++ b/templates/account/login.html @@ -0,0 +1,46 @@ +{% extends 'share_layout/base_account.html' %} +{% load static %} +{% load i18n %} +{% block content %} +
+ + + + + +

+ + {% trans 'Create Account' %} + + | + Home Page + | + + {% trans 'Forget Password' %} + +

+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/account/registration_form.html b/templates/account/registration_form.html new file mode 100644 index 0000000..65e7549 --- /dev/null +++ b/templates/account/registration_form.html @@ -0,0 +1,29 @@ +{% extends 'share_layout/base_account.html' %} +{% load static %} +{% block content %} +
+ + + + + +

+ Sign In +

+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/account/result.html b/templates/account/result.html new file mode 100644 index 0000000..23c9094 --- /dev/null +++ b/templates/account/result.html @@ -0,0 +1,27 @@ +{% extends 'share_layout/base.html' %} +{% load i18n %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/blog/article_archives.html b/templates/blog/article_archives.html new file mode 100644 index 0000000..959319e --- /dev/null +++ b/templates/blog/article_archives.html @@ -0,0 +1,60 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% load i18n %} +{% block header %} + + {% trans 'article archive' %} | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+ +

{% trans 'article archive' %}

+
+ +
+ + {% regroup article_list by pub_time.year as year_post_group %} +
    + {% for year in year_post_group %} +
  • {{ year.grouper }} {% trans 'year' %} + {% regroup year.list by pub_time.month as month_post_group %} +
      + {% for month in month_post_group %} +
    • {{ month.grouper }} {% trans 'month' %} + +
    • + {% endfor %} +
    +
  • + {% endfor %} +
+
+
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/templates/blog/article_detail.html b/templates/blog/article_detail.html new file mode 100644 index 0000000..a74a0db --- /dev/null +++ b/templates/blog/article_detail.html @@ -0,0 +1,52 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} + +{% block header %} +{% endblock %} +{% block content %} +
+
+ {% load_article_detail article False user %} + + {% if article.type == 'a' %} + + {% endif %} + +
+ {% if article.comment_status == "o" and OPEN_SITE_COMMENT %} + + + {% include 'comments/tags/comment_list.html' %} + {% if user.is_authenticated %} + {% include 'comments/tags/post_comment.html' %} + {% else %} +
+

您还没有登录,请您登录后发表评论。 +

+ + {% load oauth_tags %} + {% load_oauth_applications request %} + +
+ {% endif %} + {% endif %} +
+ +{% endblock %} + +{% block sidebar %} + {% load_sidebar user "p" %} +{% endblock %} \ No newline at end of file diff --git a/templates/blog/article_index.html b/templates/blog/article_index.html new file mode 100644 index 0000000..0ee6150 --- /dev/null +++ b/templates/blog/article_index.html @@ -0,0 +1,42 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ {% if page_type and tag_name %} +
+ +

{{ page_type }}:{{ tag_name }}

+
+ {% endif %} + + {% for article in article_list %} + {% load_article_detail article True user %} + {% endfor %} + {% if is_paginated %} + {% load_pagination_info page_obj page_type tag_name %} + + {% endif %} +
+
+ +{% endblock %} +{% block sidebar %} + {% load_sidebar user linktype %} +{% endblock %} \ No newline at end of file diff --git a/templates/blog/error_page.html b/templates/blog/error_page.html new file mode 100644 index 0000000..d41cfb6 --- /dev/null +++ b/templates/blog/error_page.html @@ -0,0 +1,45 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {% if statuscode == '404' %} + 404 NotFound + {% elif statuscode == '403' %} + Permission Denied + {% elif statuscode == '500' %} + 500 Error + {% else %} + + {% endif %} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+

{{ message }}

+
+ +
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/templates/blog/links_list.html b/templates/blog/links_list.html new file mode 100644 index 0000000..ccecbea --- /dev/null +++ b/templates/blog/links_list.html @@ -0,0 +1,44 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + + 友情链接 | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+ +

友情链接

+
+ +
+ +
+
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/templates/blog/tags/article_info.html b/templates/blog/tags/article_info.html new file mode 100644 index 0000000..3deec44 --- /dev/null +++ b/templates/blog/tags/article_info.html @@ -0,0 +1,74 @@ +{% load blog_tags %} +{% load cache %} +{% load i18n %} +
+
+ +

+ {% if isindex %} + {% if article.article_order > 0 %} + 【{% trans 'pin to top' %}】{{ article.title }} + {% else %} + {{ article.title }} + {% endif %} + + {% else %} + {{ article.title }} + {% endif %} +

+ +
+ {% if article.type == 'a' %} + {% if not isindex %} + {% cache 36000 breadcrumb article.pk %} + {% load_breadcrumb article %} + {% endcache %} + {% endif %} + {% endif %} +
+ +
+ {% if isindex %} + {{ article.body|custom_markdown|escape|truncatechars_content }} +

Read more

+ {% else %} + + {% if article.show_toc %} + {% get_markdown_toc article.body as toc %} + {% trans 'toc' %}: + {{ toc|safe }} + +
+ {% endif %} +
+ + {{ article.body|custom_markdown|escape }} + +
+ {% endif %} + +
+ + {% load_article_metas article user %} + +
\ No newline at end of file diff --git a/templates/blog/tags/article_meta_info.html b/templates/blog/tags/article_meta_info.html new file mode 100644 index 0000000..cb6111c --- /dev/null +++ b/templates/blog/tags/article_meta_info.html @@ -0,0 +1,59 @@ +{% load i18n %} +{% load blog_tags %} + + + + + diff --git a/templates/blog/tags/article_pagination.html b/templates/blog/tags/article_pagination.html new file mode 100644 index 0000000..95514ff --- /dev/null +++ b/templates/blog/tags/article_pagination.html @@ -0,0 +1,17 @@ +{% load i18n %} + \ No newline at end of file diff --git a/templates/blog/tags/article_tag_list.html b/templates/blog/tags/article_tag_list.html new file mode 100644 index 0000000..c8ba474 --- /dev/null +++ b/templates/blog/tags/article_tag_list.html @@ -0,0 +1,19 @@ +{% load i18n %} +{% if article_tags_list %} +
+
+ {% trans 'tags' %} +
+
+ + {% for url,count,tag,color in article_tags_list %} + + {{ tag.name }} + {{ count }} + + {% endfor %} + +
+
+{% endif %} diff --git a/templates/blog/tags/breadcrumb.html b/templates/blog/tags/breadcrumb.html new file mode 100644 index 0000000..67087d5 --- /dev/null +++ b/templates/blog/tags/breadcrumb.html @@ -0,0 +1,19 @@ + + diff --git a/templates/blog/tags/sidebar.html b/templates/blog/tags/sidebar.html new file mode 100644 index 0000000..f70544c --- /dev/null +++ b/templates/blog/tags/sidebar.html @@ -0,0 +1,136 @@ +{% load blog_tags %} +{% load i18n %} + diff --git a/templates/comments/tags/comment_item.html b/templates/comments/tags/comment_item.html new file mode 100644 index 0000000..ebb0388 --- /dev/null +++ b/templates/comments/tags/comment_item.html @@ -0,0 +1,34 @@ +{% load blog_tags %} +
  • +
    + + + +

    {{ comment_item.body|escape|comment_markdown }}

    + +
    + +
  • \ No newline at end of file diff --git a/templates/comments/tags/comment_item_tree.html b/templates/comments/tags/comment_item_tree.html new file mode 100644 index 0000000..a9decd1 --- /dev/null +++ b/templates/comments/tags/comment_item_tree.html @@ -0,0 +1,54 @@ +{% load blog_tags %} +
  • +
    + + + +

    + {% if comment_item.parent_comment %} +

    + {% endif %} +

    + +

    {{ comment_item.body|escape|comment_markdown }}

    + + +
    + +
  • +{% query article_comments parent_comment=comment_item as cc_comments %} +{% for cc in cc_comments %} + {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %} + {% if depth >= 1 %} + {% include template_name %} + {% else %} + {% with depth=depth|add:1 %} + {% include template_name %} + {% endwith %} + {% endif %} + {% endwith %} +{% endfor %} \ No newline at end of file diff --git a/templates/comments/tags/comment_list.html b/templates/comments/tags/comment_list.html new file mode 100644 index 0000000..4092161 --- /dev/null +++ b/templates/comments/tags/comment_list.html @@ -0,0 +1,45 @@ + +
    + {% load blog_tags %} + {% load comments_tags %} + {% load cache %} + + + {% if article_comments %} +
    +
      + {# {% query article_comments parent_comment=None as parent_comments %}#} + {% for comment_item in p_comments %} + + {% with 0 as depth %} + {% include "comments/tags/comment_item_tree.html" %} + {% endwith %} + {% endfor %} + +
    + +
    +
    + {% endif %} +
    + +
    \ No newline at end of file diff --git a/templates/comments/tags/post_comment.html b/templates/comments/tags/post_comment.html new file mode 100644 index 0000000..3ae5a27 --- /dev/null +++ b/templates/comments/tags/post_comment.html @@ -0,0 +1,33 @@ +
    + +
    +

    发表评论 + +

    +
    {% csrf_token %} +

    + {{ form.body.label_tag }} + + {{ form.body }} + {{ form.body.errors }} +

    + {{ form.parent_comment_id }} +
    + {% if COMMENT_NEED_REVIEW %} + 支持markdown,评论经审核后才会显示。 + {% else %} + 支持markdown。 + {% endif %} + + +
    +
    +
    + +
    + + diff --git a/templates/oauth/bindsuccess.html b/templates/oauth/bindsuccess.html new file mode 100644 index 0000000..4bee77c --- /dev/null +++ b/templates/oauth/bindsuccess.html @@ -0,0 +1,22 @@ +{% extends 'share_layout/base.html' %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
    +
    + +
    + +

    {{ content }}

    +
    +
    +
    + + 登录 + | + 回到首页 +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/oauth/oauth_applications.html b/templates/oauth/oauth_applications.html new file mode 100644 index 0000000..a841ad2 --- /dev/null +++ b/templates/oauth/oauth_applications.html @@ -0,0 +1,13 @@ +{% load i18n %} + diff --git a/templates/oauth/require_email.html b/templates/oauth/require_email.html new file mode 100644 index 0000000..3adef12 --- /dev/null +++ b/templates/oauth/require_email.html @@ -0,0 +1,46 @@ +{% extends 'share_layout/base_account.html' %} + +{% load static %} +{% block content %} +
    + + + + + +

    + 登录 +

    + +
    +{% endblock %} \ No newline at end of file diff --git a/templates/owntracks/show_log_dates.html b/templates/owntracks/show_log_dates.html new file mode 100644 index 0000000..7dbba21 --- /dev/null +++ b/templates/owntracks/show_log_dates.html @@ -0,0 +1,17 @@ + + + + + 记录日期 + + + +
      + {% for date in results %} +
    • + {{ date }} +
    • + {% endfor %} +
    + + \ No newline at end of file diff --git a/templates/owntracks/show_maps.html b/templates/owntracks/show_maps.html new file mode 100644 index 0000000..3aeda36 --- /dev/null +++ b/templates/owntracks/show_maps.html @@ -0,0 +1,135 @@ + + + + + + + 运动轨迹 + + + +
    + + + + + + + + \ No newline at end of file diff --git a/templates/search/indexes/blog/article_text.txt b/templates/search/indexes/blog/article_text.txt new file mode 100644 index 0000000..4f9ca76 --- /dev/null +++ b/templates/search/indexes/blog/article_text.txt @@ -0,0 +1,3 @@ +{{ object.title }} +{{ object.author.username }} +{{ object.body }} \ No newline at end of file diff --git a/templates/search/search.html b/templates/search/search.html new file mode 100644 index 0000000..1404c60 --- /dev/null +++ b/templates/search/search.html @@ -0,0 +1,66 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% block header %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + + + + + + + +{% endblock %} +{% block content %} +
    +
    + {% if query %} +
    + {% if suggestion %} +

    + 已显示 “{{ suggestion }}” 的搜索结果。   + 仍然搜索:{{ query }}
    +

    + {% else %} +

    + 搜索:{{ query }}    +

    + {% endif %} +
    + {% endif %} + {% if query and page.object_list %} + {% for article in page.object_list %} + {% load_article_detail article.object True user %} + {% endfor %} + {% if page.has_previous or page.has_next %} + + + {% endif %} + {% else %} +
    + +

    哎呀,关键字:{{ query }}没有找到结果,要不换个词再试试?

    +
    + {% endif %} +
    +
    +{% endblock %} + + +{% block sidebar %} + {% load_sidebar request.user 'i' %} +{% endblock %} + + diff --git a/templates/share_layout/adsense.html b/templates/share_layout/adsense.html new file mode 100644 index 0000000..8f99c55 --- /dev/null +++ b/templates/share_layout/adsense.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/templates/share_layout/base.html b/templates/share_layout/base.html new file mode 100644 index 0000000..75d0df5 --- /dev/null +++ b/templates/share_layout/base.html @@ -0,0 +1,123 @@ +{% load static %} +{% load cache %} +{% load i18n %} +{% load compress %} + + + + + + + + + + {% block header %} + {% block title %}{{ SITE_NAME }}{% endblock %} + + + {% endblock %} + {% load blog_tags %} + {% head_meta %} + + + + + + + + + + + {% compress css %} + + + + {% comment %}{% endcomment %} + + + + {% block compress_css %} + {% endblock %} + {% endcompress %} + {% if GLOBAL_HEADER %} + {{ GLOBAL_HEADER|safe }} + {% endif %} + + + +
    + +
    + + {% block content %} + {% endblock %} + + + {% block sidebar %} + {% endblock %} + + +
    + {% include 'share_layout/footer.html' %} +
    + + +
    + + {% compress js %} + + + + + + {% block compress_js %} + {% endblock %} + {% endcompress %} + {% block footer %} + {% endblock %} +
    + diff --git a/templates/share_layout/base_account.html b/templates/share_layout/base_account.html new file mode 100644 index 0000000..c00d842 --- /dev/null +++ b/templates/share_layout/base_account.html @@ -0,0 +1,47 @@ + + + + {% load static %} + + + + + + + + + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + + {% load compress %} + {% compress css %} + + + + + + + + + + {% endcompress %} + {% compress js %} + + + {% endcompress %} + + + + + +{% block content %} +{% endblock %} + + + + + + + \ No newline at end of file diff --git a/templates/share_layout/footer.html b/templates/share_layout/footer.html new file mode 100644 index 0000000..cd86a29 --- /dev/null +++ b/templates/share_layout/footer.html @@ -0,0 +1,56 @@ + + + diff --git a/templates/share_layout/nav.html b/templates/share_layout/nav.html new file mode 100644 index 0000000..24d4da6 --- /dev/null +++ b/templates/share_layout/nav.html @@ -0,0 +1,30 @@ +{% load i18n %} + + \ No newline at end of file diff --git a/templates/share_layout/nav_node.html b/templates/share_layout/nav_node.html new file mode 100644 index 0000000..c266880 --- /dev/null +++ b/templates/share_layout/nav_node.html @@ -0,0 +1,19 @@ + + + -- 2.34.1 From 804ff2a3f34a731056185230637b346e665d569b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=9B=AA?= <2518549229@qq.com> Date: Thu, 25 Sep 2025 23:15:56 +0800 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=E5=B0=86=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=BA=90=E4=BB=A3=E7=A0=81=E7=9B=AE=E5=BD=95=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=88=B0=20src=20=E4=B8=8B=EF=BC=8C=E8=A7=84=E8=8C=83=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {accounts => src/accounts}/__init__.py | 0 {accounts => src/accounts}/admin.py | 0 {accounts => src/accounts}/apps.py | 0 {accounts => src/accounts}/forms.py | 0 {accounts => src/accounts}/migrations/0001_initial.py | 0 ...lter_bloguser_options_remove_bloguser_created_time_and_more.py | 0 {accounts => src/accounts}/migrations/__init__.py | 0 {accounts => src/accounts}/models.py | 0 {accounts => src/accounts}/templatetags/__init__.py | 0 {accounts => src/accounts}/tests.py | 0 {accounts => src/accounts}/urls.py | 0 {accounts => src/accounts}/user_login_backend.py | 0 {accounts => src/accounts}/utils.py | 0 {accounts => src/accounts}/views.py | 0 {blog => src/blog}/__init__.py | 0 {blog => src/blog}/admin.py | 0 {blog => src/blog}/apps.py | 0 {blog => src/blog}/context_processors.py | 0 {blog => src/blog}/documents.py | 0 {blog => src/blog}/forms.py | 0 {blog => src/blog}/management/__init__.py | 0 {blog => src/blog}/management/commands/__init__.py | 0 {blog => src/blog}/management/commands/build_index.py | 0 {blog => src/blog}/management/commands/build_search_words.py | 0 {blog => src/blog}/management/commands/clear_cache.py | 0 {blog => src/blog}/management/commands/create_testdata.py | 0 {blog => src/blog}/management/commands/ping_baidu.py | 0 {blog => src/blog}/management/commands/sync_user_avatar.py | 0 {blog => src/blog}/middleware.py | 0 {blog => src/blog}/migrations/0001_initial.py | 0 .../blog}/migrations/0002_blogsettings_global_footer_and_more.py | 0 .../blog}/migrations/0003_blogsettings_comment_need_review.py | 0 ...4_rename_analyticscode_blogsettings_analytics_code_and_more.py | 0 .../0005_alter_article_options_alter_category_options_and_more.py | 0 {blog => src/blog}/migrations/0006_alter_blogsettings_options.py | 0 {blog => src/blog}/migrations/__init__.py | 0 {blog => src/blog}/models.py | 0 {blog => src/blog}/search_indexes.py | 0 {blog => src/blog}/templatetags/__init__.py | 0 {blog => src/blog}/templatetags/blog_tags.py | 0 {blog => src/blog}/tests.py | 0 {blog => src/blog}/urls.py | 0 {blog => src/blog}/views.py | 0 {comments => src/comments}/__init__.py | 0 {comments => src/comments}/admin.py | 0 {comments => src/comments}/apps.py | 0 {comments => src/comments}/forms.py | 0 {comments => src/comments}/migrations/0001_initial.py | 0 .../comments}/migrations/0002_alter_comment_is_enable.py | 0 ..._alter_comment_options_remove_comment_created_time_and_more.py | 0 {comments => src/comments}/migrations/__init__.py | 0 {comments => src/comments}/models.py | 0 {comments => src/comments}/templatetags/__init__.py | 0 {comments => src/comments}/templatetags/comments_tags.py | 0 {comments => src/comments}/tests.py | 0 {comments => src/comments}/urls.py | 0 {comments => src/comments}/utils.py | 0 {comments => src/comments}/views.py | 0 {djangoblog => src/djangoblog}/__init__.py | 0 {djangoblog => src/djangoblog}/admin_site.py | 0 {djangoblog => src/djangoblog}/apps.py | 0 {djangoblog => src/djangoblog}/blog_signals.py | 0 {djangoblog => src/djangoblog}/elasticsearch_backend.py | 0 {djangoblog => src/djangoblog}/feeds.py | 0 {djangoblog => src/djangoblog}/logentryadmin.py | 0 {djangoblog => src/djangoblog}/plugin_manage/base_plugin.py | 0 {djangoblog => src/djangoblog}/plugin_manage/hook_constants.py | 0 {djangoblog => src/djangoblog}/plugin_manage/hooks.py | 0 {djangoblog => src/djangoblog}/plugin_manage/loader.py | 0 {djangoblog => src/djangoblog}/settings.py | 0 {djangoblog => src/djangoblog}/sitemap.py | 0 {djangoblog => src/djangoblog}/spider_notify.py | 0 {djangoblog => src/djangoblog}/tests.py | 0 {djangoblog => src/djangoblog}/urls.py | 0 {djangoblog => src/djangoblog}/utils.py | 0 {djangoblog => src/djangoblog}/whoosh_cn_backend.py | 0 {djangoblog => src/djangoblog}/wsgi.py | 0 {oauth => src/oauth}/__init__.py | 0 {oauth => src/oauth}/admin.py | 0 {oauth => src/oauth}/apps.py | 0 {oauth => src/oauth}/forms.py | 0 {oauth => src/oauth}/migrations/0001_initial.py | 0 ..._alter_oauthconfig_options_alter_oauthuser_options_and_more.py | 0 {oauth => src/oauth}/migrations/0003_alter_oauthuser_nickname.py | 0 {oauth => src/oauth}/migrations/__init__.py | 0 {oauth => src/oauth}/models.py | 0 {oauth => src/oauth}/oauthmanager.py | 0 {oauth => src/oauth}/templatetags/__init__.py | 0 {oauth => src/oauth}/templatetags/oauth_tags.py | 0 {oauth => src/oauth}/tests.py | 0 {oauth => src/oauth}/urls.py | 0 {oauth => src/oauth}/views.py | 0 {owntracks => src/owntracks}/__init__.py | 0 {owntracks => src/owntracks}/admin.py | 0 {owntracks => src/owntracks}/apps.py | 0 {owntracks => src/owntracks}/migrations/0001_initial.py | 0 .../migrations/0002_alter_owntracklog_options_and_more.py | 0 {owntracks => src/owntracks}/migrations/__init__.py | 0 {owntracks => src/owntracks}/models.py | 0 {owntracks => src/owntracks}/tests.py | 0 {owntracks => src/owntracks}/urls.py | 0 {owntracks => src/owntracks}/views.py | 0 {plugins => src/plugins}/__init__.py | 0 {plugins => src/plugins}/article_copyright/__init__.py | 0 {plugins => src/plugins}/article_copyright/plugin.py | 0 {plugins => src/plugins}/external_links/__init__.py | 0 {plugins => src/plugins}/external_links/plugin.py | 0 {plugins => src/plugins}/reading_time/__init__.py | 0 {plugins => src/plugins}/reading_time/plugin.py | 0 {plugins => src/plugins}/seo_optimizer/__init__.py | 0 {plugins => src/plugins}/seo_optimizer/plugin.py | 0 {plugins => src/plugins}/view_count/__init__.py | 0 {plugins => src/plugins}/view_count/plugin.py | 0 {servermanager => src/servermanager}/MemcacheStorage.py | 0 {servermanager => src/servermanager}/__init__.py | 0 {servermanager => src/servermanager}/admin.py | 0 {servermanager => src/servermanager}/api/__init__.py | 0 {servermanager => src/servermanager}/api/blogapi.py | 0 {servermanager => src/servermanager}/api/commonapi.py | 0 {servermanager => src/servermanager}/apps.py | 0 {servermanager => src/servermanager}/migrations/0001_initial.py | 0 .../migrations/0002_alter_emailsendlog_options_and_more.py | 0 {servermanager => src/servermanager}/migrations/__init__.py | 0 {servermanager => src/servermanager}/models.py | 0 {servermanager => src/servermanager}/robot.py | 0 {servermanager => src/servermanager}/tests.py | 0 {servermanager => src/servermanager}/urls.py | 0 {servermanager => src/servermanager}/views.py | 0 {templates => src/templates}/account/forget_password.html | 0 {templates => src/templates}/account/login.html | 0 {templates => src/templates}/account/registration_form.html | 0 {templates => src/templates}/account/result.html | 0 {templates => src/templates}/blog/article_archives.html | 0 {templates => src/templates}/blog/article_detail.html | 0 {templates => src/templates}/blog/article_index.html | 0 {templates => src/templates}/blog/error_page.html | 0 {templates => src/templates}/blog/links_list.html | 0 {templates => src/templates}/blog/tags/article_info.html | 0 {templates => src/templates}/blog/tags/article_meta_info.html | 0 {templates => src/templates}/blog/tags/article_pagination.html | 0 {templates => src/templates}/blog/tags/article_tag_list.html | 0 {templates => src/templates}/blog/tags/breadcrumb.html | 0 {templates => src/templates}/blog/tags/sidebar.html | 0 {templates => src/templates}/comments/tags/comment_item.html | 0 {templates => src/templates}/comments/tags/comment_item_tree.html | 0 {templates => src/templates}/comments/tags/comment_list.html | 0 {templates => src/templates}/comments/tags/post_comment.html | 0 {templates => src/templates}/oauth/bindsuccess.html | 0 {templates => src/templates}/oauth/oauth_applications.html | 0 {templates => src/templates}/oauth/require_email.html | 0 {templates => src/templates}/owntracks/show_log_dates.html | 0 {templates => src/templates}/owntracks/show_maps.html | 0 {templates => src/templates}/search/indexes/blog/article_text.txt | 0 {templates => src/templates}/search/search.html | 0 {templates => src/templates}/share_layout/adsense.html | 0 {templates => src/templates}/share_layout/base.html | 0 {templates => src/templates}/share_layout/base_account.html | 0 {templates => src/templates}/share_layout/footer.html | 0 {templates => src/templates}/share_layout/nav.html | 0 {templates => src/templates}/share_layout/nav_node.html | 0 160 files changed, 0 insertions(+), 0 deletions(-) rename {accounts => src/accounts}/__init__.py (100%) rename {accounts => src/accounts}/admin.py (100%) rename {accounts => src/accounts}/apps.py (100%) rename {accounts => src/accounts}/forms.py (100%) rename {accounts => src/accounts}/migrations/0001_initial.py (100%) rename {accounts => src/accounts}/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py (100%) rename {accounts => src/accounts}/migrations/__init__.py (100%) rename {accounts => src/accounts}/models.py (100%) rename {accounts => src/accounts}/templatetags/__init__.py (100%) rename {accounts => src/accounts}/tests.py (100%) rename {accounts => src/accounts}/urls.py (100%) rename {accounts => src/accounts}/user_login_backend.py (100%) rename {accounts => src/accounts}/utils.py (100%) rename {accounts => src/accounts}/views.py (100%) rename {blog => src/blog}/__init__.py (100%) rename {blog => src/blog}/admin.py (100%) rename {blog => src/blog}/apps.py (100%) rename {blog => src/blog}/context_processors.py (100%) rename {blog => src/blog}/documents.py (100%) rename {blog => src/blog}/forms.py (100%) rename {blog => src/blog}/management/__init__.py (100%) rename {blog => src/blog}/management/commands/__init__.py (100%) rename {blog => src/blog}/management/commands/build_index.py (100%) rename {blog => src/blog}/management/commands/build_search_words.py (100%) rename {blog => src/blog}/management/commands/clear_cache.py (100%) rename {blog => src/blog}/management/commands/create_testdata.py (100%) rename {blog => src/blog}/management/commands/ping_baidu.py (100%) rename {blog => src/blog}/management/commands/sync_user_avatar.py (100%) rename {blog => src/blog}/middleware.py (100%) rename {blog => src/blog}/migrations/0001_initial.py (100%) rename {blog => src/blog}/migrations/0002_blogsettings_global_footer_and_more.py (100%) rename {blog => src/blog}/migrations/0003_blogsettings_comment_need_review.py (100%) rename {blog => src/blog}/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py (100%) rename {blog => src/blog}/migrations/0005_alter_article_options_alter_category_options_and_more.py (100%) rename {blog => src/blog}/migrations/0006_alter_blogsettings_options.py (100%) rename {blog => src/blog}/migrations/__init__.py (100%) rename {blog => src/blog}/models.py (100%) rename {blog => src/blog}/search_indexes.py (100%) rename {blog => src/blog}/templatetags/__init__.py (100%) rename {blog => src/blog}/templatetags/blog_tags.py (100%) rename {blog => src/blog}/tests.py (100%) rename {blog => src/blog}/urls.py (100%) rename {blog => src/blog}/views.py (100%) rename {comments => src/comments}/__init__.py (100%) rename {comments => src/comments}/admin.py (100%) rename {comments => src/comments}/apps.py (100%) rename {comments => src/comments}/forms.py (100%) rename {comments => src/comments}/migrations/0001_initial.py (100%) rename {comments => src/comments}/migrations/0002_alter_comment_is_enable.py (100%) rename {comments => src/comments}/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py (100%) rename {comments => src/comments}/migrations/__init__.py (100%) rename {comments => src/comments}/models.py (100%) rename {comments => src/comments}/templatetags/__init__.py (100%) rename {comments => src/comments}/templatetags/comments_tags.py (100%) rename {comments => src/comments}/tests.py (100%) rename {comments => src/comments}/urls.py (100%) rename {comments => src/comments}/utils.py (100%) rename {comments => src/comments}/views.py (100%) rename {djangoblog => src/djangoblog}/__init__.py (100%) rename {djangoblog => src/djangoblog}/admin_site.py (100%) rename {djangoblog => src/djangoblog}/apps.py (100%) rename {djangoblog => src/djangoblog}/blog_signals.py (100%) rename {djangoblog => src/djangoblog}/elasticsearch_backend.py (100%) rename {djangoblog => src/djangoblog}/feeds.py (100%) rename {djangoblog => src/djangoblog}/logentryadmin.py (100%) rename {djangoblog => src/djangoblog}/plugin_manage/base_plugin.py (100%) rename {djangoblog => src/djangoblog}/plugin_manage/hook_constants.py (100%) rename {djangoblog => src/djangoblog}/plugin_manage/hooks.py (100%) rename {djangoblog => src/djangoblog}/plugin_manage/loader.py (100%) rename {djangoblog => src/djangoblog}/settings.py (100%) rename {djangoblog => src/djangoblog}/sitemap.py (100%) rename {djangoblog => src/djangoblog}/spider_notify.py (100%) rename {djangoblog => src/djangoblog}/tests.py (100%) rename {djangoblog => src/djangoblog}/urls.py (100%) rename {djangoblog => src/djangoblog}/utils.py (100%) rename {djangoblog => src/djangoblog}/whoosh_cn_backend.py (100%) rename {djangoblog => src/djangoblog}/wsgi.py (100%) rename {oauth => src/oauth}/__init__.py (100%) rename {oauth => src/oauth}/admin.py (100%) rename {oauth => src/oauth}/apps.py (100%) rename {oauth => src/oauth}/forms.py (100%) rename {oauth => src/oauth}/migrations/0001_initial.py (100%) rename {oauth => src/oauth}/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py (100%) rename {oauth => src/oauth}/migrations/0003_alter_oauthuser_nickname.py (100%) rename {oauth => src/oauth}/migrations/__init__.py (100%) rename {oauth => src/oauth}/models.py (100%) rename {oauth => src/oauth}/oauthmanager.py (100%) rename {oauth => src/oauth}/templatetags/__init__.py (100%) rename {oauth => src/oauth}/templatetags/oauth_tags.py (100%) rename {oauth => src/oauth}/tests.py (100%) rename {oauth => src/oauth}/urls.py (100%) rename {oauth => src/oauth}/views.py (100%) rename {owntracks => src/owntracks}/__init__.py (100%) rename {owntracks => src/owntracks}/admin.py (100%) rename {owntracks => src/owntracks}/apps.py (100%) rename {owntracks => src/owntracks}/migrations/0001_initial.py (100%) rename {owntracks => src/owntracks}/migrations/0002_alter_owntracklog_options_and_more.py (100%) rename {owntracks => src/owntracks}/migrations/__init__.py (100%) rename {owntracks => src/owntracks}/models.py (100%) rename {owntracks => src/owntracks}/tests.py (100%) rename {owntracks => src/owntracks}/urls.py (100%) rename {owntracks => src/owntracks}/views.py (100%) rename {plugins => src/plugins}/__init__.py (100%) rename {plugins => src/plugins}/article_copyright/__init__.py (100%) rename {plugins => src/plugins}/article_copyright/plugin.py (100%) rename {plugins => src/plugins}/external_links/__init__.py (100%) rename {plugins => src/plugins}/external_links/plugin.py (100%) rename {plugins => src/plugins}/reading_time/__init__.py (100%) rename {plugins => src/plugins}/reading_time/plugin.py (100%) rename {plugins => src/plugins}/seo_optimizer/__init__.py (100%) rename {plugins => src/plugins}/seo_optimizer/plugin.py (100%) rename {plugins => src/plugins}/view_count/__init__.py (100%) rename {plugins => src/plugins}/view_count/plugin.py (100%) rename {servermanager => src/servermanager}/MemcacheStorage.py (100%) rename {servermanager => src/servermanager}/__init__.py (100%) rename {servermanager => src/servermanager}/admin.py (100%) rename {servermanager => src/servermanager}/api/__init__.py (100%) rename {servermanager => src/servermanager}/api/blogapi.py (100%) rename {servermanager => src/servermanager}/api/commonapi.py (100%) rename {servermanager => src/servermanager}/apps.py (100%) rename {servermanager => src/servermanager}/migrations/0001_initial.py (100%) rename {servermanager => src/servermanager}/migrations/0002_alter_emailsendlog_options_and_more.py (100%) rename {servermanager => src/servermanager}/migrations/__init__.py (100%) rename {servermanager => src/servermanager}/models.py (100%) rename {servermanager => src/servermanager}/robot.py (100%) rename {servermanager => src/servermanager}/tests.py (100%) rename {servermanager => src/servermanager}/urls.py (100%) rename {servermanager => src/servermanager}/views.py (100%) rename {templates => src/templates}/account/forget_password.html (100%) rename {templates => src/templates}/account/login.html (100%) rename {templates => src/templates}/account/registration_form.html (100%) rename {templates => src/templates}/account/result.html (100%) rename {templates => src/templates}/blog/article_archives.html (100%) rename {templates => src/templates}/blog/article_detail.html (100%) rename {templates => src/templates}/blog/article_index.html (100%) rename {templates => src/templates}/blog/error_page.html (100%) rename {templates => src/templates}/blog/links_list.html (100%) rename {templates => src/templates}/blog/tags/article_info.html (100%) rename {templates => src/templates}/blog/tags/article_meta_info.html (100%) rename {templates => src/templates}/blog/tags/article_pagination.html (100%) rename {templates => src/templates}/blog/tags/article_tag_list.html (100%) rename {templates => src/templates}/blog/tags/breadcrumb.html (100%) rename {templates => src/templates}/blog/tags/sidebar.html (100%) rename {templates => src/templates}/comments/tags/comment_item.html (100%) rename {templates => src/templates}/comments/tags/comment_item_tree.html (100%) rename {templates => src/templates}/comments/tags/comment_list.html (100%) rename {templates => src/templates}/comments/tags/post_comment.html (100%) rename {templates => src/templates}/oauth/bindsuccess.html (100%) rename {templates => src/templates}/oauth/oauth_applications.html (100%) rename {templates => src/templates}/oauth/require_email.html (100%) rename {templates => src/templates}/owntracks/show_log_dates.html (100%) rename {templates => src/templates}/owntracks/show_maps.html (100%) rename {templates => src/templates}/search/indexes/blog/article_text.txt (100%) rename {templates => src/templates}/search/search.html (100%) rename {templates => src/templates}/share_layout/adsense.html (100%) rename {templates => src/templates}/share_layout/base.html (100%) rename {templates => src/templates}/share_layout/base_account.html (100%) rename {templates => src/templates}/share_layout/footer.html (100%) rename {templates => src/templates}/share_layout/nav.html (100%) rename {templates => src/templates}/share_layout/nav_node.html (100%) diff --git a/accounts/__init__.py b/src/accounts/__init__.py similarity index 100% rename from accounts/__init__.py rename to src/accounts/__init__.py diff --git a/accounts/admin.py b/src/accounts/admin.py similarity index 100% rename from accounts/admin.py rename to src/accounts/admin.py diff --git a/accounts/apps.py b/src/accounts/apps.py similarity index 100% rename from accounts/apps.py rename to src/accounts/apps.py diff --git a/accounts/forms.py b/src/accounts/forms.py similarity index 100% rename from accounts/forms.py rename to src/accounts/forms.py diff --git a/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py similarity index 100% rename from accounts/migrations/0001_initial.py rename to src/accounts/migrations/0001_initial.py diff --git a/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py similarity index 100% rename from accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py rename to src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py diff --git a/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py similarity index 100% rename from accounts/migrations/__init__.py rename to src/accounts/migrations/__init__.py diff --git a/accounts/models.py b/src/accounts/models.py similarity index 100% rename from accounts/models.py rename to src/accounts/models.py diff --git a/accounts/templatetags/__init__.py b/src/accounts/templatetags/__init__.py similarity index 100% rename from accounts/templatetags/__init__.py rename to src/accounts/templatetags/__init__.py diff --git a/accounts/tests.py b/src/accounts/tests.py similarity index 100% rename from accounts/tests.py rename to src/accounts/tests.py diff --git a/accounts/urls.py b/src/accounts/urls.py similarity index 100% rename from accounts/urls.py rename to src/accounts/urls.py diff --git a/accounts/user_login_backend.py b/src/accounts/user_login_backend.py similarity index 100% rename from accounts/user_login_backend.py rename to src/accounts/user_login_backend.py diff --git a/accounts/utils.py b/src/accounts/utils.py similarity index 100% rename from accounts/utils.py rename to src/accounts/utils.py diff --git a/accounts/views.py b/src/accounts/views.py similarity index 100% rename from accounts/views.py rename to src/accounts/views.py diff --git a/blog/__init__.py b/src/blog/__init__.py similarity index 100% rename from blog/__init__.py rename to src/blog/__init__.py diff --git a/blog/admin.py b/src/blog/admin.py similarity index 100% rename from blog/admin.py rename to src/blog/admin.py diff --git a/blog/apps.py b/src/blog/apps.py similarity index 100% rename from blog/apps.py rename to src/blog/apps.py diff --git a/blog/context_processors.py b/src/blog/context_processors.py similarity index 100% rename from blog/context_processors.py rename to src/blog/context_processors.py diff --git a/blog/documents.py b/src/blog/documents.py similarity index 100% rename from blog/documents.py rename to src/blog/documents.py diff --git a/blog/forms.py b/src/blog/forms.py similarity index 100% rename from blog/forms.py rename to src/blog/forms.py diff --git a/blog/management/__init__.py b/src/blog/management/__init__.py similarity index 100% rename from blog/management/__init__.py rename to src/blog/management/__init__.py diff --git a/blog/management/commands/__init__.py b/src/blog/management/commands/__init__.py similarity index 100% rename from blog/management/commands/__init__.py rename to src/blog/management/commands/__init__.py diff --git a/blog/management/commands/build_index.py b/src/blog/management/commands/build_index.py similarity index 100% rename from blog/management/commands/build_index.py rename to src/blog/management/commands/build_index.py diff --git a/blog/management/commands/build_search_words.py b/src/blog/management/commands/build_search_words.py similarity index 100% rename from blog/management/commands/build_search_words.py rename to src/blog/management/commands/build_search_words.py diff --git a/blog/management/commands/clear_cache.py b/src/blog/management/commands/clear_cache.py similarity index 100% rename from blog/management/commands/clear_cache.py rename to src/blog/management/commands/clear_cache.py diff --git a/blog/management/commands/create_testdata.py b/src/blog/management/commands/create_testdata.py similarity index 100% rename from blog/management/commands/create_testdata.py rename to src/blog/management/commands/create_testdata.py diff --git a/blog/management/commands/ping_baidu.py b/src/blog/management/commands/ping_baidu.py similarity index 100% rename from blog/management/commands/ping_baidu.py rename to src/blog/management/commands/ping_baidu.py diff --git a/blog/management/commands/sync_user_avatar.py b/src/blog/management/commands/sync_user_avatar.py similarity index 100% rename from blog/management/commands/sync_user_avatar.py rename to src/blog/management/commands/sync_user_avatar.py diff --git a/blog/middleware.py b/src/blog/middleware.py similarity index 100% rename from blog/middleware.py rename to src/blog/middleware.py diff --git a/blog/migrations/0001_initial.py b/src/blog/migrations/0001_initial.py similarity index 100% rename from blog/migrations/0001_initial.py rename to src/blog/migrations/0001_initial.py diff --git a/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/blog/migrations/0002_blogsettings_global_footer_and_more.py similarity index 100% rename from blog/migrations/0002_blogsettings_global_footer_and_more.py rename to src/blog/migrations/0002_blogsettings_global_footer_and_more.py diff --git a/blog/migrations/0003_blogsettings_comment_need_review.py b/src/blog/migrations/0003_blogsettings_comment_need_review.py similarity index 100% rename from blog/migrations/0003_blogsettings_comment_need_review.py rename to src/blog/migrations/0003_blogsettings_comment_need_review.py diff --git a/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py similarity index 100% rename from blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py rename to src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py diff --git a/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py similarity index 100% rename from blog/migrations/0005_alter_article_options_alter_category_options_and_more.py rename to src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py diff --git a/blog/migrations/0006_alter_blogsettings_options.py b/src/blog/migrations/0006_alter_blogsettings_options.py similarity index 100% rename from blog/migrations/0006_alter_blogsettings_options.py rename to src/blog/migrations/0006_alter_blogsettings_options.py diff --git a/blog/migrations/__init__.py b/src/blog/migrations/__init__.py similarity index 100% rename from blog/migrations/__init__.py rename to src/blog/migrations/__init__.py diff --git a/blog/models.py b/src/blog/models.py similarity index 100% rename from blog/models.py rename to src/blog/models.py diff --git a/blog/search_indexes.py b/src/blog/search_indexes.py similarity index 100% rename from blog/search_indexes.py rename to src/blog/search_indexes.py diff --git a/blog/templatetags/__init__.py b/src/blog/templatetags/__init__.py similarity index 100% rename from blog/templatetags/__init__.py rename to src/blog/templatetags/__init__.py diff --git a/blog/templatetags/blog_tags.py b/src/blog/templatetags/blog_tags.py similarity index 100% rename from blog/templatetags/blog_tags.py rename to src/blog/templatetags/blog_tags.py diff --git a/blog/tests.py b/src/blog/tests.py similarity index 100% rename from blog/tests.py rename to src/blog/tests.py diff --git a/blog/urls.py b/src/blog/urls.py similarity index 100% rename from blog/urls.py rename to src/blog/urls.py diff --git a/blog/views.py b/src/blog/views.py similarity index 100% rename from blog/views.py rename to src/blog/views.py diff --git a/comments/__init__.py b/src/comments/__init__.py similarity index 100% rename from comments/__init__.py rename to src/comments/__init__.py diff --git a/comments/admin.py b/src/comments/admin.py similarity index 100% rename from comments/admin.py rename to src/comments/admin.py diff --git a/comments/apps.py b/src/comments/apps.py similarity index 100% rename from comments/apps.py rename to src/comments/apps.py diff --git a/comments/forms.py b/src/comments/forms.py similarity index 100% rename from comments/forms.py rename to src/comments/forms.py diff --git a/comments/migrations/0001_initial.py b/src/comments/migrations/0001_initial.py similarity index 100% rename from comments/migrations/0001_initial.py rename to src/comments/migrations/0001_initial.py diff --git a/comments/migrations/0002_alter_comment_is_enable.py b/src/comments/migrations/0002_alter_comment_is_enable.py similarity index 100% rename from comments/migrations/0002_alter_comment_is_enable.py rename to src/comments/migrations/0002_alter_comment_is_enable.py diff --git a/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py similarity index 100% rename from comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py rename to src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py diff --git a/comments/migrations/__init__.py b/src/comments/migrations/__init__.py similarity index 100% rename from comments/migrations/__init__.py rename to src/comments/migrations/__init__.py diff --git a/comments/models.py b/src/comments/models.py similarity index 100% rename from comments/models.py rename to src/comments/models.py diff --git a/comments/templatetags/__init__.py b/src/comments/templatetags/__init__.py similarity index 100% rename from comments/templatetags/__init__.py rename to src/comments/templatetags/__init__.py diff --git a/comments/templatetags/comments_tags.py b/src/comments/templatetags/comments_tags.py similarity index 100% rename from comments/templatetags/comments_tags.py rename to src/comments/templatetags/comments_tags.py diff --git a/comments/tests.py b/src/comments/tests.py similarity index 100% rename from comments/tests.py rename to src/comments/tests.py diff --git a/comments/urls.py b/src/comments/urls.py similarity index 100% rename from comments/urls.py rename to src/comments/urls.py diff --git a/comments/utils.py b/src/comments/utils.py similarity index 100% rename from comments/utils.py rename to src/comments/utils.py diff --git a/comments/views.py b/src/comments/views.py similarity index 100% rename from comments/views.py rename to src/comments/views.py diff --git a/djangoblog/__init__.py b/src/djangoblog/__init__.py similarity index 100% rename from djangoblog/__init__.py rename to src/djangoblog/__init__.py diff --git a/djangoblog/admin_site.py b/src/djangoblog/admin_site.py similarity index 100% rename from djangoblog/admin_site.py rename to src/djangoblog/admin_site.py diff --git a/djangoblog/apps.py b/src/djangoblog/apps.py similarity index 100% rename from djangoblog/apps.py rename to src/djangoblog/apps.py diff --git a/djangoblog/blog_signals.py b/src/djangoblog/blog_signals.py similarity index 100% rename from djangoblog/blog_signals.py rename to src/djangoblog/blog_signals.py diff --git a/djangoblog/elasticsearch_backend.py b/src/djangoblog/elasticsearch_backend.py similarity index 100% rename from djangoblog/elasticsearch_backend.py rename to src/djangoblog/elasticsearch_backend.py diff --git a/djangoblog/feeds.py b/src/djangoblog/feeds.py similarity index 100% rename from djangoblog/feeds.py rename to src/djangoblog/feeds.py diff --git a/djangoblog/logentryadmin.py b/src/djangoblog/logentryadmin.py similarity index 100% rename from djangoblog/logentryadmin.py rename to src/djangoblog/logentryadmin.py diff --git a/djangoblog/plugin_manage/base_plugin.py b/src/djangoblog/plugin_manage/base_plugin.py similarity index 100% rename from djangoblog/plugin_manage/base_plugin.py rename to src/djangoblog/plugin_manage/base_plugin.py diff --git a/djangoblog/plugin_manage/hook_constants.py b/src/djangoblog/plugin_manage/hook_constants.py similarity index 100% rename from djangoblog/plugin_manage/hook_constants.py rename to src/djangoblog/plugin_manage/hook_constants.py diff --git a/djangoblog/plugin_manage/hooks.py b/src/djangoblog/plugin_manage/hooks.py similarity index 100% rename from djangoblog/plugin_manage/hooks.py rename to src/djangoblog/plugin_manage/hooks.py diff --git a/djangoblog/plugin_manage/loader.py b/src/djangoblog/plugin_manage/loader.py similarity index 100% rename from djangoblog/plugin_manage/loader.py rename to src/djangoblog/plugin_manage/loader.py diff --git a/djangoblog/settings.py b/src/djangoblog/settings.py similarity index 100% rename from djangoblog/settings.py rename to src/djangoblog/settings.py diff --git a/djangoblog/sitemap.py b/src/djangoblog/sitemap.py similarity index 100% rename from djangoblog/sitemap.py rename to src/djangoblog/sitemap.py diff --git a/djangoblog/spider_notify.py b/src/djangoblog/spider_notify.py similarity index 100% rename from djangoblog/spider_notify.py rename to src/djangoblog/spider_notify.py diff --git a/djangoblog/tests.py b/src/djangoblog/tests.py similarity index 100% rename from djangoblog/tests.py rename to src/djangoblog/tests.py diff --git a/djangoblog/urls.py b/src/djangoblog/urls.py similarity index 100% rename from djangoblog/urls.py rename to src/djangoblog/urls.py diff --git a/djangoblog/utils.py b/src/djangoblog/utils.py similarity index 100% rename from djangoblog/utils.py rename to src/djangoblog/utils.py diff --git a/djangoblog/whoosh_cn_backend.py b/src/djangoblog/whoosh_cn_backend.py similarity index 100% rename from djangoblog/whoosh_cn_backend.py rename to src/djangoblog/whoosh_cn_backend.py diff --git a/djangoblog/wsgi.py b/src/djangoblog/wsgi.py similarity index 100% rename from djangoblog/wsgi.py rename to src/djangoblog/wsgi.py diff --git a/oauth/__init__.py b/src/oauth/__init__.py similarity index 100% rename from oauth/__init__.py rename to src/oauth/__init__.py diff --git a/oauth/admin.py b/src/oauth/admin.py similarity index 100% rename from oauth/admin.py rename to src/oauth/admin.py diff --git a/oauth/apps.py b/src/oauth/apps.py similarity index 100% rename from oauth/apps.py rename to src/oauth/apps.py diff --git a/oauth/forms.py b/src/oauth/forms.py similarity index 100% rename from oauth/forms.py rename to src/oauth/forms.py diff --git a/oauth/migrations/0001_initial.py b/src/oauth/migrations/0001_initial.py similarity index 100% rename from oauth/migrations/0001_initial.py rename to src/oauth/migrations/0001_initial.py diff --git a/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/src/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py similarity index 100% rename from oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py rename to src/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py diff --git a/oauth/migrations/0003_alter_oauthuser_nickname.py b/src/oauth/migrations/0003_alter_oauthuser_nickname.py similarity index 100% rename from oauth/migrations/0003_alter_oauthuser_nickname.py rename to src/oauth/migrations/0003_alter_oauthuser_nickname.py diff --git a/oauth/migrations/__init__.py b/src/oauth/migrations/__init__.py similarity index 100% rename from oauth/migrations/__init__.py rename to src/oauth/migrations/__init__.py diff --git a/oauth/models.py b/src/oauth/models.py similarity index 100% rename from oauth/models.py rename to src/oauth/models.py diff --git a/oauth/oauthmanager.py b/src/oauth/oauthmanager.py similarity index 100% rename from oauth/oauthmanager.py rename to src/oauth/oauthmanager.py diff --git a/oauth/templatetags/__init__.py b/src/oauth/templatetags/__init__.py similarity index 100% rename from oauth/templatetags/__init__.py rename to src/oauth/templatetags/__init__.py diff --git a/oauth/templatetags/oauth_tags.py b/src/oauth/templatetags/oauth_tags.py similarity index 100% rename from oauth/templatetags/oauth_tags.py rename to src/oauth/templatetags/oauth_tags.py diff --git a/oauth/tests.py b/src/oauth/tests.py similarity index 100% rename from oauth/tests.py rename to src/oauth/tests.py diff --git a/oauth/urls.py b/src/oauth/urls.py similarity index 100% rename from oauth/urls.py rename to src/oauth/urls.py diff --git a/oauth/views.py b/src/oauth/views.py similarity index 100% rename from oauth/views.py rename to src/oauth/views.py diff --git a/owntracks/__init__.py b/src/owntracks/__init__.py similarity index 100% rename from owntracks/__init__.py rename to src/owntracks/__init__.py diff --git a/owntracks/admin.py b/src/owntracks/admin.py similarity index 100% rename from owntracks/admin.py rename to src/owntracks/admin.py diff --git a/owntracks/apps.py b/src/owntracks/apps.py similarity index 100% rename from owntracks/apps.py rename to src/owntracks/apps.py diff --git a/owntracks/migrations/0001_initial.py b/src/owntracks/migrations/0001_initial.py similarity index 100% rename from owntracks/migrations/0001_initial.py rename to src/owntracks/migrations/0001_initial.py diff --git a/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py similarity index 100% rename from owntracks/migrations/0002_alter_owntracklog_options_and_more.py rename to src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py diff --git a/owntracks/migrations/__init__.py b/src/owntracks/migrations/__init__.py similarity index 100% rename from owntracks/migrations/__init__.py rename to src/owntracks/migrations/__init__.py diff --git a/owntracks/models.py b/src/owntracks/models.py similarity index 100% rename from owntracks/models.py rename to src/owntracks/models.py diff --git a/owntracks/tests.py b/src/owntracks/tests.py similarity index 100% rename from owntracks/tests.py rename to src/owntracks/tests.py diff --git a/owntracks/urls.py b/src/owntracks/urls.py similarity index 100% rename from owntracks/urls.py rename to src/owntracks/urls.py diff --git a/owntracks/views.py b/src/owntracks/views.py similarity index 100% rename from owntracks/views.py rename to src/owntracks/views.py diff --git a/plugins/__init__.py b/src/plugins/__init__.py similarity index 100% rename from plugins/__init__.py rename to src/plugins/__init__.py diff --git a/plugins/article_copyright/__init__.py b/src/plugins/article_copyright/__init__.py similarity index 100% rename from plugins/article_copyright/__init__.py rename to src/plugins/article_copyright/__init__.py diff --git a/plugins/article_copyright/plugin.py b/src/plugins/article_copyright/plugin.py similarity index 100% rename from plugins/article_copyright/plugin.py rename to src/plugins/article_copyright/plugin.py diff --git a/plugins/external_links/__init__.py b/src/plugins/external_links/__init__.py similarity index 100% rename from plugins/external_links/__init__.py rename to src/plugins/external_links/__init__.py diff --git a/plugins/external_links/plugin.py b/src/plugins/external_links/plugin.py similarity index 100% rename from plugins/external_links/plugin.py rename to src/plugins/external_links/plugin.py diff --git a/plugins/reading_time/__init__.py b/src/plugins/reading_time/__init__.py similarity index 100% rename from plugins/reading_time/__init__.py rename to src/plugins/reading_time/__init__.py diff --git a/plugins/reading_time/plugin.py b/src/plugins/reading_time/plugin.py similarity index 100% rename from plugins/reading_time/plugin.py rename to src/plugins/reading_time/plugin.py diff --git a/plugins/seo_optimizer/__init__.py b/src/plugins/seo_optimizer/__init__.py similarity index 100% rename from plugins/seo_optimizer/__init__.py rename to src/plugins/seo_optimizer/__init__.py diff --git a/plugins/seo_optimizer/plugin.py b/src/plugins/seo_optimizer/plugin.py similarity index 100% rename from plugins/seo_optimizer/plugin.py rename to src/plugins/seo_optimizer/plugin.py diff --git a/plugins/view_count/__init__.py b/src/plugins/view_count/__init__.py similarity index 100% rename from plugins/view_count/__init__.py rename to src/plugins/view_count/__init__.py diff --git a/plugins/view_count/plugin.py b/src/plugins/view_count/plugin.py similarity index 100% rename from plugins/view_count/plugin.py rename to src/plugins/view_count/plugin.py diff --git a/servermanager/MemcacheStorage.py b/src/servermanager/MemcacheStorage.py similarity index 100% rename from servermanager/MemcacheStorage.py rename to src/servermanager/MemcacheStorage.py diff --git a/servermanager/__init__.py b/src/servermanager/__init__.py similarity index 100% rename from servermanager/__init__.py rename to src/servermanager/__init__.py diff --git a/servermanager/admin.py b/src/servermanager/admin.py similarity index 100% rename from servermanager/admin.py rename to src/servermanager/admin.py diff --git a/servermanager/api/__init__.py b/src/servermanager/api/__init__.py similarity index 100% rename from servermanager/api/__init__.py rename to src/servermanager/api/__init__.py diff --git a/servermanager/api/blogapi.py b/src/servermanager/api/blogapi.py similarity index 100% rename from servermanager/api/blogapi.py rename to src/servermanager/api/blogapi.py diff --git a/servermanager/api/commonapi.py b/src/servermanager/api/commonapi.py similarity index 100% rename from servermanager/api/commonapi.py rename to src/servermanager/api/commonapi.py diff --git a/servermanager/apps.py b/src/servermanager/apps.py similarity index 100% rename from servermanager/apps.py rename to src/servermanager/apps.py diff --git a/servermanager/migrations/0001_initial.py b/src/servermanager/migrations/0001_initial.py similarity index 100% rename from servermanager/migrations/0001_initial.py rename to src/servermanager/migrations/0001_initial.py diff --git a/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py similarity index 100% rename from servermanager/migrations/0002_alter_emailsendlog_options_and_more.py rename to src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py diff --git a/servermanager/migrations/__init__.py b/src/servermanager/migrations/__init__.py similarity index 100% rename from servermanager/migrations/__init__.py rename to src/servermanager/migrations/__init__.py diff --git a/servermanager/models.py b/src/servermanager/models.py similarity index 100% rename from servermanager/models.py rename to src/servermanager/models.py diff --git a/servermanager/robot.py b/src/servermanager/robot.py similarity index 100% rename from servermanager/robot.py rename to src/servermanager/robot.py diff --git a/servermanager/tests.py b/src/servermanager/tests.py similarity index 100% rename from servermanager/tests.py rename to src/servermanager/tests.py diff --git a/servermanager/urls.py b/src/servermanager/urls.py similarity index 100% rename from servermanager/urls.py rename to src/servermanager/urls.py diff --git a/servermanager/views.py b/src/servermanager/views.py similarity index 100% rename from servermanager/views.py rename to src/servermanager/views.py diff --git a/templates/account/forget_password.html b/src/templates/account/forget_password.html similarity index 100% rename from templates/account/forget_password.html rename to src/templates/account/forget_password.html diff --git a/templates/account/login.html b/src/templates/account/login.html similarity index 100% rename from templates/account/login.html rename to src/templates/account/login.html diff --git a/templates/account/registration_form.html b/src/templates/account/registration_form.html similarity index 100% rename from templates/account/registration_form.html rename to src/templates/account/registration_form.html diff --git a/templates/account/result.html b/src/templates/account/result.html similarity index 100% rename from templates/account/result.html rename to src/templates/account/result.html diff --git a/templates/blog/article_archives.html b/src/templates/blog/article_archives.html similarity index 100% rename from templates/blog/article_archives.html rename to src/templates/blog/article_archives.html diff --git a/templates/blog/article_detail.html b/src/templates/blog/article_detail.html similarity index 100% rename from templates/blog/article_detail.html rename to src/templates/blog/article_detail.html diff --git a/templates/blog/article_index.html b/src/templates/blog/article_index.html similarity index 100% rename from templates/blog/article_index.html rename to src/templates/blog/article_index.html diff --git a/templates/blog/error_page.html b/src/templates/blog/error_page.html similarity index 100% rename from templates/blog/error_page.html rename to src/templates/blog/error_page.html diff --git a/templates/blog/links_list.html b/src/templates/blog/links_list.html similarity index 100% rename from templates/blog/links_list.html rename to src/templates/blog/links_list.html diff --git a/templates/blog/tags/article_info.html b/src/templates/blog/tags/article_info.html similarity index 100% rename from templates/blog/tags/article_info.html rename to src/templates/blog/tags/article_info.html diff --git a/templates/blog/tags/article_meta_info.html b/src/templates/blog/tags/article_meta_info.html similarity index 100% rename from templates/blog/tags/article_meta_info.html rename to src/templates/blog/tags/article_meta_info.html diff --git a/templates/blog/tags/article_pagination.html b/src/templates/blog/tags/article_pagination.html similarity index 100% rename from templates/blog/tags/article_pagination.html rename to src/templates/blog/tags/article_pagination.html diff --git a/templates/blog/tags/article_tag_list.html b/src/templates/blog/tags/article_tag_list.html similarity index 100% rename from templates/blog/tags/article_tag_list.html rename to src/templates/blog/tags/article_tag_list.html diff --git a/templates/blog/tags/breadcrumb.html b/src/templates/blog/tags/breadcrumb.html similarity index 100% rename from templates/blog/tags/breadcrumb.html rename to src/templates/blog/tags/breadcrumb.html diff --git a/templates/blog/tags/sidebar.html b/src/templates/blog/tags/sidebar.html similarity index 100% rename from templates/blog/tags/sidebar.html rename to src/templates/blog/tags/sidebar.html diff --git a/templates/comments/tags/comment_item.html b/src/templates/comments/tags/comment_item.html similarity index 100% rename from templates/comments/tags/comment_item.html rename to src/templates/comments/tags/comment_item.html diff --git a/templates/comments/tags/comment_item_tree.html b/src/templates/comments/tags/comment_item_tree.html similarity index 100% rename from templates/comments/tags/comment_item_tree.html rename to src/templates/comments/tags/comment_item_tree.html diff --git a/templates/comments/tags/comment_list.html b/src/templates/comments/tags/comment_list.html similarity index 100% rename from templates/comments/tags/comment_list.html rename to src/templates/comments/tags/comment_list.html diff --git a/templates/comments/tags/post_comment.html b/src/templates/comments/tags/post_comment.html similarity index 100% rename from templates/comments/tags/post_comment.html rename to src/templates/comments/tags/post_comment.html diff --git a/templates/oauth/bindsuccess.html b/src/templates/oauth/bindsuccess.html similarity index 100% rename from templates/oauth/bindsuccess.html rename to src/templates/oauth/bindsuccess.html diff --git a/templates/oauth/oauth_applications.html b/src/templates/oauth/oauth_applications.html similarity index 100% rename from templates/oauth/oauth_applications.html rename to src/templates/oauth/oauth_applications.html diff --git a/templates/oauth/require_email.html b/src/templates/oauth/require_email.html similarity index 100% rename from templates/oauth/require_email.html rename to src/templates/oauth/require_email.html diff --git a/templates/owntracks/show_log_dates.html b/src/templates/owntracks/show_log_dates.html similarity index 100% rename from templates/owntracks/show_log_dates.html rename to src/templates/owntracks/show_log_dates.html diff --git a/templates/owntracks/show_maps.html b/src/templates/owntracks/show_maps.html similarity index 100% rename from templates/owntracks/show_maps.html rename to src/templates/owntracks/show_maps.html diff --git a/templates/search/indexes/blog/article_text.txt b/src/templates/search/indexes/blog/article_text.txt similarity index 100% rename from templates/search/indexes/blog/article_text.txt rename to src/templates/search/indexes/blog/article_text.txt diff --git a/templates/search/search.html b/src/templates/search/search.html similarity index 100% rename from templates/search/search.html rename to src/templates/search/search.html diff --git a/templates/share_layout/adsense.html b/src/templates/share_layout/adsense.html similarity index 100% rename from templates/share_layout/adsense.html rename to src/templates/share_layout/adsense.html diff --git a/templates/share_layout/base.html b/src/templates/share_layout/base.html similarity index 100% rename from templates/share_layout/base.html rename to src/templates/share_layout/base.html diff --git a/templates/share_layout/base_account.html b/src/templates/share_layout/base_account.html similarity index 100% rename from templates/share_layout/base_account.html rename to src/templates/share_layout/base_account.html diff --git a/templates/share_layout/footer.html b/src/templates/share_layout/footer.html similarity index 100% rename from templates/share_layout/footer.html rename to src/templates/share_layout/footer.html diff --git a/templates/share_layout/nav.html b/src/templates/share_layout/nav.html similarity index 100% rename from templates/share_layout/nav.html rename to src/templates/share_layout/nav.html diff --git a/templates/share_layout/nav_node.html b/src/templates/share_layout/nav_node.html similarity index 100% rename from templates/share_layout/nav_node.html rename to src/templates/share_layout/nav_node.html -- 2.34.1 From bbd90ca3bf550a3f6964ce477abfe1709e2be4e5 Mon Sep 17 00:00:00 2001 From: ZXY <2183054016@qq.com> Date: Sat, 27 Sep 2025 18:04:30 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=9A=E5=88=9B=E5=BB=BA=E9=A1=B9=E7=9B=AE=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=92=8C=E5=9F=BA=E7=A1=80=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/README.md | 1 + src/main.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 doc/README.md create mode 100644 src/main.py diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..2570dd4 --- /dev/null +++ b/doc/README.md @@ -0,0 +1 @@ +# 项目文档 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..2d4cbea --- /dev/null +++ b/src/main.py @@ -0,0 +1 @@ +# 源代码目录 -- 2.34.1 From 2f06db7236e0beb05429eee6c08bf5b674bb3ee8 Mon Sep 17 00:00:00 2001 From: pgxy4rpc3 <1662607537@qq.com> Date: Thu, 9 Oct 2025 18:46:55 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=B3=9B=E8=AF=BB?= =?UTF-8?q?=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ngoBlog开源代码的泛读报告 (1).docx | Bin 0 -> 354461 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/DjangoBlog开源代码的泛读报告 (1).docx diff --git a/doc/DjangoBlog开源代码的泛读报告 (1).docx b/doc/DjangoBlog开源代码的泛读报告 (1).docx new file mode 100644 index 0000000000000000000000000000000000000000..e27812a75f0cf9f1a86099ded5055b0e07aa7190 GIT binary patch literal 354461 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

    ce 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

    0TzfEFXEplc7X4JXXQsIbsSiET31Q>`VL19{UHk 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

    l5#E@_dgBp)2I%N($mG5!8?} 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