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/6] 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/6] 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/6] 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/6] 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/6] =?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 8fb3ecd77e126859a06b0775196ee79d939753eb Mon Sep 17 00:00:00 2001 From: p8b4i7hte <2518549229@qq.com> Date: Thu, 9 Oct 2025 22:11:33 +0800 Subject: [PATCH 6/6] ADD file via upload --- DjangoBlog开源代码的泛读报告 (1).docx | Bin 0 -> 354496 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 DjangoBlog开源代码的泛读报告 (1).docx diff --git a/DjangoBlog开源代码的泛读报告 (1).docx b/DjangoBlog开源代码的泛读报告 (1).docx new file mode 100644 index 0000000000000000000000000000000000000000..ea1a6f725adf79c706c7129c562663d7bd47e7fd GIT binary patch literal 354496 zcmb@tV~{9K(>Zrw75<8${AED=D<+Kxw!r}i0DuYv z0D$n{#S9%B=-h2>vJ_?Q`x#I=2DnEKBXs0~}r$59-F=RYQ^Stpi`A$)$A z3I|=cZ+JDEXtU{OcCc{?-e46li-?1W<|b2O`%KPOYm6wPaMHU_Y8Ma+qdQm-;^Ds* zv!i+L_(uf>yJD7iwKzp*mFFeep;+LijX!2R?p?*Ms;Cwim@b-wuN#Yvf()z$(d9*7 z$s*{Yo{<0O&&`+Kcwcw5t|DmM{^kipPIC0_dfk33LZ(RvJ@tI(OzBeLb0$O*q8tJC zM<1SOw%|fgj=6!~p;{Bd>l7i?2FKX}{9ZqWZ)ph8l!Wj3)ORpwxA*R9QVPk0B$XHb z031qPZvLIXcS-u3Vf*;Ja28Onh+#jdF>V&d>LaZHc}c;6JqtryV`3^;gT@f3=MG-?eOP|I73rHUCZ!lpA0`5Pgx@B)sRV z+bX5ikfZ`UkCc)C30T#9Zu}jdHOeOI1xe@M3IHy=kH%q%6=~4->$7La+lwsSRu$8x zcQt>WG9OV8&_Z!cs7A>-b!LDWMPsDt(6oq9l)Kg5?)|rA6XuS(;*p-Ns-Bv%my{3QXswuubeNN2{M_*47Vb1y052SncLfVkB*g=6m zxWFbS*@yfp zhIYuW`~-Z-F~HeL16B4aq}UgV2+RJYLIy}?8AaQi1gU)Oh%dTo<3HD1#;U)ZXFhar zZ7;>I3};IXi5viX0R#wU2qjgny&9yG9+NU8O=mFGhzk46DuPy%N46b^(!cFp1o zDCWf{)@$)=yTDI#(=4gQrFsTJh3=lWs~{xK)V^-zpq-b3mb|e8@#5m$vKf`8i=1Ps z`1wyF@ZMd3%}TTmKfy08JV`X-z~zl3dfQ)~M*0KT9Rd}0j{N2=HlYg(1B_dC3}eTY ze?Gd+zwU1<-3tAJvA@AqekwOnxmN4ibrjzKp#-C4uwmZ=VG#O9zvwM9UsxI(p8EJ) z8O6p}0$iD#ambVmdzo^Ry?DxFaEqPX4w|1snLeRBi-YeqNwUnc*@ z#@43f$0-2=0HEOs0094AB2La8Hl|MhSb0fz;@8F~>Q)KWkN-ZE<|@)t%J^fFtYjT^ zy)<(I^MnerMI;8W1ql4yAO6viL-6{slS%u-XJCUw5UoMbYMpHjGKtKE*4Tv>Y=M`I zobI9cm(d0ulra+)-!A@p_x&gW89cHA`j?fOX)!&$S`veJiC2@y!G}3{`=8Ge_KB!Y z_el3^K!F7P!EcjBipWPJd-@l29V0%{}hu;M2d3k~zs{9v}LF zyz#;IEN-qcc$bpSYnBQd?Q*}D#6Ae+2(50M+W}iC5{;O+I0AG+tdAaYYE&a-#g+o- z$B1BFYoN2e$25_vbI+V`VOvbO`y0t6chI03uvOGazftm}t!_{eICwn};Bup887FZNd#@oYqaR%Qf z4Fmu{QsH6fv>l7iU{`8(Gx6aQo}eGxEsS2>&sWvVK2^+csO6vdTZQuMQ)-`Vq z_R2>?6v$we!ncuVU`A4qknL-)&fxU}ob5c0oF6Zbl9)<+zN?#XTP_q+= zTpxJN3It{!iEm95S$3T;=#u>!8Ae8RL6E-4iSoL*W)E4##`+axC}ucE3MyMnDtV&nTToP+MBpJY%|qs)E}C0{aeN3m=z^kw&Z5~?-5A@~ z%}X5W5F(Fq3z0*+f%?lt<jkyL> z2(Jj|XNcSg)bh7uW=yR~jVL@~%><+q-2O#~yJzWLuMl{~)*dMynAn_2@V>*Gwiv43 zXJJQGsEecFp-$BR42hiCj!hi2^P|Jqb_1N|v^+L1p1>-4(wNo<1yT?Nyjte~xue z|46fbSLixY{xBaDKBaX@b&DeX9;yp`j$TRiW)-=+=h#3Qb&z!=Uh1yYK2}edMC)V^ zS&jqwcv!0BtkT|n2$_=Qa^lbt+*E#G6M57GWybjWik&V=GyTf6)&*Jrrm&?V1}gFs zu*YbOgL#CT{&GX|%hLZ&!}|-#dKLDm^!|LVmW-_5Z0baj2@eV7pTXB5qzWbS%_M71XjXXc7Se?E2SDA)FGSTAaV z+jjr=QGYmlN=1XaaRgTsdrIklYY<1YN3f@W5N~VHrLrylvHIIRN(#>!PrEIVf4%>^ z6M9pzvyO%i-1cwB-vzS$o!4*>dtynmvrZ)I@8paBtr0J>HHa-8h-gofDvfO=+w|Yo zrs#Gj#%Hqd|8A^-)&Cb`iGn~sZ&=?MfMJtlsVtTu-1 zV9ca!i^un+Jkmp{ZOqsFL_pI3jSin7WQi75zfK5iN)*)`ZLr5%@%f0xCogcjHDk1) zG-|G>tN|@ zfLqQi;-BM`23>t|OsU)b)8J{EvMpLf*)VBye>=VIr)aLa>_};nOaW}3qUg_RUS_;H z$z?Z1!|cV<+AulIoD8F6o)X!1lb{KH3`@PC`Hr!yNnSP0R4}xcRpeEc`s<(00~+LO z>*?}81y%$WY9&AijZ2!vUY~PWMMl?QPANV9%+F!TL3cm^)atPd_{YVXGwf|KR99Ziwdriu zv4GTUf{M%c23IXkLkU-M^3>p5H_(B2j)Q$b0>hbB8RdrUeN8+#8 zTi8CWO1X0nLN9_-@)z&CCwP~cy6u27nr+rvu*`9*em6{(To-*X^W{?B#$cB_`doSl z*HCI_fgfB2Tn|eHovci-pl%TNa%HizDPd6g zO)d3Z7Y}tnq}Wu`7Y7&NIq^d;e&;Pai2G-GI2|E)V){@3tQ)@$XI(q6~tCc z?^J*Pl-V5qo@(_h(XdROs>6#=OnNU=vhv6ieM&~3gvc-YtnhkN6;yE1 z;4eQ~3I4f!|9t(FD#4p3MIbF-8Q=NF|4(=+)SV%|_7_?vgZ)pO|1U0aGIe&gv@`z~ zc4Vn;*ljSt_~6(2L40PfPTPhkmRMvsU(DJK)%*I>P&Xs!!brxX`hHPJM?x1;M;?(P z!FxzVir*hC;ZKW!=d&ae#m-A2{SNP~FU%h=IN5PKh_WbzMItp)isRuz!qu(f^J~9k zl8pvr=LLewV~%+!Wh)-r*Esm&P)J@+2-KUUf`hqQPIWACwP6-bY9E&eYPyJXV3|Ne zEKmGUs!H^LVk}pepmiM$J9cO%&UoiXDC9#@6Qv`)`HM&4K?hsmC;;e{S}wQ>N#W@Z^WA7lxZXX z&|Hf_M^}BwGKD^YY+bHcwcIIm%D2)9tbB%O?BFaCMVo+x^U%Bl*!~ZxF}0)(ELIR5 zcnB`3uEf-_#Dq`-YD5PDAm`|aWO+OfHge;M4Xa-*9s=TPfB0gBo3A4U8U=91oz&<* z#5EHja@FRPZ}yHqozU2NliO>XhIQE%0a9BRn=v|GNsA!6BYxVhX|UpkE?pbaXiMw~ zCNAjD%sd@qyfy9TkG$Dl-h9-u*Pw3;)lzw=7r#Rz5#5r}K;U9DZv1UFi2M)gfLX+& zwwXLA^CPMXL%_|kZZRAD=}g}W?i$F4sjCanBRYZz{ZO)pTau0 z1MUr0Z0leZfkZx`fbKqdO21J;k2@xNH^i?=Y6%oWoXCtmJDVDaB{HmK1+VCzUZ2B$ zhaxgHHOZ5Ch{yu7qkTqFO%aP#tQWFP_Era%H0+=(EgCQ21!WDr!}d38-abQL?56A< zp$FNb*);Tl!3RBLz&DH5(t>R;t?r&(*S|x!{Z_04rwl7>Q|cb_Sm8*J;Zmo zVqW&f3eO={Zu5sb1tNxK+>+jy~M@|{&WHn9PhB9dLZ%#5=_V?=Cei>yM^ zCaI9l;hG!3uz>tn3E<v~h49@N_E>bGXi5oM%a(jV`~^c$(R5M&i%B@p5v z^PW9(?e)r|>;T1Rj$L{+8x`pHFaRHX?pC&x6E=_R=UF;tkS6?ebcy4ncPi*dKWZoT~%|t1J}PkfqMT)S$FO2!%`Tr zq|@ZO2z`9XcmiTNHEToe0KAIP2F)lcz>2z9q8ewHnl_;weCRv^<_5q}{~=(N9>e%h z=+VWA1sT!;+$L##9He{uWhi+eG>^`rYJ2mAB1}Jd)a843b%8!JdpB@00`JG+`*`}g z_o5cpV2w_0=&@2!!~ga*7IxZVuHWVPsmZFBa>wuYGAGaPs|6eUoSyM;|4`Dye^=C~}B!0z$|mGaq4Le}`8Rm*ZAJ_ijQgYP+~1 z_`-M`%-LvJ@msMjmav|gDxYw7*6QV!DDHH5=Qd#&9a1|`1JprSFsET>ObOJ8Dp44R z*^&RwAXPcjF9u{^%#}naCS0UpMS21ywW~JT-W{cRdjo^Y@y44v%}b(?{6wmBcE%Ia zL%1AgVw@7yRY2K%re|Kx0Hh&{oC>N}JU4C;zc8L%6U+7+rTxMWXSP;h;gLd*fKzg1 z#rTPfVD2aGv^qwzP%HmM-0B$1d-K653Wk;?2ggBJmACr|a#YuzvZOB95_Q#?%WQ&k z)7Het%_E{*dp_=)UCVm;Ttc+i(w4)|l+3Ol9!~m;lcQlwt!5YVL8drC{bTwy3pVf-tjA|jrLK)Or3qaH5Hdj() zDBd5`rkmK;ZsF8LVB%WQKBq{rGgAlf=MfT%{8h6rr^v-r>z?} zN0yA5t)nPo5a1*e@y}k2;x37WooZ+}bH1xav*;tr`D*ja^F}|R%?%`FmDP13y4hVl ziA9}(Cz?Q(|RTvlFH$5so%d~zGx7}sOBjBMAud3h}Nc7OD-je4c>A7Ku&Fb%NWyUmn z940s|J0Gzdftjx`N)uaL5sWHKOS2S*MxcREfOz6i^A2EIBwB^qr zyMNT8ox;4{Drp!Lau;1W<`KcsB#ABFKx5#*xCu1%e(1Wxh=SG>PyGbqod4L)Xkr#u z?ID%HS%iaxJV=NJ#`)xuD*Kc`RG6EmqK+{M7UxSHk1zh=B?=3MWd*OkXxI`)Olmhw zj0S_bHu-AXX@J8D@f@Xm$rvyhTC?`r&B^w*VGdHJgU<*ci}f=!V#J%2SMAubnVH$H zCw}kqp0qmDt&_p5OpOUwi-7yJhf0?DP}&sTw0md8%nn!Iddf(H2CN8m6BqXlKFv?% zZRU413w|Dl570vNdM5-OR3A{@Z2}Ho2suv|W8hF9)tuHyaiPw}byj*5_5>>59AhGa-?7Xddf%Q3GU0B z#zb0W1HAqY{GU;uv{Jr{00#iTMGDZrr#t^feSgo}Tx?D4od0!TKH5$jYW3G2Wn4c2 zMI}CX!^60_R+qXU5N7q%=~>96%qO;y`4;KOkt*>?s3(&3RTVBy-YupRzr5!L5lKg4 zsyh58$pr2#zx&UcumB_@PypBGRtJ*HH2_*`Yw5b$d#;x%K4O2~z7)8e3DYM^>eHEd zKPjuplO{>(fy*X$)3fb+BEAGKAdjH$CIxu1rbuQUb!3jcz8@}szV-cZu5d@m#6lC& zg&@jD552_0kj*`((V+&qO5{(FPZEXiKFjh{h*GD_XHexx>xD}iPNs{H#uf_JHhV1t z4mdqoGbhLr-gvxc!p-RhA9&?lgvG5V;K-S8tDM$06*}Yilc!*_@gh&Uu`$1a7x{4L z3*VK`RQ8A#rzq`M*O+;U+hl4Ma{1k1iE18=f-?E9vD=8dEbeu^na`sEzV^>_m|$&3 z&F)W>2b%1hwXmJyH8x!W^hED?iAA^wL-+pt(h=@U*{FD zMgk`7ZPnmcPD}cSu{J%!_RFGI{vtvxb`>7C&0MY|K`f<@Lhr(>zZw=e>$pxFs4d(! z5AaTzh=K8%!j$XMx6Lu{emEzh<`$UXIFeYo51`_xfBu*r2}{&-3Vj%s=#ImU9ENtR z*`Ax5Q!W^IQ;vupkT_Svr_T<}v7&zp4n^w9fj6sfbN9n~XB(H!Q`Cct2c369b659s z-6eYChKViO-gBq>e(O!gTZ(JNAQ!ca->do6D5Ae-#D$4<)rBCRa|Zd6Ne2Iu?SN0> z4@lv0{Th#6BKXn?dsW>niASG?e^ZVb_2nFK|SaMN|71(48V+cH8mKP z3-0$pQ!#?PW9Vb2$;`Z)TzHjFd!36}&=t}!*E7N#q{*9w+xR?unN>7@Eq^3_aO9>e6fCspVt@CaH#)^V(FbIm}K0Jc}!FVu1M5X`jpA`M=E~%(}4#t3wN6I zVQe$RWJdE_rF1Xxv15Am_w%xt)@)1+3m>uUkOyUEQ?b3aGIySqyl_#K%c znP>5%PGK6Eu7!ywqxE?eET2iLUBdF3Rfs z8`eWeI#%7ri}9F$d50OYnG%`J@?PW#O_9nw7oD`Vp}7-55kW=qbO=dH=JHa1;+faHi7 zs1De&ud0by77Z5Cr>Jrk#E*vqMl3|)Q?3L+R1N^dOnVK>nWA8L*)pA!ST^P z5@AJKSlSg?6k~M}!F~%Ox<&YAV-;3Y4Y?Nx?SbqsU<;0woilmO&|>-nk!4JaXm@E2Bp?KgdzYu$GH-Dm=Rn_-yw7bo62K}l@^jx9Ng^av%pRG-!)67=4*}kW2+ZVQFi5t0AP$Nr= zre0`5>UL2h(WucPN^fj$m6 z-Y>_dP8LXxZl%q}XZw9&Ys>CWZ{%K2j~g_{3+)`rF72ifb}eO!0ANn3{NG@%qT%U* zU~o*2?E84bI69*W4AIjJT&!d)3!Nx|xKcDuOX@L%Pvw z?Wt-e!mJ{Bz!xX%j?MPq({sN=hv`%wNi?-q^gqlm#~ADS zby!dF^&1T~>UnGEtvw3A2OCk0 zk)P048k<{2lQd%vLp~LsZf@SwcCO|dTcCk57g4>9yQeO+%FTRlQpuLrdV$A@O(9qB zCD9AoqPI`Ux%6k;)br31@*1 zvy#oI0%%8?D^QFW7}%S{&1x$+(7Y1>&qx=SUh!y@V3bqlfRvACw&sKdm=l|Ixtfjy z!GkKMVzTB0KFz(x_nXuKlHbF);aGj&rN%4%#jAxOWRJ#_Cp~psj}!CDb3~8!38L%X zv*`Yvd?es#8+AhT6pw+vi+Y{C^x>@$cv4#XrE+cQe3cHsH45l+_f{tbMsfkhjNkm( zsXp(Z+8Atx@fa;gXUaAo3LOqQus1Ut^Uw;ujRmzcJT@w9)qNVCM)J}ja3Z{e<`xPc z)(J=?4%+4(#2hVj67z38L;Y{ddK%H8-r(SDs;ALSN6ow#Iuf<>FIwyUWFH^A{Q{)0 z6!X+u)bJ`OLvh~CYo1JZRm`5f)vXnCuHKVQ!g8Ki--k)>>B*BxZ#{hE)V+t}R~#dE zYN}hsK81NKrf#(5zH!Q`l`~%;fHt!2aTiGmO%m+No$0`Pwn>c zo{$xp(L{vjtKhS%A%bi&3$Jgo_m*8t1@9O%r;8CzXVU-*T9Qy`*n6cfkLr3xy>+c# zx2r1stK^HSv$|yYD3Z~@+`u44ZotSlDvfZ_1V(?%sq{G>w@fDr6ETJUPWv3R*qz@3 zOljWMY(s-)>JhRMM~FeMz_trl2$^z3SInK;UGBO_Lur)beT2&`y}!E0O=pTUGDYi$ zM&KfZ*w`e|omC%i63@oy1x}tHH^%MNFSXq+4rtbye*nDN5=*E-=1?a-h7j^P5mN4j z7>z)mvvwGQqn-|%Ts#oH7b{XDkM^ZSo{rYo*;;Gd@K!`;O(NJ$j>~+**Vh!4T$7S; zNlBaii^U~X1ta|FVVIkk)zg}*8HnVC^gdeL8Be#nW_1<&>;G64;9({zash=g2) zTSJ~Wm2w9x&CS+`@zBrE_x8e}imDOmWQA%*$1h?{Nq{b3i0hBJb$y|C&9EeEziX8g`e&v9repxs&N6=$91LrUmUK+ZL1QwAf zS11)5HOrM_ZJw1eVbZwNTwhDyww;}>$p%ktkRRvtPuGkF>TbCm*vMd0mvN7!P-JfB zLdspf2J}3j#$FDO)53uqqVUEiY2w@u7UpsV9VC>!ekeP|z_Gaq*{RLGbXS*2 z&C7&Q7QDeD%B32^cZHfBT6*2<2=(#n>x>F822R$lMZY}wIfT%O_rmiUm`lR8y3>5J z)>hJex{7?Vq9{8lKg3V*4x~-O{zlzE4-V!r#hia;wU<~W&@mot6-{5eqV78AtUA%2 zyDCl`ae>?9tldRykQP6B9Y3f^z9YjrK~(p`(ezq5GIVF5qmYXMXH5(+P*!%t4AY;2$p7S%u)i zp0-#9&doP6CQcgIQPs=yegNqjang_BTYm@vP!5Ng4YpNgd|Fq#4~H!=%zGdF_w zDzI*qObk+)h{g=L|4cLBQR7e+&QB)eZpvc^DZ5Z|9UP`nobfdDF=KmcV|Nxt;m~9C z>0(E_?|a{C54&L`yu|U0+(q_XSQ%8+cDiM8RQLE;$#xEHN-ZC|AC*9Q!`U$EkoH+wMVb`Tb?-GuzQMY#OGkRw02{5 znw94kx{c{czI@9Drb0>;4EuAIlMdKqms)Yb!mIl`633~`2%(EtKjIc)fIoD2kYL!= zKz#?+M4TC>2pM9a64-rKH)$`)O9VC>ah9|sPSj@=dzXu~GprNoxq#Q&eoppxJLX?6 zuJ4OT#Nqpc?)6<~9!P)nEwbO()2h5q2^MyyGdatl8mZtC)aMMgfGK0n(8VjGZq!Lv z@m+-iQ|!hlvqJc0pkW3mfGJ}Sy?{zvInr+v!qfu9qI{vXTPJbQxt`u^p;Eh4RvA$SQ(W8pb1Lu=YGbsFAPj=1TWq6%Hzg1XFGi7=J&B(tE4omJ*CZK3&M+z=8z4d*I9@ z*b&dn>czja5TiX$gxkTk=ilI1ORE-*eS*cf>;SEy6D{(aHyd*@l%gnvz|70V2+#X( zi0@YNI>qV#l%2Qrv6Q`5Ek~O_1LHoIv~r_9V#Xy^Ml#%AUM+pfG0ea_@CdWOEOP_A zfX%2IhqCis3={1_kD=U`4W%^vLMbdpy(LW@SU8@U3krTM))|L0c}&N2I4yJQLgucA z_EvkUE#py4ZtaPb`>uGXm;Sy+wM}h1!vP8~>IH#^W6bbLj3X>+VYjmm_LIJhMziZI zJCh})s60o4hzbWbj!9vjn;CHboST{GA*znD;|c}V*p6;7W~35p5qKku`S0xIrB0!k zPd?*)!iqbC;Fx&k?wz3{pwqJ?x)8pNC_Y*-`;H@4a=dz#!Uj09rv?}`)plug6WP*?H7TF)Fi|VUj(Ui!fCot*w)I+|F_YxtW|2@g)o4+3Hcms9=fQrNFDGM>E=89a`*etBF`p?#ce zH$Gh|y`}0)X)_Q02Hoe<(;}`umWLx)C~NwH(pmp$V+ZcV(;ty*dj zW{&jHJ?Q*9GcQtUzrw25J_<62dHCP7k@g=6GpI2f-#h>(M9C5`X5rgeS_Nx>G`-Qs zLnDS!6T9gWrr9KtLW=N1WygEom-mc^F+vN_O!vuSnHM)4l6QNon5acm{)LBuH+~F1 z*U=>3&|R1lr^gKP*R#3p03B}>GEhFA&5ZH5HVD|l`w;t6jzsFMj(4E?gWCj%1F0zQ ztF8G|L!L1SvhJ`ELrr8-ij-D}hRTTg!^t5B`}@r{lY9fCHI^!@5)N^x`&&7Fom&LF zD41=B8v(Qgx)3E`1j#lL1s=9!xj2%Fjacou=U1ZX@U-1zO6Q%Nf7T$e#DI!#q_kCDyU$_CIBodAX+h)rXOpe5oIeXI zrf8nvaKG!2E9pWO?uk6$@mCJK0wvnb6#B0*p?w}opwP9)6^0>!l9CJG+^6MoBK@(2 zCES~@Zymev&U9WkI*8mSBFq?Vu3N~LC&OLu9MT41!e4;DgV=hLhP)2Y zyGOaJ9HRsltYC9f9i%rd5cu*^-o09%Ujf>x>%dz>QH3>7_Y}hp>*=IISJQ+-$GHE% z?779e32=^#cD$t6+<6lvTTv~C6GWTb)B5qKk@ZhRc;^*0e&QY;_T&?rWaR?TXVTj0 z+6qL$RolnXa@n`WA{O5=r@sxf_&hNbL(&0``k|TF9<iFJsIJqoGsZut{?#F3%^= zFV8G*g5db^p24}w89gxrw~t7&e+tjQoXX`6W;)aJP2LvFyUp9aFaq1XXjOvwo#W3? zGnEWtT}DqO$318E+A1i!ZfHcWdqlS`aFZ zZ}C_Uw2`<&>H;M5I?I4oMQhxwoT6Phq#u1WmKgFKlWmujPagw2&}x@Lf=!~)LPkWp4 zkxxWkfw%3?J5YOm<>VU34$#V?L9@HvhaGW=u(oW57A0?r-xyTJBe|J}q~eX#S}+9k zR_YkmFWe8Hc}5o%;*8VyDnW~8uG{>|2edsDyr{PLTSHLQgO@CnG+(x(Hq9^u{#uwc zkTb9s*xU_xvwC}iEf1$3%HhT!1Q2T`O_PF;u%;}A?1AJi&{fGjZ;0rv^-f3w>_wuh zFX~MLOp9KQUsXBSn@Yu)Ejc1VK<)ms$5@QNSb43KrgUBDN*Ta1v0AV-de!QK&aAU>}2i9`?r2cnzcXGpEe z_*krfQY#sB)1xog67gm)-6X+pJW+Nd7Mjv_WQ3(V4 zofN58g!(`)a81P=8xC@~kzOYl9Ur3I@_{oRMQ_m{$V`9y&{?A*@5NUs7EoTSqwO6A zUy#z)S1JOIfR)aIUI&Lu9}y>W4)Kqc0Zu&gkXw$n7tGnvwzt%FNAFTSkfvi1PC_yk zGB+N`%7SttdW!d()1oaLA>lOmYurh3VFXw+M-jrY+$NVbj|@;e zD|AjshDAwyL^z&!^2mo^&d zngj{|)>%n~Ua#k>vim6Uw5HKm3I#%hDk_#&X!ehL3wtvY2zlm`d33mux1b$9a>n~p zPGj=Xv=5*&$1TRwe$9{N;--$Et6(tU{-XEN>!S>(CGgm*GwtO`@3$eaVZSiq+U1T3 z0`YR=7pj}$$|dyWL*3D16(4W#d`Y2SWFP1J45?I*Q1TBPeGl{B-0r-OraUjWA{3Ls z<6bRP;476K)((I-+KI?&ju{}@osG7#SL;#I2#NFA8D-xZjm0+$WOnUUnGOFU@gg^P zf>2XS>k=k@Wj(JGdZ4h}TPv004Y18mf8l6e7i_~Q!D4q_WuuWcEeuUqpIVf7y~y4g z>mA0jGO2Fai7gbwy@jI;I!`p5XSeBe|#$L_k^V)565E>PaE`^P{f#2~_)3CgQQ`SaJx&=KoD&grwH~Rum1%e|N z);8>OOBh0Xk1NSl6kR`{+v`_MVRCJ4<@SxLFMpJLyP@kdMMAf`j#OBEpoR7|H)03` zvA9Wo)tWF+A~BOl@<=iXH;2fE2$niZj>Fz@(s5vLfzZkIoP#jjbHut!@G=`e*Au%y zTq%-(bHH;FOQ1lB49|HgW@(WKnVQexE0&e#3o*?%-?m};p;&(+(%o>TmA68PA>;Sz z?y-#GBk~Og2U87zlbD0ID!c#wtm~c%dnNuplwDEG4g>v!ZNS)T)1dK8NLWXlvNPwE z(9z@PSf>QPp6?f5OJkOzJ0I3IF)pg|u)b?<`k8O%yRoNOgfNF-?CNtoa1(QbvvLW` z|3*`TrzxAi7Woo3VP4*+sWy-&C58yDcYQ@)1i2@2?tw!Jf^DdO=-=URz!}LX_qhKY zBeYqM7p571i!cH@%>r(r__#A!Cp5anKyqN}wI2og`t4sTWdy>6*aQqTL zXtLQ^g<+Btk{J_gc)O7aS{3?O`oqd8?hbzrvIytT-wCEpoUS!nvT2FZ_ z0NeEbpkr#hDa*}h+tY3tt7sck{;gqt^u%m^M>|VDjW=swN{46!ah56bxZhO%Az9ht z`PHlEer}Ja%53(Fh(mKZ%aAvIj%H^I)CWfl8L^5zCU1cdhi~4AD|dF}UD2tMK_rJ1 z0PTWOkQW<1mjn)aIg~%yd}q~|fx3HMm7*PwZ2Zmn3-fRW>CC5zPJXlJn+2dYv-HIO&0TeWJ5^n!lsp@DR^E;7XRYUCGQbE>e zd?dv_b+Os?i>oP&935kC2S^m@XxgGI{Hirp{9I^fh;4`ma!yEy%-Sr8%or+#SPx44 zK_=D;GTp6d1A4n8!%K7mD>lYo31u4JYnmDCy>U-ysEDbA0OlkE`w-SvPPZbQYNxSg zq7Eccm~)9b6U$#=%ZVQLtvnOteBf}`--oLjx13r&kT*hG*=m8rXTQ^$;MLAZFUELl zr3^`rbQ6-pV2-nb?h|nD7H=;OU{_)12?A%D5q7c3aTc5kggqeVeztMTSZZ zD@)akbUO}`MT*9&%%?rF5?~oEmoeD{QJq4e0T_%3xjMf?CBVXwmj`#XmvT9&o0{NK zUoMM|?k`4KRrignDkANG0CNtt!a&u#6f12ejg_;GXT7(w6|r0(1bw+muo)Y=-rVtO)7NgeR2)zux>C z5TQHoU=akYOAN)Y^dq_LuliO9KCi$liniE~bS2bzu6$!OzAE1+J86GB4P<5K0UwEP zIg}Sw2saVu)vcod=x(0Ue{hM0pW3wfQUJ)u=58eqyp3kyo{FI2PA$?VVYcM!BVR6$ zQ2VpZR%Q=%w|mrl9am`Gq4xj&jTmaUYS!$^jU{5Zy-(mQ%+ypKePWzGq~)9;T5F5YRa0pOaI&%CrNeLZR_|a1 z=BXmU;3(95%fM(zQWO(d6KEJr3S4{xA_iAn8;aY^vWF{L&Z*iAHDc4&PVic!%`9)^ zhH+5={m@oc@=~)aU2vTXtl(ZdIkp;MF`RmLA8m=E2f_>5QaJl8lSu51X^Dj^npmUw zuYTLNd(Wrl4)``cWgo|q_gAf=*J{s>@Em|d3saYB8Gb3XzQz0|kEuBax9T9v0Eu9I zwLQqs$V-R_AOd$&zaFmU{S0D3B*{@q!kki6t34}^8~i13H^F2Qtc_`;S^tKi*g2kW z0Kz5-gnMtclW3YpWRFl$eK<@XABMOe)E$$MF*{%bn$1e!ee@(hksN1x$}I|ZS+cb$TwEbCIK{@gw=ALINv zPH5y5qmOL=##M5%81feeWJ(DL0cJjO6dqUS>bpwPoRQV* z;B-41ZEkjeq=3-}!c5V=B$y?eTU}cl!GakCLfZcK%*nFbkujL19J^bdA*aN~P5r(i z^Q7a@ppB>jSV_h`pV3N>!rtB&$8$>wz5?Ol8525r{y!lkwx@VW{F(b1$8Ev-5Bvh} zG6|qm^1poAH5Dzkr{s37X3(JGGO7pYPbA)F<@7@*OUeQcbGC1udYqrRWS;W=ujM5{ z1uC}HSSMQiNT<@0-`oo4cs%Fbci+2p?)NS~l(puXYu1Q4#(#`G z_rX(%Lh4EOqGcgURaJt_ypF>O9pwjaMQJ2B!ucFGuE9X#@ZfIE|D2?veJ6fwVzM<8xTRm9oUte%7;UaxW zefzNvFL`Z^;HA-GmSP>m%D(#pJURY+VHNicsq<_UWL{lSmBS+Sx>kOU*~%pvg%bI_ zw+#~VLiQsVF*!LZfqmzraDyYSeX|AGi|M%;!S=j`=`K>rD5-Cd(z99@`VFQGUdAFD z@HD5_6x{TPNmRL~t5;4jZZX*Q{K~Ol2A!5!vU=uDgH^%v#bLfGqp~c9mNHdtYpI!B z3QlZE#FKbaBu;k`q zJR;$Hp*CNoGGK7^x+0c{>KCn3CWb`IW1Ark8ME2u{CvN(sA`sLp9+%7zhx)DCS zW->C4w-U?|WlAR_7aoRD)5agjc-3LE6QfyGKu^XX`$Dx@-6O1{juYx9U#9dtK4B+? zmd`m(ACs>v2$D|pUIVVd0)?X6!SiKk=T|o5YtEVX@}_V?W%54;M_1ndkZ7gHj9O9% zqE$CMEOK`%`X8ITH5&-UQ}rDkQ2o4dwqwunAR}g{lIl zEXX&j(UBUS%B~)xT|{L}(1j{1&R0>ZVv|xXQ2Voz&zHz@3hrUdK!Xr`o#_!{G3E!!U59CB7yK0k zWaru*SZNUJ%HQf=w5Ua!!pyJ=^*?r2kh^sQ>FNQ>hJxu0iFa2ppj2!of$7ah9Bsp_ zp;s_IWYR$yh#n@`N3>J;yKWbA5%Y4YLlT`(*f8+}?m1dks9$)0S3H{h05T+nVmRkn zdStvn%bI7%<~KDMpua=oK?bI zdTwJZIR$ehk}c4=IDlMpgFt0Zo7IlFxmeE!&+NLQ0)d!l8iyUx{p~54hmRTXJihSC-3qETrQl+pS+6%;X(V?l3h^e1Yll!$<$N?06xI2h0e0+MP58=xK*Znc1 zf&^ohts-dd^9?fROD5Uq!DyA|Ek;BTBu`%0%ML|b%zM#`9S53~Vaxg4$9)CHB6(sE zaBQym(eujX@%nTTRg`pwz~d|XM7+?&OQy{cTf?co=f?R^K?)HR+EhVGM&f ze4Z)ciiE-}awM*|c4qRkL^ATG&krX05a&Jo4*EZCA{JB|vOH$DV#t1sbd9y=mdL%Y zJ>%l(gQ`&9I?)U}Vot#o(Z+0dl^fQt(};W4nIGfI1W?yjPw=S^;49(_Tc~ny*_d!p ze(Tm>lbv)dYDgZ? zeRAC}CUAm1e|q&R_u!;ZU~#SRm^oA-)JZ~v=Uyu(s5f=hAZ%Y6hjpgUYE0p}i~=F# zarX1Z<~NklPiU{@$uF&%J-BBkQ1xlX3bCNwlI4C;XciWu=WWVlYVW|$BrTTcjlwNj zR9~qa+hFstgSw#a&(|K6r$Acf1+HAsyoML2g!fiPeRliYb;X`S<<)&5JFe@QI8NTf z)Q67K_ok}Pop5E7M=3lIT&+^NmPRq#YQnnLg%=c;xVL<3SXF^VLg|xtp3skg4K%WD2l2s>evmmGZOosyq#@0D4q#Z^h#&o;s`#inQppU zgLJUuArg@46}Uu_Yx%rEI5!Bobe)w3E#i5vGuAM-c7(`)THAxiNP%8%C9DUp+&&?X zo4rVG-oKnbk8FTv@^N>g3C*vab-RNhZ(U7>T?F$)&g=8%en`HfOGCn4jrs$+L4Z-Yf zoxBxgY3E4PDFVauXBVov4*kGE(2qwW8i(G#t1qoXFnw&{FaEq7o zbLHHIT1ODq)JJM0}m9G}sV&2SJ)4$+GJLb|Yp3lXbY z97&X}P{R~Rh>*<TqHB+ zMJfvxicjV@5W_;W=Y8420-uGcLC@K@1gSL*B4nX@*!W$%sY!pqo3YLi+e%1_g9)tnu{Cq#nO!4VYa>yY2C+K=FXp_Se8dJ zdlOYQb*05KjwjyO2W|OD9e&9Ddz7mI9G8Yj+h4t`t1r{PdsJ2a*7Od7K{*OCw~ZS_wi`r}UTy~3pz=EmbuPw8TAY0GJ+5}g{cuM}>AKpd zLZJdXp^CYRJc3QDmdc7rjfoF0)9HQ7u9q7;$M#zy+e9=|tOdxeNbJP%)VZ8qOiuWEC#;Yg61qZ(M9 z+~_P#qOJY3Iov|m;`y1Tm8=@-pHq&o&CgY%K8c$tsZx_(B@{(6a6j0O8IyaKTU`YH z-`zXv4X-3`-cZEp-S>!NjQ{A}5pRTKc8L&%R>QJxP&vH*Op8VGE9)G~>)B_)muG}S zQQmB5MQl6-DJxMQ4-G9|7V&?Qzpl-V#U@}q)QQov7tlzL?uyvP$S5f!agO}i&iVIy zEbaF;?&6bu=}YLKEV4CkpMMZc)0T8=6zc=G%bT~tlDn&Y^>~Dy;%f*AUs%l{lDjxH z5g54?%QG2kuNCh!Sh2kxSym>gv0O+_yCe6OHaBb0pH^g+-Qq%F1djd=y)%(o2wmwk zH%o(MJ5gFm#*Jo4hn=B?#f#LXjo5AHZrUi9ZOCMOoj8$Srw1uA zD36~n7}1O(AxR=RjK1j@t0w{H;DT)%D#yV=;%YX%yo8`)gD~SVpn^!SeoHw zNy#d+#FP}P9I!&_DE7%}!<=dCGq&_F^>$XD+m-Mgi|c_LS?$s>2%vpO(NTXW&H$mKErrLUT?Y0)5f^hP@ip8;p$LeZr<4J zfpn*LVEL&0^6*;t(U?G;xSIik)`~p03Yq2LXm$8Q*qay}3XQw9=7g5aqKZug+~xA5 z!z;R;J}(% zLq7G~Z#bM|r#|>d3i1g0%conHK47d+d79g{G}JCChSyiv2-oS}t+pjxkFBu0RmJ>< z5o9Y26@Pfu%)8u0_&n*s*U{Xgsi*bZdG$_D=L+>U8G1+GyUEgO_rWx3y_^3hZqiBQK`2AaxK5q?@j4PZVcOJ}S;o!ILCuV#{Z@H7W3#*SfP4 zQab&xp~&=e_sR0|`DW+kTe11Bme-4|1=5FdzP>8GeP>0`JKDv1AMu&|V|m+slwQh9 zEUO=i=Nw|$?Iq_|kIbLvc-3nAcz4YAkOWd~9&$EYCpsI=YM`%-eCB*cbeJ5$i?fzM zV>e!bP4fk@E5rMfxwWN#K7-}PswPz;f5s5|T8zK-Yr6q_hC1SxrCtZTc5_4v|W$8-|SLn_6g%8s0*3zfo~xXZJOR_EUGD@klmIDP#hl;dnl zO3b12zKo8ib+j5IjO^XHs8Zo;@=~3{LR@Sfc{M_)F{rElo367Twl|8zRtV4pF*yM>y=IGwEQU32EWkB`xPX3|fYtCmdjw=R(FNX*BvZlguqc8I*j%j17z zbARMIs%E{r$|%UsQ;gFJyCl3Cyh>GC$%9RaiW$)U@ch#kshPF{*2I!1()bl{>oy2Sr3hKWtA+g+in z*k-Hw$EZW;VlUezv;`VaidvUEiV-PgC?d9>wY@A33~WLw3TM+jsLc>OU}y7Fy%w!f z>a*!;ed1LQrP+QPyWkio#cA@Y*iZ*cn#dG z*UY>f594{72g-utIE;yJ1#TdOrt>5&Ins?d2!6Q;op18*n3mJ-ij3xYs`TMP4}GY6 z`Q+W&{=#>Rmi!NPZshaedtYcfVp=dx4nz5<6+N{%yJbbTRVME3|9r~h!ng@}($fa- z+iK=@x195Dnc{L(^)e?Y!(utP&PzTm;*hL)BU^a?lKj9Ml=JOQa!zD7CD>Z%LTPuE znIlaL5a-FhTv)t?-Lcbmzm3eBEx@F6p+QB9T5_r8k&GWClG<4!q#9*q;eBhcoC;HN z0R}nm{d~Eb1iI`K606d;BPK^TBxX=r#|0u%TP_Nq7ivj4w0JOIzF)Nw^C}3&M6!Lr zOoF>}$V)9chXuKqJH^E3cL%ylXR1odsXO1CygsL~PV=%)vN>P5O@fDvLeltMwq*<6 zN9>*fwbG~hOd<2*>-;w^mZZ{lZN9ZQtZ6QZ9f9e%tS~!XCZYg?A+P|B%CpeHNQNwTz7+F`k=ioS7qW~ zKR-O?naIWKjDtZAY%X8{=XvpGhLEGJkXf>7scM^u2cdMfV z{mkgB!$gO?D#t3@^e^&#Br7#ul6LS?l)buQdw&zAW@m$qU@-mU`u4$c#PwsoU8ald z@5W|j%+smH11;O5sYoQl3QGv5wF<4rEYW%MOSUaE$7W!dv-krp7ZtP1S&rJ}qubri}NVhv2m1vCOjxyHa=R_^S_YFLeTZCr4Ojbl*6K`a zVp1$=_ma-byzw^V<3nBF(d6TA$3y;lsI9x$ZuWa1Ap_vV$X_JH!qLG+!`Q^` z+j*B==%*SZu!L~$$FlHXM~?0xu(OKOUrhlDqpHv;+> z?q?y5Q=<;=TorGy*gFq0*MF(65fPjpPfqro%5+zZwNT4o5@_oLry80vFkJDv6H89l zIlzl=s&+$MohSS9(-dJ}x6!ThS8P_r8{YaPXItsM)12IO70FCmkknB{#66!Qkn=G+ zaWColZs`(Ld>suhX>rt$vuN8MG25%>2DzPSw*-BtH%YS7D!w+P(QCv=DC=a#4<#Dp z@~S;#p?A*ZO-hI_q+nDSkHbiND3LUK!$xN?2ueKZI`c&B5yh=L9r80@>Q0>CJh2(lkr$TvB)#_ZN=%mQe@z>0A!3cg$2x2el zbp2(+$NZ>Db!+x66xK1+WaL&4*Jv1let6CWT=7t= zMN$@fA*3r4Jzr%j!qU-7X%G;Q$j)&}>=D*Fe@MApaH)@%x1u&P^@a0&#+_j5DbG>~wt*99(LomiNXd>BS~?^^s6*h}-Yf+)rp(h8QDR*gJl3`O+wgfBxN|<;k~WhyVDP)L)Kau{SrfHvZ`t7Gm^^Mg^e=2@0T8YJXb)8I23AU zu4Qf_Z|wqqpPL`T%?FB^RW^K(){$|lJH-!9Pnz=g7o5?^b)+^TeLR&UhRgfx~7}8yS51w{N;0T)8Xc^__>3# zZtOi|oQ$DXijHRHcA!NwkMB(g@(6x!)6DaG0WaTafu++~H>dM}7pPl%olWlBRJb@S zU9HW`oxi_(TG7SP(ayyh`rATNM+XPM5WlHt?BoQe`?o2dzH8xVcRIP#0w~~|-_(E? zsQ+$};iX#EW-eCW>k4r3f1eh-To$+uR%bnPp3TqR_^k5jOq{KZ0ULgQ|95oyzKmy! z>E!4L+WUcwKR!OIWbYwqZUG@ROE7At$(Zh8mj!%T&V=3NL_*tc$V984UzZ?|)f) z?$ZJlM`vr7?~-F@ZUI(N!pgMqIRk?zZHRf+}2#u(asURjX5|vz~6Lo zbTI}3|4kjh`2^}{?cnlF40t8zc_qM_N^$^t@B&*v65Im`J-Y`*@aj%3l8z3}E>6ba zJh4ct7Z0{m~ZGn`tmKakMH+#YIY z>;gO&xZe4XnE!pnocqiUIOP%kQ#*37Y`*9lpZsWgf7dY+v2NwAqYy4nc|7--9 zIJ$r@#{v7Txr3QF+!X`6up`tQ7!nd@#?Dsea7PSo=!3_nejnWYGj(-x`}PRF={q^P zI+*=(_q}s-v)_4af4eGnG6#%r?FRP?Krg41`J-yZjAZ0@518eVwz_IyKY) z*Z%cu^?ee5v|{il@P+t!`2W*|0rkHR^sgBdHvmR}+w_Yg_m|%}t^Lh=`tNG~&dEAO zL;frxX6|9_q78(A1H!}mO*X;34qQ$l92{`zgg=*pb2D5zzumwIb9SQwVoAXD{f0(; zD{}yD@M&`b{O~v61@QJbAY43W_rJCWe=7A$gYdhbTl_Cnz;`AF?ZSoklwca?ZaL zIN=1w3fx}#k#W8i{NPUgZSoIX&2Lb+>={tZ3ExQpRQ7LznsS|)bU%>sOey?G#xr;6 zUnBp&2fzOVfSuY`-x>VxXkKw~{1#XLBMv#^h95cPY}@$PaL6AZ>;IPp{g(jNsX6#Z zMEvuK`R{RVCr5x=d}r|=RQ7)YA-~`vf7!+T)sz3ld;S&vI1|{vz#?Zr>Yqo>|KU*g zzt@J&1I&#d5t)AvIsZM_4YhN%v_8!q`j4Oki}mlgq4WF|g8t{*(Ens;_rEC=_jhEY zKRfKk&GB2vdT%m0IT+g|~o|84&Ezc=2-eFmfb;BEh6s{h8w zI;;3E@uto&fgfQLX+C;!fm4`GP?}yq;y-xXKQ&M3k67nVhqs-@YJX&$|I2Rp@3R!S z&uI382KduC;&77v1e@B!Q^`TrKD_up2KwLEKvtMSKr}{~Xx<#oC8gI1Rdgd-o3m?%&%1 zkA80E`-1!~MdrIHdWzqk`tQGqnV*W^|4+s{m%v|)nRD^})_CXmhsVsh1b?#L&w`9+ z7T2G|dOw5k{;`QYXQ@wTSl}5R@YlxlzmM{4tiR?7z>`ye6mb3YkqbcX@3QefYyVs3 zz@IX42;|TFykmYZ_+hX7r)oo>jeb#v|9b`Yt5OBY{XZlOeNPmG=Lf+F{9CR1%iUk8 z4gY`AasZC~=~F3Oyr;_iOs)L=aR@FR-k&=?vnbCx{XeD4e%3W-_y03p^Iz(+pEHMl z(QSXH_J7V1{a?2KC)Sm}5A*#wbmhNSmgWCXdXxJvXH@d={nk4C31|KLt5j~D-x`O! zXTZjvOric0Qz_5#xc+g_&c8Rb%X7BP`~d9y8KK?Z$6o0HvXME(E|wP^JF^ z?fz4t-9KWTGnnwlpum|3o++V!Y7Q>f?*p=mp6aUL9FCtL!5`e5f7K`ae@KQ zhC&dd$dE)Xh(RGAX$0aCBoUzAAB=yRz4sTh`*Sr}1U)`aaSm*)ebBS$d8}&Fkg(!6#g%&fGP+ri z*iGc!J{ig`mZCZuqq|=-UDn2RAoW{n2^_|-ai?y!xq0TQ{q5y=W=XUVTIr)k%tC`& zH-^Tx)uY`>{*L9*au!&((w%80yceOMsrVbXg>umh?M=@S7?qN&=-yY@kC`-1R^6tB zmF5bn5~IJDrec8&q|Vg1E}Ok8G`KEnP~*ZWoGaq9-#K3Gtca_GQt9!TFtpz~`AXrfBF=QSjDnaC`B`pSn+6v3*w!tp)1cN#Dk; zm5~ATe*AGlEq&@w9TD-gGU0UCCfiFxvwNQ+cIlgXn!mzg6{x2$5RFi>BREz^N@eC= zKVbxm8qfVkNPVs+fmS|_m2+B6Rrqt$IVZ4yVGuF_m|mGx-JOxN zuMeLNoO3~bm!(KLQtR#*5=N3}6of-Agc$c21Gb66k|5DUfYgAocCg+U?>^xvgNjRG z7nt2a?w+(5k~8SGHhx9R$frEtuwGY5cw3|Njq5uDM%&?hPDu?JDCOO~RH4ltZZ5m` z9AEdBi^4FJvW=fy35 z{X47W8-wb~T_ct$_Fj(rJJ^IH^zr&84(Izc`vT4rC018?G{+9#bN2FPOoGD51fb8P zn0s?=O9HSenedbJU>QMvzR2DL`reV6Hp~zM4V6B@6Uu9D`B)@ebQQMKc;#gj;~(57 z*-T$Ph~%H}kX#xru)V~QtC2$^9}nSe85omv*~{H@W~LG4L!L*|+_>+@kL*o2?mp?m z7$Lm#Scmwn;qY=%BjoiH{)pG*v2<1axmkJ7qZt&rc3<01Hhh?IUoNOjO#WzJ`N1MQ zHSFT(xk18VFzE!+dq?l_k|;I02#_Z5K5Ig+;%2uFDn8-sGCJAU;+7gx3`lPsu3f8< z2Ry5lkCvVH{BAj+A~A6wOO)aVD))l=IY zWg;jD$>)~TFU-D*-OLm~?WmG!31Aecg5 zo92cv6@ou8qNQ2Dl+{H%Kd}XLs;ajEF_=J=;UFT*=z;nYKywA~m1$rh;fX=Z5Yfe` z%8>E&tt92!sSrRF(tlBv&j!%+>kp$MX=MUoWy7Xz!T71qtqnrMN* zI`ZLavrbEfsY^-yl?_?`9oVT`rZAd0BIbeZl~u`Kxd7St72HK|}@ zRkuweQ5)~Lc6ef;+5FPUGj$Xif$ZDEIqDreZsZMCl3|Xw!jn+3i1A2m1hCb1udS91 zm_!XmUq{Jdj65#19>|(I0m^(gzXNP>wz*wi&@W%yObC5%J3}LZ^Qd(4j_RzE5Yx@B!!PvtaYQ7EjV|>w;@qiv_57(R9!#Er2tq)jPS$nXMa*$~8 zf~U$VyiihvJWH&rMy;NHjmp)`d+7xo$Wn|x&tCU-YenO}lCpApCbo7_8FQDOLv=fk z?mTAE!^%6mYneFL`cuQF`ctBL?!pBod#<_+%9S=wdqO}icT77mu zFxrn*@J@$LCcRutVp3XHKA9*n4&q^86NYbUFX)bHDUP7U=^v*%He)6rLXgh%x}V zFR)j&QRve!kuO;(ug0h=aXK@OIy=v4kYRYC5H*`$=Zd9BigupHtRWJv>dzpVT$W#H zZh!Y!u&QU8(j~VEc^*r>e0g1Af63{t2`#%dj*|HTmX7P1Vo^_s^X-}lEmBzb12YC^ z=pvSCIU}*?dZHZ; z?Q>SHnVNSF8xLpM!&t8_ywG5e3X!z397?9pxA=m4Rr49_e9gup8CXThj6sQi0d6LlJLRY`*{%BgLU3A0JF1 zP1`LS>7k>fypWCix$C ztRxTJhjj`%&vll+GbB}6th>g_=;Ra!{ZjaloP`nI+!YL^_ML_Fh{ScjrFWTGN>49; zuf+x1Eac5hA%MxJBrsl?o@AH~R-DZ;sa>rYo6&0Ak7ew@xJrN=GzGJ}2bN-=xvDh? zxBGSnxuuuAF~O62gw51det_|`k%!u_GAGdV4 z1TF+&A&&e4AORJe1YzX#w{g~9u@qs=s>Bi`P}_)Q6FWo2VeHm~ymo1lptsJ=2Ne2Z zThTr)fd^AanAZ`4qz^~@!vP5d7x6TG1x(t4O&UMR1^U6F+TrAAx(;hN;N?Q|)s@g4YFyU_{dapos*~^#6(lq6wP?-)LH7->7zu*_#)3 z&K_KhrOY~UMje2>+`MUVIL$s+EFQIP;+ zsP=O}6L1gA!H)NBhu>gSN6(NzXwH-f@d14#0q@R<%kJW3Bs^KTMG?s}P9g6o{GcPXRp!R03z$dIuF)9vVRd8&56oGe{K6ZU*S!A{;GOV8wJ^m!>RCysrXdZ*ADhZdG5Ljq8X#A$ENP&T8 z1S)@!ITF%$5E!YxNTxiC#P=Fwpdn zJ&CEq%{TF1K6S{4U1Ia@N>QMGgo%h8k4e8H_r$L-&CugxK!1sOciBJ`0+J!V7}PM5 zjuKt0=_)W$YaXdkflC%}$r3fAejillbDYqJRL@-aaZ(-O)d_8GGgYJ^yAYO}w1>j|y0>s*noDK8c=3Yc z1=7sEe|OB4bOh)6%gOO46L;2QF$tMFfv+L_<_Q+{BWAG9%sCf!B%xi+R=7&-MPb-> zc<7-3LXe|qrbpXrI)~eNa=d=hf6sZOL3S-1Jm^pcZo+@%FT|5geToo;+}T-)1m`58fqqJGY{3 zCV*Ni9d@Z}YjME*V@m)8Bbp2;0Z<^JR1(^8YZoZz&X*5Kb);Rtt%F}V?BMud{q-2r z)>9Kc$DZa9GV)iKT!Aw%etdM03HIrY#q5+YS$a| zUPe$FnV@7VB+wk~Z*xmh5$e~vt(vvm$7tX2JvrKiGje~qtrqgUoU@!TLA?i?Ud02= z!NJJlY+T@ZkK>9SQUQb`cd>Y}lJj6|F}e(GH^TR@6-#=o!k)#k@u=ePo$pb6zEPtg z96s;_NXSf*b1(jD`4nsKy^le&xW0S$bP}#DW<=v9;bOPsvA)aKp>-d>9Rv`E(LB~J zG$d<+&x%mp3$amm`5cDxbu6k91f+&y`Bn>#mW@s>96{O|d=3;G6@oGnEdbu+bhQ1h zd$iov99X+qj9G8^lD#*&veK|r2g2QlFVt=JUSrtt>NB|~rUE?uq=|)*5_5UMlfzAO z;3dqBwCxF2p;3CPtkrEQZ5m(jDKGCGT4ZsX@LFaYxHCZUtaK<}N8O>}tHmbg$I0WJ z%DE>`z&yDn2U!ZMTCl7m$ONNeOwUl908VG9sL4>+Ti~u+XCb=`psfPvYL+JN`8$2* zhj7%&2`tC{h4WkiVl)62nP-J84AlW;41QxDPifq8T^bC*D7bsH?=+;T)s-aZEEO;a zoTBO}*!c%knh#iR=5w?@eIt6JZ!?rj4-gGP@`>E3(K2gOrYVlsD z!P}EtLl`>cH=rOR~U^HBrdDSzu#UZ8OB&_2v!#*WLBWMkxz`dhoXD z9=(cwM+!R#d2VRu@+I5yO>#R2e9BJ198Vm&&4+Fl=C=+z*lj$q4@?G-+7&)D{yRH% z)}v)PU}tF^;NruL{E^6;dnh688 zQck|6IL*dtB_@}boB(|8b-V2R2Y{0$A*s6g`?>R z4jBejUcEomel9ix$7jpY;P#N=lL?`GmV(jf+U;SzhkFjSt9;fIb=3)5!^STrkn?v< z7xj}VNntEEwqNs4^0cbdFEBYje2AmyzB)SFebJwilm5+TJRl9D=hgAaZ5N0mRJC-s z&E8h?tfD+1Vk)09NYGkj=~zhqmJ{-mrB z&yVSfGgvBh^;WU4Rc(;JPdpvJ6s_wl#&A1YJ9oRwm(x-LBqyqoWb zP0Z1Rtg9RYUTQ;f8nw=D_d@n+5nz182g_1?l6V3%kf^mIbwfFIRSE=)Ij1e`O)-S- z^10c3#?)jEuiH2376gbA0P@gx?^@to6ovzU&*!G6JLdk0e zMe3b$=p3tO~E--%3?ma{+KOi1qZ^1Nbzd;UZ@j_T7RqdTj-BqH8b z%_~?&pV;-k5YYubZ`U)G6BZB_Ekh%Gqo&Td?0IE(k0L4DZJJ#y5z#JGkh2 zZJq)quMSmGHXg6K%>_avkK3l_J=bd2MXbRlWcPfAtc7G+;DZO_`~BTGHa!KR!}{F` z?v25hxAU=^2?>*02aV%%r{>>rm_>ni zg0rG5@>ObQWrUW!?lUZb;~}nu2`=Odo)?n!cOd11$7*%X%#Fl(xtmhRGPn>|?5~yC zDKO_OJhjDULE+DB)xbH;nsRZslo(BTFg$T{S~Dv{P$1U$5INVagJ6DIO~+UQDvc8V ziYy7T*Y?Jh3vJ%eoBnmPb*)>LX%zTN8nNzrZFBHTI+-Po1Mr!%&bw_MSX# za>s>WXD;hE-8!1`7n7Wae*eVlyNkL^&Klr_=JDf62?}x~ck! zUFi!E+fk)6&7%I(=$oI2Pn;R~sz)VdKWPZe71D1{X~+UIlJ!DR+nCh!r%XG|`yrdd zi}`6ZRXk}ncW}FQHZuLM77%pidDp)}YmSWsju&xnTlHH7!H)TKPk~BP`iU%c-Q()Z zDPj%;SFkPkd?ThxkN1=xBqsCeNp4;tR$2k?^f{A(3K@fioUS__|Sg# zV8y%1oedNQfw9S`Da~`Q=~C<(+NB(4-#7T&VQ9y4y{tE3BH-E-P#c~riN3r^hs01p_)pd zh$hlP1RbJyOJsFz2YDk`yCSAFZ{H&}0=(LrC!3Lg3zH{!tXx|w>|e(m$2R*pSHy7k zQ`~vjCi4aBxqDOn$a4d04$4$Tp}^3p%Z94r7018X+8K(c9(jKBgjc<$gfI`@#L=OT!Q6;@yon6GuB-K{wowU1eCtuTWp1fWS0`? zF_lCO3nra3s~2UWz&^|h!gTseBylB-jE_VaA88@dh#a?}HG2+84D>J>Lig?8rOQNI zu8w8V>Scu}BydS%Vq*4GnrsvJ)Cd}>_xpC7=r=NpI!*{#k5?6cb)N8?CuG5fH>Hwj z%zoQ_`jfIZUXm6Y&uecDC>jpgqsF6E$GO&bjL9Bpb`y0YGaJB6ksZp_R9|bZKiV1F z0J@JHV7$uVuQ6fx-FpXx(>RGcL8W z!`>SZ;x*OXV6T}@{66-mWyXoEh~op`E%K!~-`@=qJ#6lIZC+Dt&y+y%ZpVcW+IJ;T z9#Y-5wXiKYW+#ddC8b0rQn6)Ty>3LVd$oU{(a7T?iWa@=n2J96_;eenKXCw*=z=k$ zHKW6)ds9ePzuq87de`nWrcwuqVEa1KWWGFH;M6NPPbGkC*=_x?fegJ@Sd!i8g*a3f zSMSwF6m|AZXmm8)(E4qz7Cz;z|0 z0qCt_vk_LAtL8XfM&D&Z3X15D`vNVSUDN9AGyex zp_Vu&4^7IVjoa{5)ryqm2CKlpk>cil2PyU@tB;&@EVCw^sOZV#+I@rY{#Vw^>mNXD z6jAz8GYYaun52f!$wblJZI-_5OOV3 z+L(Vx?#!g1l5vN2)3XRynRQPKz8V`aVTs45z3!1{DVMN_C)fC;=@(#foQU;gQ}l#( zC{HV1T}d!=@GwN#Ny_2Vr`o9-5AEXI-fG>O1ObqZFr&kkIufnP1{HIlmwyoqJ1#!g=P@a0XD&E>! z=z}9ZT{+pwm)An)#8zqOs{pu^z@Oc4xY4~)J9(s3w#M2D`GD z4!{}T03P~iKJ!xc*J=LMzQyq_Kg4q-D8g0<&qI;s(#qP=T=lbB7La%GxLvA?HE$J&>X(tHzEf;or?1-Tk9aYnUNTCL?brk zk4C}4A6YsAJdjL$0-u!lyV#2}(aK7+gtZa~Na6rWB#C-QD#)nt_?DLougzd52T{~4 zvpK&d9VO)X+wZTrui1II>}MeB31BSD zpWH0aZMT;a#4`lX9#QygsnS*fcR#dYaN|CK(&s&NGqCU(H^As`Fh4wM*k58v5cO@? zT7Jj=P-DgBnt$cwT|FhyBNxF>54i2vnP(4tfnRlAQJk`2ZL+aZA3$b00+)k6hlv5r zH=CPR$W`xLF;3+-Rf3nTQ)-c5oY!zq>Ew8p-8Ug1-3gr?LujLuc4Ju2u?xgX>w4nq zycztayiv(g>U+BcCvSOd4{2ormYsXIFLnY#9wx`+Ln+I(yDXZyX1qaO+e>mF1)}7= zi9n1Vz~tD(`y~;Oh5^by92f`#F@FaPDuGI5Ie=zrgQ&j_>19md>{S83Z~OwCFf!FG z0RLOsTQLzMQ%%KzC6#})h3Ds24)Evl)YMzx^2bJlF(B;hMJkPgO`r`iDpuqz3=*GB zr*G)`3yn~(_utU*5w+y!pUIFM@YBI3<2MD+(bhOk`S#UYO zRKid_*OD8|+}VW*c+yjFJ;E{v;FeyuRrRlZ0Q3)+esJ}7k5(94T-ibtiBFhJ+t3*>bBS~?FEvh zwDfOFBgo7@!rKFhXzBEO5@K4aq&!ws!0X(l_XWrqGnfE#%zHGj!g63}jIGyGsS*4& zz~D8Qij_wJlD)k(pd1gA z_n_7Idh3k-F@W_e>~w(h=g}Mc6e%c3j0FD*2-;&QKP=IY<}!c9)Wk|Tblm!oLRk0i z-bXamzT$7I-AnI@3~=y!xFSq&8w`!Yqlw-)N|MSM%+}eBfgUE0qhh3Wcki=t;B&|r zQP&5cht~F}1>z57aS%lTtHrx4o~7?mWg}-tf1SL0P!PkSr36)qGE}9&8)*a4g`R;T^@R z0AhI14#d1i=05Po*;mIZ;{i!K0m5eX>E**;lKZ}HuYDL>S?>s(ic$xAOD-H21^}&` z=!K=O?mPG*6KQbEom9OM z+I(O)+3-|zaH}t}MG~Gcx6qef0!&ic%xdTPP#l7CneElaxXxbLqFWJmNQ36{QJ zw*|l~2wq%Kg<`1++u@s`_~XF`@Jg0GdwnJagbWTBm3_Hj@(dg@&`IBudVSX+%?$vL z8YKH3dvdQ2H~RWke+(q;fyx*)rq|cDQRm)$Z+5u%#c8H9G9xv|u)c=O_wY-@{fB}A zMlYhSAcJJCyeW~dGX!u$nH`=eIS2r2o5APQaGRY~r$h=QznR<|(Jiy0-WST=*$Tua zWrz^jfBt}&Q^KL{%k|A`gyUdy?Rin5Rb-gezkkPvJfgJoEl3{Zu5%4+4KYiS##9cb zH-iBJ8h{kOy8p-CSHM-3ZtVje^eECPp)>;0jevAZNlPkq5TsGMB&4KEN(3bYloSLJ z6a_(~lu|?mDG?D{esdFwq> zZ35QtP&AyT!B=%~$UfTY`OM`(`+#|X=_?{VpR?;@O)G=$KI8F9^79e%=yfdnYom;L z5I09m5s4l-Vjf07XD@Hqdxg+12NL!=mt;een)BFqA; z$GbnWirU7@7$|uVIKH}f-sYxpuWx!^2M20@S5aN1VGks$+!m%BS?Z-PJ(f(VYQ6d) zgC4G<>$K*z@!`S569e;QYOysL`mGyQyKeJ{8f^vv;PasK5{gjO>A=m&Y@f~&_a#Tt z^>_YrL7N+sM?@B_#QP<{Y8M5%U3^fGYDRqvA7CDytIyALr16~`j}aM-gX~0x&nx$- zTkg|3OcITf^5Ny;N3Jq1oP^b|BJSfg>QBo|J2;9D#KcM*V}#}8ur8y%PgVRd<4t$F8$5^#wL52;r->8_3gk3R4|ZR4eSCOcV)15jSKqWF z6UXJ*vRhq;sC^e7i4>4@XO9`#W?8?PS(=@$M_zJsG57rtYSeiUOr7BAXM=l9mno7v zc$%Ieb#wCHd&@bSk+K{?o>hYDdl~e<0=$5MdHb<%NJH<755o`m+nwgozO;UW;pVt5 zC>uwwwTVXf=G@W>==kz=rA7==bp-mEC!2!E}ttN`QzHRB%l4lM8x6<|cL9CxE zYferK(#OH_Z5N9p!hmdSdBB{t1NrDPTF%6VAZc$dHr>zlBspSrKskOPcGuGE1B=a;eq^PZ3(xS5Y$ zHC6mIvQql0!qS}Vw+uTOjS+(=H7;bz#c{g&iG%S>?TUyH;E~wv__h?E5($16GFBLH z7w5pI!gI7M0{80w+aY7O#uQ_ySpX_sl~I-A1Taed)c6*-rD@W($gwIz|J_z!K5zCy z`sOJ{Zw?oWagZU@+}6CTGH08rhxVcrz`4(YJ^?s;#X0v?#Ba6yN`?hUg= zcmFTla)j;Om*k=f)W}R!)_ibNePI#9Vz-s4^Xx!*Dx_aBjB6pE@4r%+lfFMWQoW$c zu7`2D`%=ig4&_z7OVez3bOYCKfgPb}M|;wW&g3Kmo9WF1&y1`d-1^v;4UpP^s~WkVF?g#aWdurHZ%1g>(R@N6=*!=#@b?m(~H}D zLo5W^yY^CPSy2`9Z-B*%-gp%-iPTusZ-+G=x0ewIt6^p31mYUmGt|{A8}Bn8B#U(s z=%u;or==IK0nS)C0t~F9_xWKtcW>w6Q+;)}KKf;iGR^G(pv^99PTW+{D)px!;3{NK zCr?u}wZ#pFL=hfSiP~GDUzz}6%y}kRTWm&JhAR|A5!~qG!D3}C_Axj??yOT{Z01SH zQO<*oEW-6iLo?*)SCNLCq6+((9`0*und!VW{l2$EPuH~PN$UmHqZ#97WbAwh`nV+N zr-;b`Kj+i0AHrX|6ZYe*Ky>hI>Xg%Lz7>(P3Kk?%Q;$ZB6J3XpO3rzuMniQcb}FEQ zg58GBfotTm8=Rbt4PQ4muH!B^4Xx+&v9SYSo2Wi};w~cY&%zF_;el{P?CKq8#2Wz( zJ2+gq&H!k;<2I-aY6d%$1~LoTcNM|Q#yj{7oj{=#)&X}^`X#{Jf+nxW!<@z0BLWCI ziSQUQKzC@}1a#b(m_t5zr-3aokl}f3$pjD@T(Z;u9payOP%!-!V`=xX7Z&O3*MTd; z_~iKzgj~vruoA3RDe;y7qPjZ*ps59ttPt49K}D?QAqwJ$kYzawAHBj7q5&U10!xJ> zjIv(v@iBaStd&3vVI&O%Q^(~G5<)Nq3U+dzDt|B*!U19hhbZJk5EeoLK1#@`C_;$k z2$6*&@g)iPXbT^054OV^<@Fe>;7TsWB#2Mm19W?FFUvmAST!I%H45hzV5u@2K4xEH zW`a8uhC8f@(AxiV+^n>^vzM1wC?M3ct<~Z8VIhK9<5E-EI#PL3`O$SlGL&Jj!!JmO z?jaus<)?GockR7iEcL`*utNfu9+n2{7wf9r-ds7}0~shHwD^;)#T_^Uo-4myH+_+D zwSC(u)nx}Td=9Y$)}Y8t;7OFdkEeo%mJSb1OkB*Q23Ay60C8Mk^C;uTa25RfMMl}xU{UruGftTHsksD};Ac-$SZb_k}&q`8dmv~3& z2Ebs7AgV6XIswoxdNwrrY%fh{7#2bY4VS-kPYOlat=A9uE@cQ``hJAEiOx4Hjsu{s zQqXCXmY)a-uN7~v-%Uc_W?ag)yM#v$HGO(M-(sq5Zz}}C<8jut*10}11Hk^J$7{Qu zt7D>mkL@(7oTkZKF_^k%)MlUSy{)1>Y2$rVN z5E{9iI6?3_f-VuR207JFv!V;P(21gFE zqg~6>&+Ann|A$^bcO5W2Ltf+q3*_UKkEIg9dmC-wb%AiSb+8{q8eK#I0IUSa=2T2K#|;4n$$@q$0NvJ0`EKh~L#bU1jfg6`)D9*J1ct<7pFh5~ z2BZ`OQMf1wiw_xBwfSyJ_`^c!!w>1U*?g}3jtk@M{BCbL8H4*8Sk=?Cu5Crl=%PAE z8bhrJ3V;8*MM#@;9CvDDLBr+3>k`Njy-Z^l$K20u`#(zYB-7V*k0?T2-$TYD0f48o zhI)@yZ-8(!f#9YMsE8ZE-4g7Ftu1i)GPjY1!AxC@{31S22bvE0rX~PoUYU7 zXOI|u<~5nM0LYq)qnV>3%V?+@EM1@K#itfj29%ubirO=(JjI)>Iv&V;nL8rQe(20C zEMq(bzo7tdiaM7S@x>;Rj*WK;JWhgz`DU^YfTj}>36@%)x{L;_X5@&;^}|wK2f8dp z%zLSDx*3w#>QVswGIT0~Tq8Q@e%{$U9exx5aZX{)?ZVFu!XNlMKpmb;GDOw*fPlH$ z>N8!C8r|=HN#+RCnH~ko^!-^YPu&ZuKsha-L2nWrUa*AL4uyIXDfIqB;$o(2q+qm4FjoEMFnn!&Vk^!<5JZ5i&To*#5AEHa% zV5zG!_0jT^C&NB85@)Y>W=kGNV_DZAfp0Ih_u3J3dhxe=!( zF8zJ5Hh(t^zZbGKl`X~SaC^EXjN2ZNiOkl#MeicU5)!@Vss}nZW=hI$XK<*lgal5a z#{-a^MObD>3YP%x!05$M%7WGM>FW!ZFGwW-maQFAb2oQ6covoi-uitO_w~@%dF@1+ zF6xr^E7;cj>FM4A#k1Z%5&M-UCw1lh9gy5&dNFGN0{j+p`jTd@wCU=42V_JlQ{uT- zm>Xy>KP+3lKW#q~LClm7W9HSK-}v<6i*QL{eR-s2j(vCQc~VV@8);*sP=2!5%GyXS z6E0Ib>RpoF(PvruP)C!SZvUxrEzKHua#!}UWI;G9UIpb_Lo6^yR_0$gWgB*nNVsdu z9u?X+n8ell7Jb=#@x{^Gah=taJVk0+Yb#WD%l34REtPBr{ATwd zV@L*e9kzE7gLvJrdUX@-w=!}@t%Rvh7ahYCQ5-TrBYK$%E=*6W)~($8YRb={BP+#@*s;WHi?ch@A7qhf(AxGPI-ZB@cRZly zD1GXJLkw~px^bEP9+9KriGa#wjv4dEDO}~A`Sf{-WzYGGWv(xr@8ZAttcXSuLTX!u zx_J|!H94|$p4?9%r7Uok$#cCeYqYJjx6exqj!8rxL(@!-TPR|l<8psrZ>^KLTh8Ut zi-i|vryC+{k&QLbJZQ0m+p1lQPVqeCkzu z_}$IBEY^_76~M8E!qnr1k3B2EH6%jfVP{eF)Z+Do;Hk+M?ShD$X)O905p4_(>;vF2 zWqq6u1GtFbsMz8CG_m>lF+oz{`Q2m`6y7=PVzTtI2nkYJw#&7pVdd^#9f{BKARaTK zK7+7DSsc{Y0M{ZV3=cL?MJ6AECsXiJ);VQo*og^n6Kzp9VMEX*LopBmBWmvd-6?mx zo0q5ilx(^(*#Z5Z07m=X7`Iu2A!Mizf=N!?ZtxZcNarTq5G zr`7q?mAR1)NOgBU<}fHb1W36g2shKdI6b?mEq>F4A;`Wm&!Ja*?*whrV@NSgHHY3?4jZ76qC@o_CGklFe53*8I&R)4!8sL{jY>^ZH1G+|(AZT17(NN&z3Gks{5GKw|*H-uT;xC>1K8HQwbGM-FU(YD4Q?& z?7O}+&1#OIYg(XA7v(`E z80bPmRE_Dv*dhGAtq&pn(ADeAAgZ=Q?uIL*94ZA(V8b1)YnhN{ID}6L8+ge$uYymj zb~p5Ycgk7To0-KPv1Bz+ ze-HH@I0#=wSzZSV3!4i*$0B#a-&V9{)bJ0sn~st!R#_MAG!_=udv^sxcgM468Wk)S zbl>AGLKS{qJ0~~yuLPzF4%R2#U7fZ&)WB%aNP3Ie^r(|J%E7@Bxc`K>fzy=tpaJT% zmD3qu%I4M3g}?ZKxeS;y;S=l-M)wAUk7tDi+2J1$5G8w$5Zx5Mhy#L;nwt1OBimh@txk z$ZiwxpwOMi;cCWCC%KL}8}e!L^NFach`4wud!Is+3w=MpFQkl0zi{NhQ`x4O6~d4d zSz%go@NKhH+qzFZ`kg04ufDi}{_$zl7)ZI9{XWg z%YmeBNwKAa2S((6O-l98H64NW(myu&gx~k`Hu>bkbi>AwIb)>zJNBz@q@v%`UqR#Q zuTSz1AsG8DpSe^Guop4w*Ma=hJ*Wm z5R;#dh3@xgwuK!)5z9J*W%-3{TiC?|`bG-KI$FA-`^pK(@H&Hy*#64p?&J)c1h+P! zWr@qu0cDFm>Ll28v~>d`jkyARvPEAB{@ohq1ZIP!GZ=;Kji3d;ZU1f?+!N;HUr8`n zTz3@ceVMNV%WvJ0?DlQQJ6PMGhjfCYY#g?)${HPaqJtDnC!?*79hgQSUz>Trw_ni^j8XnSIO-|cAIHvcQ=?7w@n80YqTQ-ay%YVBa@ zj`I9568}BFx8>)>BxruGh3Fq-b3IYk=uF;UPIdmqSHsBB z-4sE?1ya{NyX3jX7C=buh1err(wO+|@uZyN=SlKR){#ox;FZwCEO(2ST4bw5b> zAEX*R+)?Oupno~l_^qYKH~HPpYka2~F^2wo-{X%}jgWXj=U#pgoL^<6{%@)g<6;{9 z;voKgxAI%}xbM#T=iExn%=q4~-1Z~>d>RxZUcZ*Wf201ygie2J$o~lKDe!Nb1mEH* zv_NAbtzUwq-;b?+38%KE0XkOu=O)4L#8%t+t3Tfy_*I}IfQhyKjdK9gQ*nC^{AS4i zh&k{Rt)Fd35Pdv_Rx;?S_Wf{N*C~ZxE<$eZ>Del7n$Ge;&d4w=EEDFYiFC zVuV=+`umYQFE{`HsxSQiZH0*MPaDQyL=Gc@7!!#xiT{RFC`P3J){sAu72aBO`Wxs} z%!1S}FyQYOnzr_0u*hF`Y@e`&?B3eaqLZ?Og+*nAu7-hDnWo3vDD)^tGdy{PR659d z$y(mGkdWuM?KtRuN!BtAF)SlUc+lFbP~sR$J3d2~sE~+#Fw};*@N8g9Z^T}X=@!9? zLpwMH*dOKCe);141qkoH(Y%zc0+&9*u=d~}PhBA}%#T%3G-s+6exkjYK0 zFQ1_Twq!Rc>(qCs)`+b0a(H;n>xZM1|&-R-_s`p&o;`HL}w5hd$p__NVG5u*z~A|c5igcnc^O*i4S@U)P#o8 z?;-X9FQUPqMkP?{P)xp3J_VgzN{3H~8(w(N>lhu`qHKK0B-%AX+7O|Wm{Mq|c$`EV zx?o;aRDVOAKx7J~&O&u*4}gGjFgV|tK)V;3xCs$M)6W`3ZkK#(MFUs5yjur*#i2sI z`>;+zo<^Reg_Koi-5kDgCUm4iO|~edGtLZ^5@VpiaH3&$;IMRitv_p z7-cvSKKc|_)gng8LL796$c_|ceMw8(iocgbRa1+Qv6}=WGg>LGN>n#Onr}-QBd62* zBa!!Fj^?;`$j2jzuo>gna6S%Z#_MNGYUTRe(g-&lA#bZv)-#!Nc1%reR2bk9qT|e6qb+h>1TydF`1gL|8`9<-8S*`8~gB6xAH$(d)$Uw zs=3vhPmZIWpzlXEYrXuw`N+{XL(FSsA?|;;5|1jxP`~Y6~AZ;}<-6V@%Rr-0oircSz6FvvW z$AnnDboTHc@T~F6E3rxs?GnDPy%~`okK%vKicev1c&0k)VI-b}V?gYT`3D87A)(6E zr!+^jfu%hDoKj^bp@y+%ijDwhVR}J7^KJoCSILS=OeTf>erZ(vQ+r@3t{Akwx3W!c5nAws)$ z>_nC=7c^|A9=f%<;n%w{l52Gg$)iTythkPOpn*s)z|W&!wpay zo&fr$DcX7Mx?xA&%Z$V-?a?)!HOEx2W19CRkKT{2ial_ojEUMs_Sk9NtBOYnFF%dy zMB>Em-4&YnI76r;<=6{W>-JktCr)T4z88sb0zxl)ARXY)2>4X5Sc)3TpLn(7PV-6# zS}1m2nzhihrzBrf6=e5J6Yb18T&oc2(ifh8AhBX5qlRTV^_*N~7Ch88@&l>lWSaQw zHVo1#-Zt`m7^nV>Qt8O_i1_UJl5&I_^T6U#sGMwZTfaPNm}6CTIsY*)u#>-cj|#Mf zn$v;4zvr1r&R69zirpaWYlHSq_PtRw4Z#=aJ}AWNo7@{}o3_+FNBQIdNn=|EC2s_W zln&3gw`Ni-x29-pBW!LkVlt=Pa@nZsDkIn7@zkqV8@qyA$XWNLpAuSrD)3gvOgL>N z3Tr?J;OE@D*C-T1w?I)*ns0qu zQt(B@P;w-&D3`6T&X-?x7VVtY>K~CCq)d)7zkb2nhe}9N@Y6MIC&Jb>L_o8`PWG+= zA0NA?57iUBp0=*>nBUIqKeX~mNPr}`Z0h*lgNYG&GtQBkXwKSonQ--nH&jI;{&`6( zs3+4cHi!rN-y8YPCr&LN?Y&bS02=bSgTY~wkvi@(=RThmjqz8Qzv#VQ5L;CtIQ;qc zgN9VoB5{=q2aULUOpqksRw4q6xKJ1;l&zGUQ@;@C>9x5GuS=uJSB4uE zX3t|0cfV*Muepw!cP(4er?~0!x|*rmBcT~+Y872GRVAPM3Q3kxMP$uTir0pB?C#p) zq`+|T&AYw6j9P}hINHz8zK)q`3B{{6J5i+&&VnPKdmT)(QSVVW`MP`{k}LtveGV1< z#n6yq23lh2Hf^q49F305KMO_1I<{SzFNZ1oQjxl3L9z)HPj6g5yuauOuxl8MSYN0O zhjQ!$AQ4J7d+|yJ5h1-ybdVV}^jxc&$G>DUtSSw8lWFXz@w>c^9{%wMRam#|m6SJ% z!|3K>6z^QDDjq*~iK(G`A@W_nUS;jnrZ&ZU7s$x3UR0G4+2j2&z_w*yzQ;OR+PDcJj@ zRUHWKu4!E$4zO_`N8_Qck& zRCNJ@HxeI-N4a+`9+;`nMpQvDej)TOxzt_)jB*br4~wt|gmI}A*5e;4sPdEaJy+Fz z{}3%lsA*6L*MobrP~bfSY_r2V*=;!G(~%iH^!WZ#3qv&-z-VYpvX&-ql`1aMZUdc& zRrA=A5C?#mzEobkE4?oV==*A^)JeD;hAI@&C(l(8q1poHp4C;lX@;){H$rnMd*Hwk z?_FZx((W(Q!IuS|5zRwVvc@{b&&_Hhy+zaVJvKhN9Q73KTv+V7vsuod(m}~rb4G^V zptpkQfI)AtB`K@Ao%MmdI=++ltLOzQGsU^zGn5O4bVXM=9~sAzOIo-`s(Dt)ICxiu z@t}M%3rYnM}OP1Tv!?ltCscoU=k)*GkgkgXd|4%@gWoic17P zqdCxHuMMxSbTeDrP{`vpa*8xwWJ0YG?5J^KMYytXUE@(G9k9U13m2=#TVByJBf9CH zz=N{>kd@NV7I+&d0(5{>$&uXeRpxm@m5zclLsyK5ks8L2_Q)rV-p)_9^0D($S?&1b z`{eB-n)%{qIr(~5ugVeDqi0h7FEfd`w9_~J_vXnDQ|RqcV?8;}N0e_}s6y9UZl|x) zGy6ZX67hIMfbG=#@b2!ldtF`F2h)*)G&rHmr8n!tKfdy94#u5QS_q@(y|Em{XIO*42Sd!9nH z(GNYU_gVK?!@2Lx`--V;7>-=w>ZAOicb6ru5^q(r)Piil`2)t;7q}Qk z!fKeR8scgmj&LOC+v)aoFrl6_b1zx!;o|YDt(|+=?RhR`KzP>J+!XE49(VVGXPZ)c zA#DG>OA}r+NzO80K^&cWCgQ7I{=zS;x>!XkUh*0mMR45*_udx91pXGBDzF zaN;Q)cMftaoT_qptym4giLnP;ZoUkJRICD7i^{)SDf-J-u8x_uUOYT&Vz?hL!#wdeT(-}f?`GKcoS@0l&-YU5Nf z>!;tL0h}NPK>wfz+~uc;QRy{%!fc+Lj$_NKgwS<%SJ(zu`~&EW{RB)>wR$CC!X5rO zqKaT{I&pU`N**&^bmt@>Q#VUU8(w2JdBx#4KU}TG1su$u&JA7%=4cP#nJK)f%-Jy4}t^v)ay$pP$Mfvl7ydOmz26G*SZeZ_A@4E{&7Dbzz zW0dLf?pa+auc+68Rg`C13-HZe_w?81{^!MC>K9u$$yNT23 zK3~*0k8NYdneXv=V~wK6z&H(y!2P|L)aKH$#v3CNXbSJpM`}@SGgF`3MR3P!V0# zso?Lj1gw1p{@7!)*9;#R0lhzGMS_`YG8;2ksWYKSJiN{o?^HTedN$Swv3C1%#C2&C zQLACcn$&8(0iipELR=EGKwwiiP?haH!`_%O>COC-1cDQhGAUuU=}CLGkx-f3m-#F` zaq>jGO!i0Bve%{h`=Wl<~Xxi8KI`%LbMF`LrcIiuwb7)?PNb}xJ zAdG4)MFou^1g=>JsayGJKCR)M5W0NaCrE1AE%M-DE1CUK@8LDAW(c#sB&pV5;i2?> zT9s#}lbe_z#h}v5)2WqDW`J;I;5ull@w)SB)28k+16Q2lvk_GT=}ynvUF#nn98gL~ zcFJc#fJOZZ4s-R8djs*U{@iHeEee?q zG5N@kihEzQ*ELAGR}T#k_I{);cTvh^$e$?FxM3XT`LugsL6)O(Hj&~mPkEW^t@-Pg zcl&9a8ZI^MySu7XvD!6fU&DygsuY;7w;`q4Ph@`Uq#9Yxo|mgYMpxo=drcATZ#!MN z@2|XHKKBqZt}3H}?S|tOFv8xS)Pdu2nS#+bVjV0|G4%Ij_r2x=Dp{m1SvLo{aR5>~ zyBl6YUx02Od>Q?=gV-%CF3=KYnoCk5LOPv@9+jLga=`ziz00OX32+ekZ6uN-TiU%3rZ zPGY2VECNfVBuOd|^$NJMFQdtJvIZh|5g(+%iLO$8dO3|125PL&dkZUC%(!RE35VpK76OGPsmT+G1Xv=f*&=}oeW!~C%M!@uv9q1%a~CUSmI7b)Cgr` zF?Z|B${l6*=^pBG-wT&baP6g+?o|yjrYUDYRTD!4an#m90mk6Lu~jNEb7(WYd;xx6 zwF0^$f;X_CKLKtzcYNvb5&ozo=J%{CAi!cTZMDi z)t6kS;61xKhe;^o^~t4cM7E>aqw^%_DCaQ;JuwuY@1FqTVCVGLRAV z@%4iXXS&zF991?pbH3qX0lz*7+k-eYrHbAM$gu5u@)XvE=@|`rBY`|`R>uGKt}P#z z@H{+N=8V{iVOitY+P12FAopk4C9vTU5OB-(KE!q(tq?^2geg_nlDn8vAOk^>oI%3h zeq)`)IhgmCe1#K*Brec%)C2Q49T&3#L`6Wf8?eAd1wR>YV4L(L(X_2v<28^{syJZ^#Z|m)tcU@xD8HNINo@}?XH``#F$W09ZA1D0 z5}Y<@P|S~{Hh?StiAimKg?i`1pvS%gO$wl2EECvCJP(R;*xPPa3;a^|Dvkfl(0erg+lKWSHZ~x(vZ?p>e`;2D)ghu#16!jle z!)N)IrJ~uMaoZRuK1^xHcS-`IM}ANe+vepDQ4-q<>JL&9-xR}^n%Gtue@97tM;L)V z>zmH_<+OhRdj#K~Qc3(o@ZkUQ4uR}~v-XW6^k-HQ+vwoGw35L1``^0~e@Z2>U9a@V zylhOB>p$Z*ihRS4|KCl*@4f9mr%k~8(jSWW{~*utuS$%=2Mpd@ar?IC$d5tOe`geg zwmr3<#&g8v3;!5J@qer<`KB8FMXngh~d}8OcNRYEjnH}ZJWCH*BAIku=@`bj&BWy8UI&#w*Oq}T|P|a3Zs2~ zhn96Kzi=KnzT^G#E@ zTRS=f)gu(RTH3mqV>0jN=x?B1w&=V;Red}ByKUd8uRo3Y_~xu&)Cb1?edoOJVN}ZZ z`UA59`G@4a_0g%g>{o{8KO$j@sT%${6wzO^#$Et3d$(22e}>@vMA_H>vUmT4 zlJoDaOFvSYb_@%PPHwv{J?prg-hwT$xi-;4k;98U(>!3%-W0B$Xu1{Xl$QQ6P#i)9G;X$Va%b7_f#Uy*x|g8o{x6G zwY4v7O=2T{T|=MVrdO`2-=^_jvhC{3NPld+`23X3ikOeShsSbbbF)m#1Z&XDj6F3K zm9v{O6*^bEn0 z@5VoJg$`G~+hy0uv+=v75ll?#HP{gr6`LWWt(sVjR854@D_D$nWIWM_rLh>N0!an% zu^FdshVV7CA?SnSiCIUm=P+nR}p2K5!Jy0V@ z>L@NQt~>PEhDxc!&l|iFsi~<8OG=_xSy>D7@`CltOjHsR69okYL{ z>pi2RQe0+Gc9ox!lJaT2WpixxdjG({F(svNJEyZ}(;}myLN`BraM4z(xrkC<^W^7m z_9a#FsUeQgLm0VhWQxi#F)_JMJWNiCR^3AwaGx>?ze%vLvhrBO*wj=Z5#w!9(&JTj z@z?JLKa_nsGNJ|z{qpvkZwx_AUTB=k4M%(XyzK1#mf<6lkNdr!?T;!V6KZU4$>t-Gph0ow z%jh-q9KV8_FjvUPte`dgVNXg`JLB26gM9IGHw-Dr$#=iJ<-1m4Cr94V+uJ*>o^<5a z*$*coyW3al0uE((czCR>pj3T`nhif(cELm5V-5C~W%F8_t4t@8VZYX3qR(R{UU|^f zh8@>+w83kif>z`$drl3$b4X)eyq;FW-Ee}=AuC0;RqVAFDs2e8@SA|0S8<6uMO>?xqR0088%FiU6~OR*I#Sgdmjyx ziMo}h$;ApTh$X2kyfwE;>&_50w;?U>xWKUkbP}g1KwUikpnc*jeca(7qYKb3y%8!0 z#m?Xr?#7|V!bu?+%XufqYFD(_?MQ?bG=(^O3&}{8#dIhT3u&kd>Y?a0dIz>7+^Dx` z%N}H4U;%tg5hr-Q6Pu`84vVocoax%ndT?|EJxvxWwViSw)G6EH&@l2wToJx6-<^L@ zo~GjS@D+0uYWTK4W6zJRD!~U;vL+ow`Ar)V<*`JbO3k z$#8pGv7Qad$-k5M9P&Pyclt#k^gd1Me)Jw; z)a%ZIBSbP07Thi;8)B!=+YR4#k!GMl3XJkv%Xu=zw=x71HD3!F8W>PoUS95pi4C&{ z)o~#vM%&t&rMjj@$IZ=6|5o}QtBeSd)6a56M$d-SIK9YrqhV$9)-oIInjN34rn70V ze0aqT>%s(d5-tEx_~hHmMMdoPpc7VBJUKWx9u8^LM7l)gK2%vga^Y4@jlO$hQ&Y|! zJiJPLrrt?$-!jiTb&}1wiN=%6D{gzDUI1x;YI(y#q^M|}-zU$m*nv6sY9<-4iDzrH z99s{|-!wffEjKc9PjOk~Q^2B7X z*X!4>*_Rs_)7t%O(l5mtL>^d0t-Kvtns55j3uW2d*(eEp1JK15C)ZK@^YI&}i2@a&?eVQxRQsXllBs6Xp7Z=O30=X!?KB=$QgXR+t zJ`|+PzkQn-dt_hjhFH1a#w(#oY2`&`S+bq><_#k9v57S`!s2dk_Chhs@p~b(tGE3= z*#YeS1j|I~?sb>mWQG~)a#|mU^jt(&Uh5f)-7#$YzQnM-1`ZkpEwhHReAd$(S$7%P zLmYu`hTYQ;amwCa=baYqwYR#}**T;`MV9Yo_I`kBMxyi_vLh67x8FR@;X$xGB!4>X z>bgjhS&K+>)YQ~`cysft3i0^uiomQR0cH_FWV_zHs=mBHLr2$m!IAqo{?<>-A#B*E zam6~DOewbX)vH%YX}SWSy1gE;8UK!I%%eaadB+kNf354yU_C zam84+&#O@OrE_|Z!-HFsZ`p$D?k93GPCZb0&=4`>=5*j1k<5ISU?FnBVxmHkPSo~d ziGHbSU835Dq=}XY%PZ>dUxpuGHCvcmw2)t@^PJ0Q>(qTIz#iNgD{!VO*pXbuV;|c| znNjKSM@HOuPgLGLCygf2tcjvu%~?fmipn6!ang0>ucy<`?|OLH?TWgVhje`iu?L~{ zb0s|LG*`{IS=W`70rPRSb0Sw&3j&o|i_Cds#an97p4!Dp3w zb942@(pU&#6{zdwYxGxI^HPlZ@4~@YJLS8yYD=`n^9DamIJrWT{D8- z@xIZ{^j!a*aHM^NUDKVUtM@h>jvah@Ubj-H6H5cJRUyh7hOv5Sm8 z7`^9njdb+++?|d$9?Ex59FLP(J8u!^&7HjaVWQEas*aAGd-D_miHW117Cf_eyI<|K zXfrc4MgRGoO&X`_bKW@w*Wtbt5t+JNT4WmiSTu5>`7V$(*#Y@VmF?qft!sJx_)oO% zv=B81=4$Ol2ogEVBE%Ap*SoVnulI<0)0L%WP;Q2wiLY2x64X?_J0h38;yNDAD(Ti? zhh4`bKE9IT30)i6CsWyc(BmjBnS3{y(I77NE`_*Ir}hLE_NHgFf;7=O16I5;h@-us zjzr2V7JjUSHmzkR&hb{T%@{ZH-^vah(t<0pHsAsSf`I8#HoM1 zpRv$V%%O!{<}_pd$Rj1J%lJeQs?ukk$-!-EpCnn9p)UuwC(Kz1o2Gy}=~93LOQ|H+eN zkpt+9l(7?ArBHI4qD_07^C|C8pc%c~@8j2bjuQ+=2X#eP_hJu2E*eX}f z>yEm_-DP%_2h=-j??s@4nB}FV?)UF`A;OsuI+qt1qcd<08T-v?VAVhjJqQU25#;Aj zs6@W;@%6&LCn03Bb-sKAeG^-LL4mb6!rRBEsH`k@&T&#h`i~@d*GiS#eMJ~ zk;pByQTnXfRC%=g!r)Hx(GMRYo69A;1g)E_19(j9<)1ux(pM7g{vny2NcW=7c-U^F#<2>>-L%7I{g$5?^to_3F`5N*+-~!i zSbgS}7eGts>QUgDJm*T|`+D5p>%sVg7M`w!iCyrvh#A{*;nuus6PkH;7?R6`@np43 zh)Q?m9N|fM>YBGeqHdGk`!bW=#iC7xT>fmH)oB@W;ljke7zLP&J4L8aq?yb@NAz&T4hp$KXIGNKx$3dCz^@sPiooY2) zt*LA6R3^85>n!eXzPT9sFvZEj{z&>&+5?pd%MBa(O}aIQs?s;^eBPT-`d}hFcaf>n zR}BpDUYW*IpKy~K7Td?vMYQ|tXG}G1!nN-x7SSl^f-%mtNQe?Y-A_%}#o_J1bWF2i=nzJgAOcx8O zE}J^{kee`=`p}I8A3Zw(t^f_>Jy;HhZ8t#&^B>p0An}YZ_|b9Urw(i@gGu8jbQhML*8h zNz_=ju-D6X-*TF$WusyxHL0@Ge)u`#MRsoeRc1oDjg-6TD~L_sq|^IKHSDS-=d*Kb zdD2tzr}r;@xNmfYD@1)nL_FoL%nBmJDtLxu#OZXwEivn1AK_`Vc;8@bd%wAo{Bd4e zUMu@f(bXOYfr$6=Lw7rB9xxsuoEdkY=(pa_bIQsy<(<1|G;%#l*gJmXAXd3|KF!gf ztcA`qQ&{R^sz&DG@`jb+{j!vSxm^(!I83eQjs-_teCHWrb);su@*sXd(_NBw7P~8H zcZePpsf|V(2$77B$#WfeN|-Kw?eM#y{rt_Dvf}~C>Y^+tMk-?B;*daL9M(59^1OLI z>Y})eR6Xp4?+$^I#%HABGx=f5qX?w?m%JVs-gbcBa%x2w#Ef*8^Dpul%GMJm{+ zb}9QSdqP2Oe@~Ch#zy)h11+{)T($yuOLT|^g!_X=0mhqW=FP<2r-oWRxr6*~H zG}F{sZ34tA=0J5`jdj#;7?QP{eJY_6jPGuDE_vP{4VlzT7A#`*+*1s&Wl#P1A#9Kh zQ#{ymteeFaGBc!8#;_$Tm|IK@a-z}$vap5Bz1;|MDTsOs*m5;jtqYPTtlibHMI6`E zHAUJ?-kH_6Qf8I(t@xZ6O(PW*ApmsCk#j z5(k04k^V8`=XnJq*CSlC=$ub@^di7$6pkY!8=l{TvGpi8waSZWfDVBx};krN`^DGY90gC{+fr zgWe)Y*n<6s%)svB6A<(Qjh75?kI1UBw#`$5IwcM$DJ~}2cf!_|t-89p2kjQ5q(r`T zad#J-c)@?s0nz~P-n~m&w-iuE->lgRuk>!Bv^w-f-|nHPA!XC(ZflEpudbkgt7l+< z=W;Cd?v5Qhbj{5*8yg#S&CH}sPj{qn=^7cSl$MtEcX!i&mXean);SHO=~X9MBd6C} zTU+})GA>?}Q#glyJUsmtT2u%dsv!BuvBAMKqWnZQ%{^z7FO?J(sSwL_y%LqPx^aku zgRzSWNZQo5%}XDoBbE0sHiaePxF#cTpfGO~z@g-Tu`)6YQ>xwKjY2nXgkL*lC4to$+;8$xdfOLIW(J1hdE zF%U$Gh!mX(e@`TXkCw?3j3AR(;Lc(*ss~D|!@QxsBZm-@24RdiFFh*Hd*#DKsZl1u zB)-u9WA7{As>;^40gqBjhav(Zf=F}d?glC85G5oJDWQ@g-O?$NqJVS>h|(ZPOGpVw z3Id9h-#WliX2zL&@Bh1V#|eLmoVE8}YoEQ|T5G@0^OB$-KZbmu^wheUP(J_|PGq*9 zUfFFK0em;Buh|Aska6-u-R&qhx@4J@z#g^}jthvZh$1BjWZUt-vJAfdSnQKKmMQGDqe(}q1g-@xm*q{HL7&Bgy^d({^SOdO7sZ0#xB zZNNTD=C`< zQ#M~olD`5B4c7q&Zjr;h+UCY&h$+~RS_1^S0m&0ug~lgDK*vasrE_Cq)LFrEtxPop z6ODp?_5EP|RNoq94gXs1{0APq3p}?!-1-*dfr$ z<3s?yA`&=t8F1*Wz2tr0Nt1jF<-I5T!Kk6U^c(Xbca_?%!4}w{m>*Nx8#V!7Sm4C; z`4zyef%@%43a}2mv$0q@`tnep>;3w~OzLin#rbVeUWbf%#E)C5+xdLAeneR5(*J`*CPU9z|yxr z14-Tc{edJdy}Nro1nmAjdBXDW(WA7!J~cBdtC#P{NCP!UpT6S(b`=R1Jma&0S5P1e z=dT2z5fKo`ncTR2`!+B)OmEI8tEh->XOuTLOOGGjZ6S4Y+W@Sq35-n8=kc5{e^#Bm z++2liH$%^l$CzBOP|-BdJcq1vlpCCo=}X|n+i;w0`ZfBF5dAVcWE8#3_9gtQtwS26IW?GBnm z2&Kls5Y(sQdF)wUn}D4Fuuor$f!hOB)j$TK>9GZzBrYM4U1%JVn6*SDxfhsNcQndl zKuV0+kbTDjZwMa42EvUib{y>Nw_OLG^VFj2aWWuAl)fi{)Wy64VgVi8Imrjq0_WM> z+}zFW4`+2imlVw1Xv^4)Ux*bco+p_mg$DmR1BcC#S<+{kNJv%S-}e|epnD7)Rhpt1 z-(lb|1^wRNW8gSis&MXS3>^Ff|1fa;@eCaHDw;AzGLq(+a!T?h=Ju>otP0NXMDSlS zNBk-S2RqjR&;~)k0cAUA_=kYwmyZ#_t?>^52LQ?1SsPq4wK7rNhvXm7Yp9zV+Fu8l zLXc_sQ~nK*X88m2Z~QC;@H@a4==U(wU+eSyA<3h^q~Ev-3MTC%K@KIP|3kg;-=N+= zqz)fsY$2rKPrFW6|Z$Kk|q=zi>N4Xi-twtydO|Fpes3@l7dtoFOGFf!gh zg0S?!WFMd`*?+!(VF$rED`?$|#KAB7pAejP?5xbd=7VP7`Po_F!UulBe@MU^fAd`6 zT@KD=KDgKaQ#X5~?fp2YOaHYOc>i#}0~$oE;U5lM7~+KeFAqOzX zAlbKb_I^LOc<<=mH$n7p_$b_m!@ncki-TeJCgj^!+kn>uukojb;ov+;@Mx~f1Dmz? z9DyD93X|M7s0efOwHZ7}L>rjQL%}BNPp5Hz5}^lEcrby7rVlE%H+etds5>-m2a^V$ zGWakD(*=K?tYALiO$fXBwF!JzX1G!Qt-pIl^Xu$^x!daz3h=*seg1}XM(|PoFhSi9 zgj)JH5c+>fvH4j{hgcdM3h59`b3csKa2@EAfrZ2V8WO0|zHWmVI;72rfaAd9?)zJW z1K#tt`@RDn{SO!-_U+g|TX7(Gg44qNQBfNkGbf-a{Yl*R9V7a6gYL&sgM$F}M^FPp*foa( zdh)Lk{#OTm$SwaR!#9M@MA*>pNY4)!zkQeO>t_owBP&B;TU)S1hKgOYFtD>Ty#}=O z5Eb}Oxx^8~;QuFvNBApDu(e|AtO*7?L;(DMCwTl0anga3|2GzVhoYWJaWirrxHW_%{SLzW^HC8&Fk;9* zh43C2xSx3czmfq*@Gu>wzWb}{0V^WI0ig#72}MZc@1O+mH&<|Z`L!Yc6iV>DH`4!e zCHOZi_`!W1mSu!||MiSIVttN~te>0L-$9rW`toZ-{waj{2YOBZcdg#=wG4{qrIJ6vp#=FQon} zk-$%h=n=ac-!>*d4Ky+MWPRPj(13r~FMRZMM+33F@$Itzw1&}vz#rBD#Jv6GlmO5e z{WLa3>^A(4N`Nr*2)+CHkbep#_}+`P|Be#C1^ADz_J@%8pJ>5_&aI=B2@piA^!|ofY9XsemL|~SFmUB;np9%4g2-Ozj6h84rI^x zf4%HKbp_uEhyIHt0e&nUKujh=AQ9ow@2CTa`2N?1{8Q+_AOCI;ykj`@wr~Hf;Qu!s z{cyWe*^{V}^i5&U)ULisw4(TnziXZk}2t`6D&bQo8c!2P)UkLp>Z(4p|f@}Re zNJpiJ#6&3^v6 zf~wZNH5L(rFuMp(&N(q+S}2yTTp%7H8m=Mw#VF6ntkCfghuue?YK(8bu>%y=$$2{h zsFdp?Hvvs1Ao;)*>55~ND>il_2i17IAk zvU_BKB_G4=;m#PZnU9Kwmg|EGW&@~c@u~R5Gvk7S4QZo>?3}lp~v+#q8 ze~p8RT44gWHuC3|mehQ(&M52Z>P}v%0VKB;**Q6*lZbrnXK6h>gi`LI z38mSk9;Hmg?~FNy=5$wRR@`NMuGj3!6$MIvxc*euKYl#6sHLUV18Ah< z*v-|JKe3zlL9Vf}vu7K%MrFgQKT8A!24(>s=@|(cF&@q=7F?WV3=bJ!HgsqJbEF^- z4^KlRFW{=Vtgo+6Q3WVqCIucu*C@X>1gwV-Gc##6EG#UJQ!ZXU`TP{c7!yi5v1zV! zGOwco;G&^*ndk6h?N9skel$<*kp@ey0K@!iR4h`(HFA@eR3GWzHKpyGuy8sCxh0(P zf%2(I2U)RUbGT3y?PP{5@$C5cxYp+*l#8aZy~Pok;nrG-gzN~F}uQgmja-P21W zX3`@iEqs87%73ybTe{mz@?xw`nKd%|tVEF0#tFzp_(Rc3T1xS817x-`3$s{6Hv|NO zy3Y^>$i!em+=Whfbom$21+OvHhhAwVDb8`8Xn2NO#dT+y8wUx+0L_W1>sH3*z>{n2 zU!GUj$2d3%r4>q?oIHXoh`X$p)+%yZtb$&y7K=iW0YW|OIC`7Yb!)NIO!I`f2Ov5@ zIpL0UhCzaQ1WD|ze;X`CJ68c`=~Lm2FO}L(;0}8wB(NNh%La(RDRo86{5OL_Ql}l{J(ygP zi2Ko0?f87{(VgjJ4E^K!*^gFlAnWLyQ*2*>clG!}7_FIJKf%TXsK z57woHd(0SEDgYt&}eZvGK#mPM^lK3S6D(P*>XaUJ1QyZ4@M{ zc7t=X3}+Tk!kw`T5ap->8kA!3OKzi2Z#ss`t1QMrE%>2W_@*w`cua=c+uJ9rJ{jSf zEc*CKt;*kH#=@VyKvsa!{K%%cWl4gZm{PY6ty5ep3Ob;}Xi!vAa_kfqxCp8 zSZjGAV_!A>#G{>jTvHe8lZ~QkXJgqJHD8asn*@*fB(hDcJ%0r4^9&xg{ztPNF4A6` zN&6a{-JN*}l}2#mg$Gjrm${&Yc--flUtBG1*0tsyDtk8J@~WD4AMfm_RLtiWzPAPd zYa?OG*xmymQf~-%1NR+6?6MAF@&6M9EfGjc{kiQQvD_xD`W(AC{pT;94O%1MlQW^dk${ zTX-jiHCgqlJyp51%k|h}6C@sH!Kh)3XJO)ga!RWe8MVZM?g8eIhuma9Y^n&)vx>LR z$D!zNBQfUUW=%VRiTI#)uZe&?&m7~{%8{}%iIliWVIYLsN|Y+U>{LufDH{8IEgfD> zsuMJ^AJtX<_Immmu|Z<$amiVnjhSS(9=i_yOo82vYvMJDo|tH9ph!dFG4?fNlw)WX zqt&`kS6}kZ0ZCH>#7fY29@Ii(#z234F(^WjQSOO%Byg+fK6SY)pU86=5Id8-wm-(j zSr>{kD{bZjTR>pLyv?jxUn7Glr{o4-FY_`V`bS%e+6g z_mK#PE~V#1V+^s1t9PCae>g``c)f(^wf$#jx*Ttu*#g!hl^6X$Z@_+)#&OxCR!l`{ z_W_yP8s$4imx9Bxdy@SPsD*o@mW7<}146kpBp@%38qlh;LgLAN<> z%P+2`>&E7LO7Z$WWW_a`49+IcShTQsGHZSZy*8aI zeIv+#waK+0sq8kT-Ms3u>Oj!1bc<^J@f*HqCVjQU#KdQC7T1U6<*(Vc9}Si-FMgg$ zR)>>|rgFBJ+&=%zZxE1&Wu4)@kp+r@h{s8=z$v-K<^}xna{KJVx#x?$rB0`C8MWL? z=>_amIh&iCR(R&>cQOb~j}QmO<&#VA=oT6W@mP1I6k7JYG*?M0y7i*Gqil)zIa`wh6k@2>gq)|*FdbC@v@9z8b0Q$5Z zz;&f6D=T{ib%g%igbfweJwNNaoR))Z4$EWDQuKAkx}6pz@!ZmP!d0}aU!2ti>ACg!Yf?9)CbVN}^FvAbT~r_IR&p$dKut|gZw-%*>f_h< z_4T>OKY8-xith5L^YIDB~R%~&Nc%aPLx7rm4J zoQe!W7RKVZG>jAq$iy@mw2Gu`Og*K>d3m_yk{R@?b((Dqacn4BQ)XP*G6-nmLU#4u z=O)}mqKtC&@nGD1k}%(&mjgIjQ3ZxU3}PRRIThq}uNp%x{dBB@#OL;BQkGIzLVzc( zsH`;il-#xXNOJcGrpGpYE)|uH^h92gbZemRbSBqz7!13t_u5@zC1pYGY9}3)vy?Cj)&`wwwi(xwB+eD!S@}MShGZ~l_I#<%J1VQq+_MYRG%z*5G*}_6 zaXlf64lCMaS0-$FLDE07D3Z9ST6W?iwQHAx+>(TS^29dU^OPhQ>zgccOcs-r1ip9D z$Oa#PROak#eaa!EQ_JGEo9=^P$VU9yJ*&9raeTdh7+f5(CaY=C;$yY3mdmgr<5YH|r=- zq8N2~f~7tq1q}A%&$B_A+QJ($Mo}}6oLB0J&p}N;M!4<-5t~O4jB5+1?kFZ8zPJ^?;T;$N3k%@Vgbk$^XWPwK}0U z{?W~sO%9csX|GYAuGOWUx$8{&k};gGn?)uxm0C&f7$ucFMkIlUn38 z7=HD!`ivyvS-Z@N7({;d{56(P)6-!Jb1=QwBEF#7vbhfaCRjwAJsx8$bhBBhCFQx< zW2Gtff=E{5rAw#S(AjkjG7U8B>&%u)s>O}ESeK0O2umZNgr=KEYXOa3nc9b*ehF1- zUD(LHkwWaf7e-wl#Dasl7g;B6G-gnm4>59vy{w>q4C_c$kfC}i`^IJdG4jj2rysRK zx#wZ7!6cX7NDWB5e8?Svo>Nau+aQ9bfWV7{GAvKl8BFDQq^TLmqijE%Z56rsaY0EoiXJ`blJ%)O1sX$fMS({y z@^=k?@SyBZ<91CYo3phQHLn^Mi$h^}#Mt^e%(bttem2;}Rorz+^}HHfO~7X>9*7$xGyhU7ANm+5Qwk7OlZ2piy>VD4Mokgv7?9x z7Ht|^edIZM#Vk`^Zim@&rl$JRnwuof~@n*QjloURx&)AD4EG;+|_reC1ty`w* z)Q|82ikPTk_$Z+3kwR-ztk2RhcH=g18&2I?Js-ZfBXwHzBQ%8kY?xGK66qu|Si=p# z!#IAVDP~4R$4heaI-!gy3o+Ayb6nf)$$6m9t$dC+{Bxhfl!| zqZ0j34v*n@Kuum|-CQcCIjKZ!`Z@MFEge5tGoDh5I|S81MFDlk5eN0o z&8~FWIbcfE0EsA%Yoq9MY|5bXN5NV)7fq3a|60Rwhs8m5cFVycp_JJ;RwMt-$q>O% zanUrpAW6An;50Opd-86eoW!$$tDi-P&X8SfN_%q09SNuD3^LwKEY##7SS=j0yL1o- zYucmXb^Q|f#^QWzr@9}6wG z@jJD1y*Wp0mfpWdrC=1Rzd%GV)%Ptr@#4XIo2MI%jNLVDzQBl!wpJ4se4vCt!cDxx3rhfC&(q$#ffV*$%C+G)#s8%IEYa1tpZ#aGa zVAKoN-8U8naJR34_DdVe z9fSDmSa?+a?fEBD+#+mhC*-byK$Uhw^Cc)?q7CGl_K8%h!~G83_l0S@?MeK+Yz?vG zT1ScsDfF)ElJPky@^8L-P|W^pz?)lAlXrbmzj6zds}W%r$>r9{lb*c0mO(aZ82vVq z-OEv0$*y-R@JZMW*l-yOtAZ`DDfQh%3d-5h>%#4cyqp?WL`6l5ZBdV+9Nh#}gpRy= z>sllQ_UFuyg**%~$JBn{vPYe%9X~4XHgnwfj>nxCZ;$HUA|M!7hNOp}yU$jb zZ#wH?F_R@XPYg;AI&*nYe$+Q&vgu?*I!>LpD}F4i36aTeWGU}b z*#l~2rML3|G2$lA@178Dlc@ytA9xufrs9d~GlJXo4iaIJ9d9mQ?n`kDHZExlmZ53 z|G;VPh^XOt-pr$zM1igDr|g0~&PfLMI7M8|M7{Y`E)I5YorukZot`qBm*1dM7G^*x z&xvxIhT>@Sgge9?5(HghbG-IBh^-)CGIlvKI{2v$bb)u2@4^))5>Y%ToK`P)D(@b(8P5l35MSkX*HZ_3 zO4Y*RpBpRtm=iSG-sVO}L_8AXvC#1oeOl(Hol{^TT>XJ0YNF+2lWy!Vt=Nc87EC8W zRV2+czQr*o|7g48E>&#NamHiYz-64+jeU{8ieFLrBvdwh{jr4(^_h|u65X_U8?$if z`Sf#jAS|5+s{sXKPp+`(>VoNP6GH=bQ!^+iACW@61L&oD^{TWkJzf_lUAG>gir~OC z<&WsI#JssH%9#u9iqR|mjE2T=%;59gCKIIvX#eAE<8}u}#(;Oycax-Lhv(|0XT^C! zz$RC^6xYVMkcMTPv~kLOB(GvbwWfkjf10M0p|RAqP1Iw-Uom3O_Y^#MBn7}yH^j)fKe;feey_p-t%H3&Fw4vovp ztLWx4dEbuEw9jJYn$KYuzE#n9DB0@#&Wh*u=Gs_u)vYV1)Z*)AgTLTg=mw^8rGK=I z&rU``;~fz$?$3%4$e?^|CtuQY^IdvCj9%tFc6kz-1vXK&nKb!Jj!kXMR+%YVjdj_L zTqaS0r){LCBwWa9J&DwFqW!R*c89X6I#;B=u{tY19Fw#Z`Xr?>E6Je0@LZc!A|1bi zE&l94ot78-^|!W?UP@bnkh&jQ^{bV-g!ghA|Rl9)a4P zPJOk_ezfl>qbbpo?AzDX`B1~2ZgN!h@QwG*5x zH)`{a(|#EX4IU2&K=6#Zrl90RAYMa)Y)c?fE^^kuk9qim7fZQZ9?az6yc)0pztzrE zbp%ByWddx>(ni4Z5R7DjdPKAS_-LzOZ-4nTvTxDob{#M1@*=25 zR51amyY)Be9zr?t=8fw$C#SNB+*_9_RBHaSB!N z0ohJjQUodYCFv}9r@8bG=kp)W`OLv4d|gukreZA(RZ}u{f+~rs%EQwTe~BynIb|BN z;Qxk;-p`oHxE)o|DDW*UtIg0 zl)?X>?hfYow-V0mt{Ygx0iMP%3qYX?vMJ#Vfhv1^Z2Rm7aB{%E3g1hb0g7x(n&EZm=%y75nGtOfxW?13%De~GY+lcKyW+o;XJzt z_zNDXC_G1z9dMb6g3mzA5uZU5{B&D;QJ9q-;ERL<$f06J26pz2Ad?ClJA~`s|Hi*B zRoWhLCfqRohKwNcT@Q=MKeW#Wq*rjNy?>PeXN{o9`h^VtTsGlEiQh!}fG+eaMdA;| zvG;4(fJ4Rwk-PsB!8x!uzi0m2ullM3_~#)b0>}F!%so60Tm4Su$w^pF*4pxRn zaH;~hrhhv}4z^zw05qBz+1op-?6Y_SQw95}8Y2ife%!2OM1uQ&1Hkfc=j6jD>0hy5 z{5#2xzdG4J5RCI*vK{|4Nf&O956S4C*GRCk{G8H>6(MVf%mf1G_QRNogDn0-Q}Dku z6To`>01%%89N%Gt&jHiLU&~5-53jhNBoDmyAuUAEU;d5+ntxN;RknX4l0YE7ei##R zfbjccOaub_stFHD_7SfC2L1_dSH1#azu{uP!KVIG{1d{I|K)82;*0)dW8n`n5`R}l z2v(MZ;2p70MJz531rqEBr~EKhg5{5ge;q<lJzvvd8_?Qnx?j0b+`R~*ltk5V+UHMKN?m-yIUcJFVT{3bcy zchID&E>Dqa6s8P5c9qtwQz$S#NS5{5JvZKSaKj7 zf6ArH?~CTd)19M3LLzE8KyxPQTM&v9G@w2ZK8^F`zNhDho-}L{hPa1eB@#&%&St6A z1jjKbg*_q2=$Do5ZHA=uK=2+cdI@*OXlx33Y6+>TD!%f?BX_^+j^~Hwo7xZun<$UX zEiJ7a8hJeu3)8#z<~6sStI5Y9XklgH0}>LFVcPv2{l1Z0%0M?sZ1A zZ3K+6DW(mWDBypC3~Nmkd8i&vinS&N)Vme$dfbX9l6R|e46RO-PuespVp>FffP#vEKj^c z5z0F*BGJ>^n?4Itz;fQa(PKAmJL$0YPU*6`dL}?QuEPMr@nK=1N}|AREp~SH*_!)H z*(#a#{CwGO02Ix*Ek?=T@Y1CafYaM2`VYFyN)uwwCTo;62p&RSkcw=f3X9NQ%m6sTHtge=KT_lla$^1ERUVjq*&7LCD zZdH)|_wu3K_+WuifL6I}deWfqBSq$;fpj_rT!J?iG%%t=9wZ46Cka{I$x&`7Hh6xF z9Y77jvn=0K#nxTkxG#w+MIiwCBi1=-7Gw^R{L+iFf`hRV&r5&o9UkU;S2j}VXs#4d zza`^H7;95c&czoFDWD3a8^>D-uk@!BV6}YV$=8x59nrHg*(iG|Mh*(VOAp?Qk%0uS z(aSHiVn3gTSdUiUPM;~epreV!KS=Gd0kNggSnMeABy@s! zq#L(q=jT=0%KXnreoTT?QBpvOF{0HCG7G5`El>z@uQrA}r9oywS1hYK&YMW>aTPzs znvmCZfgjyT+P!YdXkeBN-ivq6z{<7v(zUWcG`_KxiF69 z+TFR?StWpC6y+!cIA4$t%mzmS0muVM|8QvIM*ViAGmZ~p~*68b} z!$tNBqq;-$ON9ik`(JjSV+&>h=t>R{0C_kb=ds>XtuMFh{Rz!xUMBJc#Pc~93>KS5 z$1xjtVhm~*8q2?>r+5TYLVaK1Fy8}`GkeQtl5%!8=b6^Wys)wG^2=_BU#T?&sbX=a z=H@v|dg|)^UkY@%l{^=eYMwsT1&NSyDTTttjICo^aL8(#!ThWa=oU$?O6Da5wB1%tzH5_wDSJ;`WhvXMe((})SAJE?J8 z#E>FJL`_|NzSed3^GrHzdV(CzeSq|i&Pe34p;k!9I;vPu4o|VR94awA`&^o4Q6e_3 z;Y_~4Ri{_t#MuA_cv-#v9xe^pF@{a&ac|-V7ez&d<7g*XwV2)Ck9pUO*RcR_nK1G3 z*uL0>SD6&DxQyK6im1d%!NI|J+T2){GPlymb{SS`z0WHM^C=|pWkcz4>CsNMl-W$m zP~v1R$~3+>gKBW6m2}j2r)Xp9b8KrJTC{V4km6gJ;HQ~3B0^<`VR9cviqIP6tB>nhpszq^b4h23iBzJWyx_a04#0IW+x6~Yw zM48+;OQxeVnmTiP@VP5Cc|``ECdN8hZqJSqzsp`Y7sAIn5h$Bs6?um)204~ENT$xa z`s#fFjnLi^=b1z=q8ah!v?ZydkCYo6n$JgPO*V%32$Pp94BIf{jn`>Nd69e1*DOn- zn_1fkvS&BZ#~&>6@H4iuT%_)&PXR?RFr`+ z-u$XfA4$DLIXh#Kj~t=O;(6JqEPz#JmAN3&8+1A(3?tf}@r3Jr(o&sl=2Pl1?2OPz zEZ)n<6`)hcgnEY7Y)}`hSIGEXigG^;Tnm{!PPd^UrcA|@J2)q zL{3r$tZBpu&Lr3=$BHxr`NY=8wGx+6K>9IQf;c)*wYTq{&f#!l3baP{=T3(vFNVv} z23s419(N#yw(!UfBlGIyce@1B`CG@*U@TocmYf64q?I1I92mE9j9blLQZIRuWNvA} zy3RZMs&)A}dDrJbnx|`K)XFTllGB2U?wFSe7#lg3xf<=<+_y^af88p1)kP}4D8#T= zyMbR9r3KXlh-j-Wiq$1lZ@T-wCWP;R5xm1tY+NuA9yyXGJUxPqp)P{kaj_E^K{Rc% zcEkGfbd}OxZI$G}xSG~Fn=u8Te#8-q`T#i@5*8Flzu~?3+#E2d}V%~neE9=6X-KXVrfv_wpS!~@Ypj*oHPh|`G@Vh z<(=m!x0J0>UZN8xrPgeJXeE5JDEds|7FSq)BznLZl%|p-w_Rt;<4kNoS=Z;hum$q3 zC8$+Vj3rnb(49_T!(k?<8uDp}bUWSe3&6#D)tv0QDQ%+tWqb9~tdyZHK%CD_$HRgL zNOB^CoKY0eiC@h5)8%fAwiY@mozf&O@iCB&MtTzu5F|52xYmL0lSy7D$B5r(^~)EP-oT+s0jRCvaFVqApi?w5j4p>W`RrMnEZ zz0j@##k$dbRm3k~iJyB>|0e>(4k-fwx|G)q*czIlG<{ zSRq`9g=ei+wjArQ5b`pRu2M;Mye0VQd{iP4BOj#)?KxQv$TIUX=jg2m`p>SO^`HX5 zL3mno$uSV~*a)SG*J_T}m9FMLJ&_Se{~D)OWesHO_vRTqIeb@&Wn2_pl;D2G?dEAN zB^fqE_BEluV)UkiP^tNeQ{a!+5kadhmr&ivfe2{`Uz`(m+L>!*s_9|UD}?vMiqFV3 z-s(qx)8q3*-f=|?_3+G#L#*IN=mSypy)jQ?EWIwwT$`vo+QT-E4k9aWA(b*}jTAY2 z0eE2MTas7j(0f@u8$^Q8$pYqvTTtQg0vwxUZ_LV+t1V3;DAIV=5@q?Lw^URjjJt=1 zavB0pMNnnp7Ek3^8%#EI2y6w`v8TRz?$wG5&ig>Bm+U69SofYi#(ABof``Bn z&4nD6d(IGLef>P>6A+JjUy4~Acp?ho^m+Od-KSPZ!;9OK1@#>q z98T~EC{Rm|r&^zP?kZqN2fd-4eDSVVUH>w`C3mgl0Ed6##0jm?wrtAkNgr&ZH2|6h zF@4k=|Mv3p%~mJ~UmgN{$d{)zHExPeeaKv-K!9^ExRE4?)T;v~&QOif529Q9gcuv~ zGc1n;Nj>U-He!1ES_~FRZ&6S53=D8dm4z~##eeSSg?ctD_6(1`)YJ7o?YX7O_iFt* zc0qW!&#QHDo ze7jtGAB{kv%&-D&To~7;jyUK1B%V#(Oj8@H^9Mr)!x(`HE?oX?;w_1B?La5-qYi2O zg+XN~)8-BJ-NBG9N=2^m@L9^f?B&;VhFSnfez9zqu&qqaJ)RTR7aK_$A}3owXQ+3* zgT6qGx{fAU2=g=nhPsCGBT1BTGy-KXtS@bnp|J2ciCGY4>YevYErUci!q9RZ47AXm zU@%S`F-?$UxS%tB?3{U+kOqo30V6K$t+Sea`7I+h^p5IEOBh$%b1Ew<7e?yA60IHE z;cf2K%a1w=>2D%XBUMSLthTNr5lo}^_^Y~V8iuDv7*@FBETpnM9))^tQ$~%dJ zh2hy@0!GYG(>gj3C1Z|*g`v9~z8+j4(Kx+ZQ4Hgj{e_I{>iK7$l9<>>LGaFp`dOc! zd*&82?seRW>gvaZ*JdLX4yG|z;;DpCxX+H=u^vx1zvVjtxEcCjLF53MGfwMBghzqQ ziOzLaXG8e|2wLs$03xDHFmVFKHx{)x&q?G-xkCzG_fbnP0g$!&fZ4IUFELZg&tN6WFZ1dqpx2qQ>=-cDtFEmzk=TMS>Lz>A>jHxEG47+hd`U zPgfdFyVNjje~E`xXIs_m+iri(k{flAhye9Po4 zGM;t3JGAcTvpP?{>V!5C6sj=}WRp9Xp6swUd^_1J@GoDA&UH>_`qnHE&LM`9U zDYhQ3>-t1Af_3`oIpiAnSpzp+U!{uAMO+eC;Z0pjg78s{1zO+UUTHLI4nIHlAVlB; zg@t_>WSbf^3+FGtoOqeYyAyl?EQqktz~wlPrdp$Oum!g}z4Bj)<1NE)%^OAr2w9XZ zN2^EP`*z*LTXx^SU)}%!t6V+k4hLYvkucE*DBX1hG^8k`D59)wm~ALtQ&4zy-(?uQ zK!l8;{$yh6GK^M7&zm>AkRVs8Oye~*uw(Lr^#$VN7uBk&<`}$NbF3w8qv{CJiRp%@ zz*17WQlkV9pYc|p0|20zfrYON_C^tAiBZzG)e2$z{iW*LTWb;P&j}ZpShy|iTQwLh3_Jl`2hyJ^!79Q{mCEAD8U3|@IzkSf-CkKaU=4Yb?$@=xbjD+}VCLFi z?{GY_E(Xc+mvhu;`P$|{Y-DrF&-!&RlXt5ii~ocTWt}&^>+1reK`Wpv0xN(SCu?qQ zPSwq=X1CH%f)Z<>*#K}nZI%KQ`a8$tD>$^(LJSi$G_7D8Lgvy7S>9)XvJjc-Jm0pw z;$jCy^5yq0Un&@VEUm8AGa(1Ap2sT!g%<$o7;f^-#eD2+@Hm31Dk|z_MNU4?d|Oh# z&hVx%d7DK+g4=ay(=&om5Lv?%bxJH=r;ajm*-YHIflq#t#TlF3x}~1j!A6+bIUvMW zTU|Yx3=@#Vr~~>Q2HoQqKc6h>2&8MtUQizqyMx(uQTH`3506Z2V1gW1EKBi=o}-@L z5d>F#BJQWBi(|ScAZI|GO zA&r+2t@U3Ca2H>76`M&)T?3~m(c7< zWBt*jtzEAAW6d4wFKf#wH*^&q$0+f0BL_C!L^q?hMU^FnKN4Rf``X9To`lr#VONeG zr5P}0ZpHsHffnuS7V4-m9-i)QTcz*%d8$kN$njQmW=7TUSv4(_2iD@;kvk9L{jH@r z2*mtym}R=^&d1_cUk{Q)(M%j*_HJ!kZmagIjd>krh#Gz(PJ}l0zM+k97`lQ_9h!!s zVmrsP6P|i6Yq;vppXvbq>zM9^I*e#Ez4Be5vNrmQ7n*EWaJ3^Lfpt^SC&gDd2*)vu zb&P#3${S4>>pWX%En?AWU{@WzD9_@hGHCl6H@Gp8D;A@b%{FUHwWW;Nfum5i(2A(e zNTHjBopVctc9Iq56cf9kXzmW|b!LISg9>i$*htP8iTb@jOwG`8J!Pw));4!TVZ8zm z85C91AXzKME1N#M@#Z$q);ZR2IcctF&&jA2x_#MIbY!P{Y!fS>`r#6*jHZgGqN>)- zz<8Tj;*2Y9Vp0Huiia6N+sEv4GtK+p=2^yBGm)m2nvTA|*m4flpsy^{@{xW`wS6NV zjiGgCtB62C4T!pXKxFuscw5}C&4JK2VE8C|=EZ4(OBuz=u&y-eIbf618!x5~Up`)# zS|r&u@d^EGZ>RTa>x&iFB=Rpx+K~X-z)3*q*UfFK+-iY=uJmGbRnSa*?Mk@bh&8E zlxUu@1o47?Oey}`lXkk7c8R8(x;eb*rpMOC@^vtyQMYHW9tExpE95@d0_Ebcm36O+ z9O^WD!-AncefA3K-O7jIIOq`c7kG?UsgSiHW%6YVUG)X{nq~e|WH)7j@umbOEkG&# z64e+N_-zt7L;=y*!@RspK_p}9=pV@FK88S~D1^rJ z&w!{RWUEsdfO~!gL8PZ-=kkde3F!m)_dNs=`yPVm1M`WF?;wcmK(|4A2qN*zahHCM zAoBeOLG;HXh%QN5T0@mBpfZMR28v=1vW_YYpiJ9;gdk#JMt~&{5F!r5d!>IMM8Ad+ z`UgU^S3vbYgAf6lV^DP4;Ho0b&eYx%W(D4OALRWXf`a@}HCb5@sS${*8-!LIN|k^j zGI0*4-LN3ooBxnJf9L^p=37;CLo)*_6WCP?n8`0N82{ji08EK>FH`B)6nj6%{#&N~ z*Q$_TYhV6CP!RKfNKpR45s7>Q0_~>{e$CFZ0}v8ZD=QPbLq&SO9z4``2uify!#`!K zfx(0)#QtF>5&>U8m`DWb@?S~*-xml4PyG4giN%>0l6;us_C9{3r;(1ANorRC|^KoXTIzRv+WJ4(Ciu(5 z6Hr93@Ec|Vamo)9Paw46N01v+D^u_qWZ%O9!im329Lk1RocxB|AWr#T%gyhL?ODKy zzJ`QEa1w#-k(WB7PSAKsL`0hg(7t+p};*t#FT-Cy% zWP2s9ycs4UO57_i9VQak>3zbr)4RTGmXbnuprSL#gYDSq>$SFw{CS^(-cP=X<#lX` z{i5Al#rtV^i`UWBZu+GLHUWnB$mh?_bu@Y?-Xb1=LV4nZ=Nn-ph^_1x67J&C($X}M zd!8RTm$kd_7KRTO1riGPIi%Q|GF*mB=xlZFLJH_Y(@A*Pu{TkIm?=V4P(sTyUx-d8 z37tER_Vyl<;S%jiXgN(M+7T4)^YF{~e)P+(Fs!z>%L9UKn%!1ze0+R1E-tCEfF{&+4>gL|7*_@*TP=yEe_2Ooh zmX|H3xOP7+Em_4SCNgaCmrk*?wY4dNAy;0Gz3{^?%Fj>P440|WG0F|O^j>m_tFOO* zcD-L;eTNOy>zEXiC%U+r1+n1+H^7d^*F%8!-Ic_etQHI z;k$>VuCCsDG^M7dW+3={M=HO=oNBXLkd&i^Mc`dfWvgmxYAUsDeJ-8`e|S2~wX&k3 zCHV4Ny`1FiY+AwyLB5YAB^*tpGuUYEwbc}gD$~l5A%TINAoHu)utO)$)nPV(URz7+ zp8eucQ?)@ z@;U~WRm`j`OOlh1B8%;$u!hLsGErKqG*ShwG10~B;?p;=dWMn-@v$*&zc+`uQGI2r`_&y2jNa% zUsP{WdG#pS@M!4>baY@qaq<;;sDwOaiDoL^ixr?CVI`UyS9at>%wUE2kg3PSu5?o% zJ;feWEt#k;^A)&Y=S=6#ghUN1Y!cuLktZ{28KU3RW-WeW{A%mj5IvWY5EA4g)FUp^ z`EhgkM*95}d)+ln^WkUjbDqv#9a1mSoPjTp4ea?8*3mD{FOGv}Vyoc( ze*5fd0uzcHIYj8n7ku}&Pi|N&N8smDG?4}%LBL6@LP)F~LU=5o`T{bJd)t#^piPM4 z2>5cxsc=<58xnF8sres!DqXDIwOgsU10y3reI7ULqdts>2S97zp+P?4BAdrfreP-Q z-dzk&p5LwOpdSp=jFn`=;39#f;<~pzSSjQgEH2XdWWs*RC#aAW+efOC5KRL-iN|5y zg0=7?#IDA#g4h;-%>^UM{hjYBfD3Zmae zh99X}h6da)RGY>PLJ2VpoZV<=K8$-c-_%StLHMz7ff~-GjnoeS6sF?o{%(xN*D^Gw^mPpqHWb0 zvFfdvD`UlX4Q6nuiGqJH6zV%^0TKeMLoL zKW6f~J8zruFZ%_&(45=mc3Hcwl#+4VCo;va&6`-ibM50ZzS+Cy7!!V%*R+H0=k1Hgd_9B0wkZVPdc=wI;J5@5n0zv8zWPD+^3fTC$^4u0Nt^6L03g8uSv1LU#%) zM;aa;reE?dB>4P5eVXT;7PgRsK`VzWc>WLDK3fS3;9X+m*mTUmOdC$(w&Mb2RtKRq z_Op1Pz+P`rWhY+X#Z{Tl$8XM{#GcM1Jt3Sz=`mo$h!SlXB!hS3i3yp+^k!1|LIBLt zGQ0A~n{JSFcwB9zWd7kG@R-Z1W6!+QhL{Y_pKS9RzPKkC0*4B;Z|UIesedy4 zng%c?%G)@^5XIP=dPnwJJGlJE3s^(aHqJ7lfk>|3-Q2W%sRN4D#78^k8!wI0H$)$s zHfE#D=*E3FB04femQmBd)7jnq1TtOhkbPhYa6{nlF1U7A;~XBEI}+Lp2_)8#6AV1S z$si%SK`89p9^sfgXLw-J?VQdm3^;*%AE{Q-1~ zJL&sJYrY;8ozCsQZn2ZTL#42_p+iHR{h6AOb8(x(u4#CkBO^q}=Bu!}A(Ej;Q^o zC0&Q%t}Z;#S6@qS(tC#iN*l7(`=UPY0EpFoXVam=dfd~dCZqW@DXA*`#)A-jlh?0b zOPiaU*S91z)?BBiBKx&*SQ@0`%%&mtI-NN-C0 zPCoMlx4XNzR9(CKft4g+>vvY*RdwG7ciG6c;HTDH8qYx;l*HZI@CT7BHc1#Muxe_J zcTGOTlv|eO=Hr7G!?fb6wwqmEq@)zNoEx&fo#`cXVW6Q))b1q}1F_X$!Bu=uo&%dn zp5^@qoQNu=6HsYqZmQjv2uMRMlf@5RJtxM;rN@^oL^m5Pny)p`Tc-6?XkxRqB8s4g z{ju=al6z8a7iF9R5xgG_cfQWBCEXMFM#qA~p_^R|`rdH6QzSP8L4Qa`|G;IDGcM}Z z<2*LiSDs3J5?7m|@S9YK<_aBH`0H)jlOzgIhYzP5E-kCFhkXP!kBlxnc(rqbP#d~z z8I1*M;wLY=GD$D)-Zv9|(|+JqH-NPcy z%J1@s>#3iGSZ^Z;F&4L%26NLKl;oCWAJ+@t26cp4fI>xbvMfGXa;{Zfem*Ag9P~_5 zkq8F7{>XB+&wNH~X){a$U1E1MkcOGI4BA5b(k?g=&=H_fZc&D++nqN&)s6L4@O4xw?s93+CNjBcS{_p+1C>FecAIB; zP>8o*z5#f-9Cnsf4d{k6j~jE<8c3dX8EJJo8bO%tJqGN~>0gI8c%=YR9<6A_f=(C- zWrXWAdFIuUXM|Z+LtmDhS!0r|{3EJvg@xa2?!6(K609t1HNfiif6;H6(wrdk5-%FzM1T;u}=0`1tmzEj0 z;qsekVPnu$=*omz`2d!^0#)&wUyVds?Zo=W4 z&Jw(btA`V;hsgzjfk(wvb78KRpI!>?=1^+OR`2PGvhVd+zF7Od&heVXd>52!88)*+ zbFtLrxUoWGd&6>`*pdv)lKzo{cxXp>&)z3$JkE%XBQ>kE*krk6<+21uU1I++P)F4ym`G;i=5!?c5+VV zy1ND|>}w3R2z$rz?^(2bqjlFiW*fb8l)TNY45a7c4gz^1>m(O`TGPm?m6otmL=1(5 z3Ip-fq>VT`!Wf4$o@>9Qbc~LRFy9bhr6GK2#E6jV9>9E(89h-iAdEjRF#f#c{a6UE z;F>fNrvkxk->=4>3>bg1&_y)FWiJC^vD=Z#xR5XE(WgIa@c`IEoqN&T;EPW!Fa;2*Qrf0LxRzgX*US;hYyCgktKuk$xbvA46< z2j28QUuqNAA03JR3HIP`#JFMr4&wmdmyOv!q3m#f_a?tPq5l(P=O0SD@{?*s<6B#L zglF;Zwm$#bFa105%2ylpm1qS)Rr}RD`R|}z`CnO|@jY7pJA@qnvC73i;2{5V3Eci@ zV1E4lcMdqd=j#7sywKlkiSIt?e@sjK)C>Q4E%6Q6`ClJ$e9y4|$E^1E_5ErQ{*S4+ zezI@Le~0V%4U7BVZxp^g=-9pjd(;e$Fx|ARXpSoStijL zApu&H>J!Ux!%>mbd}KM^F~Y{%=d^OKtKg^Yv)}GHc1_-bth;SY9rbi22yVEkTTOLA zp}qWDbtd!m&L)zniXY-rQq(I~FEbKgnnNHx5ZYppPosFPSnPTk_z&$`EC%R0>?fiH zL7NBJF)xtbbt|!mz{q{>a#7nPApTiJpuS#@?}3Z;`e{} z>(Pdg(IQ=S(3jlY(NQ&u-{Y_pq}q?1g?c)H(t+X8(d?}3Y*ohnLqe!7!?j zS-tR8VIkXzbIAs*A&NmE7qHJP_jcRHTwLkGN2?_>CZmN9nS;nM zx?5WV*p2|FhS^6wTN;klr6nn=1yCwcebFJ}@fzr^|Dhow;*!xgT|aGOrmd`;+>&em zDbxT~=T@<~XOm4FrNhR^aoN$Cg%UqQ<$1C>+$9Mdk;VaP&AK zk|=LycZG2AAD->+R|+_>DmMxu zGqD)rC9>*#+c|s$m~n!UrRKU^SxTi^&)GI#L-9GpW4u(ccsWD@;^Lx#^0{4K#B8FvH1Mcm~)NazMHO!VqSI;@za8~k4`b3l5#wPQC2 zqGL1WFY}Jyr4uxJ7s7c%XFZZ6K|`DF99LZ2+_I7r=?P0mvR3qkcimQx(r@Cu+Al4w zsnNPEVrp)ld+2aO0Lm{zMnb|U+q4Q?{68n+rAtXUT<3Ln~bd<(kMhweH^raIrUIiz1w}&L# z8F*MsZZWeD+-hCblU8=Alb+CJ0ss1SVbN`6eV zZ+}rkAO=V?^Rc{|C_WUR?rDDnG)qUijL5FT!#;NirH{>Qti(qF8E=n}OO<^qX(&M| zn=dVrEmlt^2_8ssyX0L-gNu0W8hCA>fP?_N_6~;w@%?LHAmNafm*8D$NFX9-jv7M* z=Ig+G9u1lLkp!%rvG6f6SX>blkXq;MeG>@A)Vk1d`H-l1ItE^)92nOP0%{1ZUV2Ud z15hp`$auU@i~CiY5l?`9E-*ptgK6$Za7pq2poVBHxraZa+R;HARHhM4nuQNOMRkRy zi|H<6s|f^izsFF6lC`FGmAw>uh<8tpns=tvBxwL^5tQjDUyNoRa=oQHA3y{7{xIv8cDKE@UK+6}WKV$V!o^L8_RX1@v{T+Bm4>RSoU@1T`#pvi z4Ky@E@Psd%<43N-<0XY~+s_WJN=i!ZaF&3E?f0Qjs1l3oH5I`3rdj(628suX>Oj?N z87M4r+L(H%9Jcz|geNv1kGBfY7va6SOI*YHPK- z7So^)5-V9*SQvZOoQ_!{*-BqAZu&CSGuns z5xia{r=URmB)oH+pemfJBD`(>y`jYIflKWLl@eSPcFkFKrM<0gcH0}3nGEc1pel1G zN=36tT9*b(q}?DY6y<}x7#NhAS#Jm>`RB-(627T0uD#(!YoJ$yQr_ATnXIJVX-CG} z`~Z`cD7}i9<+~^gnT#fS^HZ9c*_pN4nRWd3SImWNrn58s{QPcu3};08-68T0Z=)Al zden>Kqx~#_?Aop-h6d6d%7nT=Z=(btJ3LEwEuVsf->`1Iz?auWff$Is7?yp4c%>j- z_e)g@_zdpJ%MZTX9;4xwEr^S8>=z zL=l@AC!oz8H$O?8Xbpl|K-Di9M}SNRK{GcDlIXKR zHEJaOosI{{)6d_2P68h`8`RIF9aZZ*{CVA?!2#E+o8ecidt*uc%&``&{?R*Fv1Somh`toB?D$-oSKwfiCKn z5b-sTd#wsGpWXUd5u8(p;F1w#80VSzu$ug@+aikwX=(2UTQR*KF>J z+!&MqRfRdjJ(dBjMEm$ao8&O*v^L=-%%Nu@#WSrMXmzK7$;tXf-8%kC;7hyvO?EuU zqQPWFgdpEJ=nz=9h%%$#F!8dt{Fmqn>}0_tMJQ>1LA~aciqtcgj-5E!B_h=`AXH8d z5x%HNs;}%e0H28~k8>cekBgeWYSruPDRs0UNF1o8tbiFEg?rx24Owiv$)B6p%uPq+ z@}mESHZ=rUUf%qU07=Fg9mUg)xs0lN6I)5T1E&W)Xfv@xQOI){exl>aZ}IAG&~!_T zCliOG3VZ}tOz@71iVsIzQL62u8^0Obz^?m3T&Y*Ry&1Z#jH_Xai}uSEdzqyb0$ukn z61lvwLxPo&b|DEYWMpJPp&A)n`k)LrvL{=7Zon>;H?ekqCfKNfEqX*JO@2Nf?;sdg zAlt&&_%7)APYAD^T~VZl1eh$-K(3indG4or65{jE0u6bOoOvhNp)8P$Vvxj{&uOMn zo$*C1*2(&dLR!ht#Vts>b%>OOf*(ZOo#~*vuc7N%ti8G=v9jIq+{G6f{G0^-`^Ho zjMN!uY&nJ!r+n;OXi;vcuBi!jUN?0y7UJMYeTp|Ht3hh?F3UPiSWaM98u5(ICxCVB z&e6xBhErwsx^3lW?XQJ;bU*=&!`A$jin-QKuiG}$-iT_Dv}%V|wlYb5X=3|`Ag|B~ zVx7mLZM)0M=2E)pw%u!6@QakUUa>u_&RC#2N;rJhC708+l*_MS>iaq4>OR3e2Z3XJ zgN4DOzBLxLUfYi4ib>LM@?ObSZ84U{gW;5sX)RUDmrnNIA zVlp$6uI0kB5>rKOIXSr;P;%)A%0I2RT<>>~o+$_)WUS|1nFYK&0)cXpGnfZubHU6*ra zaA>LpUcKY8IHnSG%KtP^S>f@>*g8=tf!CQdoj=sN{@|_<@D^P^e6XJwm!{A)d&erk z9~ngE7)1YYJFR9AkRd&c#If_Lv@E!)e%;>fbK`~K?gZZZ93?STF2AD{G+QfFY!*xff^_mO3=oV_hJ5Pgg%3eBtFklcu`Ubtd}3;mj`kaR!!32(_AJ50e!t*WvIl+K3ingh4Q#`k3og(|oeWM+41%;3t>VBWk+Ltg31d94FF$b$U*MO9oZ zUD$m!_ue(9j7*$DmOBw`0t4C^YjY7f{*|g^&gnMtM?wnls>eG*73QL`d%c-JOX^G= z*qvn}6le_BJ60bPQaX~$FJDzJ#Et}LP$teUN!dq~s|WXbiLFE}us& zI1H&Kq0dwZ8~F*@2%PRwKd?==^sRT1j|%gY7b|+)n6tGRf!4DrtEUto5Ek9`BiZcy zO#DU@I&k2p$-qNdQ+AknQA7A0wckz#RHChF$BFq>R%L{gM}pzgrTTNK0#7UV>Ya0K zzyk~Qc{)!1j#Z_1-dP*3;`p#9FIfy$Yj;7wPnVJUd@Hdy_D*00G!0Ow_;YfGeo&2TY_wX zTAm&Ys=YK2ohp>1Ms@-`6lL&9b4?lBXcHsi1+J6C9oE zqlU1Vr59WSmzQEpl~o0cm>a10mIF+u!E`Fm1zW7)k^>6jcdQNdTe%B(yhSjdo)3pl zkkJHwi-z<~-&Wm{mg>>R^`pr~%{PQp2Dcq4#s`-QWk;C2=CFB<&MTQk0|8{oLQCIv zvd!xtUfx#l+Qa|sYkqhYOG{?MaCi}$PVFl|u6?<=!EP=HP(dk!b^zv&AM@GP<)dXX zB$d|X&(63Oy2P#&S!h*Hd;G={ z0$Jur5%#Elp%)SsmInZdlfA}9(VP7IIb0QhV{dqDEC-jI?}Qs(2sA9#=|JWf+s} zFtGb3kZcPy=wa`GbtRzLr=+A1*J(V`SXN`%9Eq=(X{GM2IV*fvQ~J59rziLIYmO7^ zcopNjjZm#r#l9xuZehmR*=m-^tV$0A;Uw~a#1-m^2%G#;5~+#zS0O4--{pbcHfk_N z`*|{~7+c%1`5Ap|({v}qBkZhrVSx`HrKd#11G%nk_>wbA}e3h)hiVg7RIh7jK_=M z@oE`v8I5Y$axoAI-MCRwdCd)?5toTqafbXt;+aOgTf(tOYGUQ<-b%v zqF(S!3xdwK{%SipVRn5Xi(jgz>I%1j<{c2z_;4WV^M(L`SujN@as&VmHK0QBA3ae5 zIE5=f7=|ZMOhJd2x0qn#66W8)1emBb*vqbNNNb>a@Bs~yZHX`8U4V!#1E509`r=Cu zPzlHkcD-706C)@CtOKfXdF@>3MSyiY0jdjAjRfr+-YZ_9%;2visI13dy%k(X28ayz?7!V^m@jax{ap=2GQewCAZx`?3%xAUH_8}vofq#EhyvL6y-a|t2KJ)r> z@g8a@64HgsUyJwl8S-v{n_$@7(*WE@IHQPv=k72jJpN-vAJT7l_v$28wED2e-Wfac z3?nJUWkQLFXquaZAy%f>LpYG3SshfXx?9g>waf6&r@7ep*g1MXq7KD}TopICN}SD& znc7NF(0bL>-CPn|4)yg81_=hHKDYO!tLCC9J3>whJ&F8r9-eUrN$-y;h-8xB06je! zH+e&p;=U(5DZCG8vi&b2qf>h${qYB-4+K^67XDwo<%ta5A^xj(>Jjgd{M9?)b(Ft) zeHU4Ud`zqV@;OvI5v1S!Xpcc^R4R;3=(zgl!R>jG{^tLLdC6 z$wU8uh4-(=`o3=eTCJY~@-HF$Mc4dmZ~daB{-rX0QNI6D8ULQjVD@ET0UU@1&!APyH7x?qJg2I zYs~>TQPtJeY!&G_h<44BXNJr;HVhjCJ*(DH)ID2WrrbZEZ+< zRD5E;Ah(iN zY(3)eQ5)J5-5NeUf01$(RUL4m=jUsbv|d3R5@%LbX~eQUWCC!a%hO_Il+4!FyJMwh z_X)g?_L$Z38O^9oJq$>lo&i53?uI@7uO2{qq<4jj%4$`oIO*(k9vxY%1%(qvn%t6}W-l3K%a(wn)>Bf{2i_2yrR zs)7hsz$)GTso|kVYGY8So0PMPIn?1Ok^e#`8F|&j8oU9H5k$nU1EYNN z*pImGsoKRQ7iED~I+@sZ(6kD3+Hx#?ksiXJLGdyoAWjZEz=QsZr9W5^ab&Ka9E-R>_6B0OhbBB2YWYvW}}cj8`g z`9JRlpg&Co+_T30>}YJj*3rG2`FO6X9h>L#;SC zzpxUGf46R*j3Zz9-mN4?P3L=HmK)@)z1#qt`)NMG+Nqkunqkmwv9U|ZnoQ7X^@aP< z1UweBN9qJ;FqxpCfJI*eBtH>)09< zF(Fj9r^cLiTG&1N%}AZ|#;X>RnL(<m*?gu=bpFXtor;8PUqxv6Xy$ zpl9JUV){w=rH(6V3yyp6qp7{|yeF9aAGD|r5s6QTbq^0b%p}+uDJ|aE zQM3@6kb*Ds*v^PAz{@Z@=MBiF&2-Br4lUPZ5*$G6k!ev!!uqSieuwvX<@3?z&R9_= zSFh&-y-z2{2idvqUWJ5`%nKK&3$7xpQkDP)QE$_woi#&f^hq#xU_= z3S4AFr(LXH=O4S{TJgXP>5b8Qq6O*|AgldL^pKVa%JEg_2hM#XY!e0Mhq1f~3hGv> zgRkF>2@sX`Oz{>$`Oi}rQZ6L(=#)w&gLU2<@xS!Db;dx|dHm&JmFoa=+Q*24=f|?v zRhwAZ?FTpOGT=}l8JoD<4tl4^1u<0Xqn%H!lHg&`Mj zJ2aTbf~_-=!!y8?`P!hZTvzR$O$M!+nQXY+z~(^HI`&XKF5Enm*XB%F{8w7}7)a zL1x`tIPa5Qgme4U=XO*vrKf~Pb!t;iH4zxMFmpclf2l#q<7@Qmwi{DpKR}}))zJpW zkMd>x*}^+HX@0aDLS0oVniyFZ#PiapR_5Y#WBE+TC6cU-@X1}0j$3n)=)IYpZ48SY zR}cGhS?v|dT$eKA4EH~Ov^$uKlSy2I@bqF^MKXX8wf3f-_^+x)G2`v4*2y$N$-}A{ zFp<7lT*0gTr*<13-z%j??2@aN&D6UoPQ3KK%7VSQtGaA&8H5o^>GJ6kLzhOqdlhg4 zvNIYZoU#nGDs56_6L<=Q!Zm7~3KpTFhfGH=!;WrczbYvx4SO}+ZtVeExnbHzy-@6c z6B2++v5d{k#P|YMSyEW#QdS4mR-BVboMx2wHuBRnwQ*Sez)>|9tCazAvg`)sc%}B%CxdZA zyI_dK0*&HFKGl=s z$k}zR)@6%aVWV7nFTR@j1NZUv`3IsVG-J1jOQqs~NLP;p_y0r|G8Cy-ci>8c&vMoJ z>&vRA>yOrh-Ek^TpD5{86imqYHa!<|+ikDgn_|8wy#L75%hS5tPF-KzgX-pnetpAX zfY91gy|eA7D4q2}-5xLAq~sm3Pm2!(p=iSs} zgNYUD+tb2%A#|lm^zf*FCQgu~93zyJlr(WG6_#q$=QDl&!%5wq#%@ja(Q{hsaI>T5 zeQXCn{dMTl-VOYr{=)5iiW@2~oY37|W)FZwzO0}?dkx(M<}vNUI!NKu!l>zO`f?t5 ziq}HI!`iVKnRIFFuGe8EaT4?UDv557V!OmOf%WhXiqe*x?JpL2Q-}5q4Yd>JC4sH6 z!A*x)SrJa~21u|fskG15O`&EA#PQqD^KJ1jB!&_ii0VbVC8ysFvD1;+z|pSV+d2{| z@yHuLJvkD5Eb(x`B-1BKO%>Py9pIuSSy@%n$=)V~E%FFV%4ZX4zRt4eoajr#y}vhc zmzYSu>dd*Z$dzMp%|$@1!LKAe8K+($46Lo|NKnKtYpe27r7>cURk57h))!}uYq?l= z@a~xz6KmvUa-m)8r*kVMx{jvn)pINd$8>d8P^N(%3IhKm-dP+vn1y+A5r;u*X^PKm z^7FletpNTk_6gQ7+n1OHd;@EShq4k@U$97n*J$9PjY^5luV)`e=ULID#yGQho(X#p9OaLtf`XT>Hl#6S@-d=eUZ=wqf|P&>vF z5oM;(F79Sr$pPh~%bcVhvaRj2j=(7xk( z%A$JV7zx}kj?u@xI85S_6*Y2DP6fEZ|C7Tdq&v6HEf9M=TGDU!G8^kQ2Vq z)b;~IRnhUDezj@w7X9ZMaI)bMjeIyS>8U42W2rPOK5n8yMzIxt)537BGe-I0!!_(? z3T#A5#K6c1{cjQ0@Y2!Cx4Gd5kBjv3G_MmZp@2Et_d{yDq=52^Lt;d5W1@PSXq~zR zHo(Idl2%S=q6o%`Ya zH^HS8?*dc8IWvIx{kvaF`f6xoBw3mHC3}1Od!^Oa(lCufi zzx%^#q7(But)d+4ojum_bo>0`c=xT6i^my<%UZ=5>jn6&k)@_8-eek-UO~!%%UHKI z-_bzp7I!|>)q8yM;CEd`;omY4`jm5YoASt&?W=dQ$S7j-4&Ud8KJk8ghJL)>$8pfF zFlBdoyqW1-puOI(qJ1#LpipcsL+j>qG}3V09uiDN4i!v<9r3yBe)QSp&7ec9hIbKI z?~CVuS(vMD@*NxSN!C2p8a#v!_g1c(c$P|LZ@l7T>YYs|lq?}Sp?6xaraBNQ)qft8 z{=q>Tv7&$2+S3fhtH~$8>yK3#iC#krB$QHPk_)20)DkC8ImRV zvB}>u7q9DjGH?-lR#Gf0oZpGud4Ci~xb)TsW#GdHMqu8+j#nRWE6}QtAIaCC1xf1B zAiFDj1GF`M?|lwM+OsGUxAtAqCGr@O=yg01>OBO$?}du=!=2n@wi)(U|FqCcWs5XU zxnG$$mA!T4ZsyQfm3=k>EtzQvHd^dUeO*2B$P`v-TR)or;PoB0`u&;R`Z6UzI0B?t zjb8QzH8^vnC>#{Ysj3<~JRMh%kqK5&Rn^eFpA⊘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≠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 literal 0 HcmV?d00001 -- 2.34.1