From 58a036283014b33da06cdb7428a9294c89c5edbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E6=AC=A3=E6=80=A1?= <3093609022@qq.com> Date: Wed, 15 Oct 2025 21:17:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/DjangoBlog-yxy_branch.iml | 26 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + djangoblog/.idea/.gitignore | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + djangoblog/.idea/misc.xml | 7 + djangoblog/.idea/modules.xml | 8 + djangoblog/.idea/vcs.xml | 6 + djangoblog/.idea/zyl_django.iml | 34 + djangoblog/README.md | 2 + ...DjangoBlog开源代码的泛读报告.docx | Bin 0 -> 374426 bytes .../src/DjangoBlog-master/.idea/.gitignore | 8 + .../.idea/DjangoBlog-master.iml | 34 + .../inspectionProfiles/profiles_settings.xml | 6 + .../src/DjangoBlog-master/.idea/misc.xml | 7 + .../src/DjangoBlog-master/.idea/modules.xml | 8 + .../DjangoBlog-master/.coveragerc | 10 + .../DjangoBlog-master/.dockerignore | 11 + .../DjangoBlog-master/.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 + .../DjangoBlog-master/.gitignore | 80 ++ .../DjangoBlog-master/Dockerfile | 15 + .../DjangoBlog-master/LICENSE | 20 + .../DjangoBlog-master/README.md | 158 +++ .../DjangoBlog-master/accounts/__init__.py | 0 .../DjangoBlog-master/accounts/admin.py | 59 + .../DjangoBlog-master/accounts/apps.py | 5 + .../DjangoBlog-master/accounts/forms.py | 117 ++ .../accounts/migrations/0001_initial.py | 49 + ...s_remove_bloguser_created_time_and_more.py | 46 + .../accounts/migrations/__init__.py | 0 .../DjangoBlog-master/accounts/models.py | 35 + .../accounts/templatetags/__init__.py | 0 .../DjangoBlog-master/accounts/tests.py | 207 ++++ .../DjangoBlog-master/accounts/urls.py | 28 + .../accounts/user_login_backend.py | 26 + .../DjangoBlog-master/accounts/utils.py | 49 + .../DjangoBlog-master/accounts/views.py | 204 ++++ .../DjangoBlog-master/blog/__init__.py | 0 .../DjangoBlog-master/blog/admin.py | 112 ++ .../DjangoBlog-master/blog/apps.py | 5 + .../blog/context_processors.py | 43 + .../DjangoBlog-master/blog/documents.py | 213 ++++ .../DjangoBlog-master/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 + .../management/commands/create_testdata.py | 40 + .../blog/management/commands/ping_baidu.py | 50 + .../management/commands/sync_user_avatar.py | 47 + .../DjangoBlog-master/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 .../DjangoBlog-master/blog/models.py | 376 ++++++ .../DjangoBlog-master/blog/search_indexes.py | 13 + .../blog/templatetags/__init__.py | 0 .../blog/templatetags/blog_tags.py | 344 ++++++ .../DjangoBlog-master/blog/tests.py | 232 ++++ .../DjangoBlog-master/blog/urls.py | 62 + .../DjangoBlog-master/blog/views.py | 379 ++++++ .../DjangoBlog-master/comments/__init__.py | 0 .../DjangoBlog-master/comments/admin.py | 47 + .../DjangoBlog-master/comments/apps.py | 5 + .../DjangoBlog-master/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 .../DjangoBlog-master/comments/models.py | 39 + .../comments/templatetags/__init__.py | 0 .../comments/templatetags/comments_tags.py | 30 + .../DjangoBlog-master/comments/tests.py | 109 ++ .../DjangoBlog-master/comments/urls.py | 11 + .../DjangoBlog-master/comments/utils.py | 38 + .../DjangoBlog-master/comments/views.py | 63 + .../docker-compose/docker-compose.es.yml | 48 + .../deploy/docker-compose/docker-compose.yml | 60 + .../DjangoBlog-master/deploy/entrypoint.sh | 31 + .../deploy/k8s/configmap.yaml | 119 ++ .../deploy/k8s/deployment.yaml | 274 +++++ .../DjangoBlog-master/deploy/k8s/gateway.yaml | 17 + .../DjangoBlog-master/deploy/k8s/pv.yaml | 94 ++ .../DjangoBlog-master/deploy/k8s/pvc.yaml | 60 + .../DjangoBlog-master/deploy/k8s/service.yaml | 80 ++ .../deploy/k8s/storageclass.yaml | 10 + .../DjangoBlog-master/deploy/nginx.conf | 50 + .../DjangoBlog-master/djangoblog/__init__.py | 1 + .../djangoblog/admin_site.py | 64 + .../DjangoBlog-master/djangoblog/apps.py | 11 + .../djangoblog/blog_signals.py | 122 ++ .../djangoblog/elasticsearch_backend.py | 183 +++ .../DjangoBlog-master/djangoblog/feeds.py | 40 + .../djangoblog/logentryadmin.py | 91 ++ .../djangoblog/plugin_manage/base_plugin.py | 41 + .../plugin_manage/hook_constants.py | 7 + .../djangoblog/plugin_manage/hooks.py | 44 + .../djangoblog/plugin_manage/loader.py | 19 + .../DjangoBlog-master/djangoblog/settings.py | 343 ++++++ .../DjangoBlog-master/djangoblog/sitemap.py | 59 + .../djangoblog/spider_notify.py | 21 + .../DjangoBlog-master/djangoblog/tests.py | 32 + .../DjangoBlog-master/djangoblog/urls.py | 64 + .../DjangoBlog-master/djangoblog/utils.py | 232 ++++ .../djangoblog/whoosh_cn_backend.py | 1044 +++++++++++++++++ .../DjangoBlog-master/djangoblog/wsgi.py | 16 + .../DjangoBlog-master/docs/README-en.md | 158 +++ .../DjangoBlog-master/docs/config-en.md | 64 + .../DjangoBlog-master/docs/config.md | 58 + .../DjangoBlog-master/docs/docker-en.md | 114 ++ .../DjangoBlog-master/docs/docker.md | 114 ++ .../DjangoBlog-master/docs/es.md | 28 + .../DjangoBlog-master/docs/imgs/alipay.jpg | Bin 0 -> 17961 bytes .../docs/imgs/pycharm_logo.png | Bin 0 -> 132045 bytes .../DjangoBlog-master/docs/imgs/wechat.jpg | Bin 0 -> 24722 bytes .../DjangoBlog-master/docs/k8s-en.md | 141 +++ .../DjangoBlog-master/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 +++++++++++ .../DjangoBlog-master/manage.py | 22 + .../DjangoBlog-master/oauth/__init__.py | 0 .../DjangoBlog-master/oauth/admin.py | 54 + .../DjangoBlog-master/oauth/apps.py | 5 + .../DjangoBlog-master/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 .../DjangoBlog-master/oauth/models.py | 67 ++ .../DjangoBlog-master/oauth/oauthmanager.py | 504 ++++++++ .../oauth/templatetags/__init__.py | 1 + .../oauth/templatetags/oauth_tags.py | 22 + .../DjangoBlog-master/oauth/tests.py | 249 ++++ .../DjangoBlog-master/oauth/urls.py | 25 + .../DjangoBlog-master/oauth/views.py | 253 ++++ .../DjangoBlog-master/owntracks/__init__.py | 0 .../DjangoBlog-master/owntracks/admin.py | 7 + .../DjangoBlog-master/owntracks/apps.py | 5 + .../owntracks/migrations/0001_initial.py | 31 + ...0002_alter_owntracklog_options_and_more.py | 22 + .../owntracks/migrations/__init__.py | 0 .../DjangoBlog-master/owntracks/models.py | 20 + .../DjangoBlog-master/owntracks/tests.py | 64 + .../DjangoBlog-master/owntracks/urls.py | 12 + .../DjangoBlog-master/owntracks/views.py | 127 ++ .../DjangoBlog-master/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 + .../DjangoBlog-master/requirements.txt | Bin 0 -> 2554 bytes .../servermanager/MemcacheStorage.py | 32 + .../servermanager/__init__.py | 0 .../DjangoBlog-master/servermanager/admin.py | 19 + .../servermanager/api/__init__.py | 1 + .../servermanager/api/blogapi.py | 27 + .../servermanager/api/commonapi.py | 64 + .../DjangoBlog-master/servermanager/apps.py | 5 + .../servermanager/migrations/0001_initial.py | 45 + ...002_alter_emailsendlog_options_and_more.py | 32 + .../servermanager/migrations/__init__.py | 0 .../DjangoBlog-master/servermanager/models.py | 33 + .../DjangoBlog-master/servermanager/robot.py | 187 +++ .../DjangoBlog-master/servermanager/tests.py | 79 ++ .../DjangoBlog-master/servermanager/urls.py | 10 + .../DjangoBlog-master/servermanager/views.py | 1 + .../templates/account/forget_password.html | 30 + .../templates/account/login.html | 46 + .../templates/account/registration_form.html | 29 + .../templates/account/result.html | 27 + .../templates/blog/article_archives.html | 60 + .../templates/blog/article_detail.html | 52 + .../templates/blog/article_index.html | 42 + .../templates/blog/error_page.html | 45 + .../templates/blog/links_list.html | 44 + .../templates/blog/tags/article_info.html | 74 ++ .../blog/tags/article_meta_info.html | 59 + .../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 + 221 files changed, 14661 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/DjangoBlog-yxy_branch.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 djangoblog/.idea/.gitignore create mode 100644 djangoblog/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 djangoblog/.idea/misc.xml create mode 100644 djangoblog/.idea/modules.xml create mode 100644 djangoblog/.idea/vcs.xml create mode 100644 djangoblog/.idea/zyl_django.iml create mode 100644 djangoblog/README.md create mode 100644 djangoblog/doc/DjangoBlog开源代码的泛读报告.docx create mode 100644 djangoblog/src/DjangoBlog-master/.idea/.gitignore create mode 100644 djangoblog/src/DjangoBlog-master/.idea/DjangoBlog-master.iml create mode 100644 djangoblog/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 djangoblog/src/DjangoBlog-master/.idea/misc.xml create mode 100644 djangoblog/src/DjangoBlog-master/.idea/modules.xml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.coveragerc create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.dockerignore create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitattributes create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/django.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitignore create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/Dockerfile create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/LICENSE create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/README.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/forms.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/templatetags/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/user_login_backend.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/utils.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/context_processors.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/documents.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/forms.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/build_index.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/build_search_words.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/clear_cache.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/create_testdata.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/ping_baidu.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/management/commands/sync_user_avatar.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/middleware.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/search_indexes.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/templatetags/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/templatetags/blog_tags.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/forms.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/templatetags/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/templatetags/comments_tags.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/utils.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.yml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/entrypoint.sh create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/configmap.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/deployment.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/gateway.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pv.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pvc.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/service.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/storageclass.yaml create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/nginx.conf create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/admin_site.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/blog_signals.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/elasticsearch_backend.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/feeds.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/logentryadmin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hooks.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/loader.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/settings.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/sitemap.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/spider_notify.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/utils.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/whoosh_cn_backend.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/wsgi.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/README-en.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config-en.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker-en.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/es.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/alipay.jpg create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/pycharm_logo.png create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/wechat.jpg create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s-en.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s.md create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.po create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/manage.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/forms.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/oauthmanager.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/templatetags/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/templatetags/oauth_tags.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/oauth/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/owntracks/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/article_copyright/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/article_copyright/plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/reading_time/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/reading_time/plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/plugin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/requirements.txt create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/MemcacheStorage.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/admin.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/blogapi.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/commonapi.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/apps.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0001_initial.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/__init__.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/models.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/robot.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/tests.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/urls.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/views.py create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/forget_password.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/login.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/registration_form.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/result.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_archives.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_detail.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_index.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/error_page.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/links_list.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_info.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_meta_info.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_pagination.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_tag_list.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/breadcrumb.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/sidebar.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item_tree.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_list.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/post_comment.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/bindsuccess.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/oauth_applications.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/require_email.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_log_dates.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_maps.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/indexes/blog/article_text.txt create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/search.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/adsense.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base_account.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/footer.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav.html create mode 100644 djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav_node.html diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..35410cac --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/DjangoBlog-yxy_branch.iml b/.idea/DjangoBlog-yxy_branch.iml new file mode 100644 index 00000000..9b074581 --- /dev/null +++ b/.idea/DjangoBlog-yxy_branch.iml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..423a25df --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..9d79efe6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/djangoblog/.idea/.gitignore b/djangoblog/.idea/.gitignore new file mode 100644 index 00000000..35410cac --- /dev/null +++ b/djangoblog/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/djangoblog/.idea/inspectionProfiles/profiles_settings.xml b/djangoblog/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/djangoblog/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/djangoblog/.idea/misc.xml b/djangoblog/.idea/misc.xml new file mode 100644 index 00000000..db8786c0 --- /dev/null +++ b/djangoblog/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/djangoblog/.idea/modules.xml b/djangoblog/.idea/modules.xml new file mode 100644 index 00000000..3d4e23c6 --- /dev/null +++ b/djangoblog/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/djangoblog/.idea/vcs.xml b/djangoblog/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/djangoblog/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/djangoblog/.idea/zyl_django.iml b/djangoblog/.idea/zyl_django.iml new file mode 100644 index 00000000..2c78b724 --- /dev/null +++ b/djangoblog/.idea/zyl_django.iml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/djangoblog/README.md b/djangoblog/README.md new file mode 100644 index 00000000..e7132068 --- /dev/null +++ b/djangoblog/README.md @@ -0,0 +1,2 @@ +# DjangoBlog + diff --git a/djangoblog/doc/DjangoBlog开源代码的泛读报告.docx b/djangoblog/doc/DjangoBlog开源代码的泛读报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..c1ca3844cda0293c81f968078fe30daf8ea3b88f GIT binary patch literal 374426 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%0D$&n!lqrS#B<#vbbXS3;h@eonSa-X*_m6T`Y7Y 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 z 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 Z>&1_4bR12H^zQ)t(}z!t8BiYee*oRXP80wD literal 0 HcmV?d00001 diff --git a/djangoblog/src/DjangoBlog-master/.idea/.gitignore b/djangoblog/src/DjangoBlog-master/.idea/.gitignore new file mode 100644 index 00000000..35410cac --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/djangoblog/src/DjangoBlog-master/.idea/DjangoBlog-master.iml b/djangoblog/src/DjangoBlog-master/.idea/DjangoBlog-master.iml new file mode 100644 index 00000000..74b565f1 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/.idea/DjangoBlog-master.iml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml b/djangoblog/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/.idea/misc.xml b/djangoblog/src/DjangoBlog-master/.idea/misc.xml new file mode 100644 index 00000000..2ad41b3a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/.idea/modules.xml b/djangoblog/src/DjangoBlog-master/.idea/modules.xml new file mode 100644 index 00000000..5494c002 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.coveragerc b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.coveragerc new file mode 100644 index 00000000..9757484f --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.coveragerc @@ -0,0 +1,10 @@ +[run] +source = . +include = *.py +omit = + *migrations* + *tests* + *.html + *whoosh_cn_backend* + *settings.py* + *venv* diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.dockerignore b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.dockerignore new file mode 100644 index 00000000..2818c38d --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.dockerignore @@ -0,0 +1,11 @@ +bin/data/ +# virtualenv +venv/ +collectedstatic/ +djangoblog/whoosh_index/ +uploads/ +settings_production.py +*.md +docs/ +logs/ +static/ \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitattributes b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitattributes new file mode 100644 index 00000000..fd52ece8 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitattributes @@ -0,0 +1,6 @@ +blog/static/* linguist-vendored +*.js linguist-vendored +*.css linguist-vendored +* text=auto +*.sh text eol=lf +*.conf text eol=lf \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..2b5b7aa7 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ + + +**我确定我已经查看了** (标注`[ ]`为`[x]`) + +- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md) +- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md) +- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues) + +---- + +**我要申请** (标注`[ ]`为`[x]`) + +- [ ] BUG 反馈 +- [ ] 添加新的特性或者功能 +- [ ] 请求技术支持 diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..6b765223 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml @@ -0,0 +1,47 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + schedule: + - cron: '30 1 * * 0' + + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/django.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/django.yml new file mode 100644 index 00000000..94baea98 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/django.yml @@ -0,0 +1,136 @@ +name: Django CI + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + +jobs: + build-normal: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + run: | + python manage.py makemigrations + python manage.py migrate + python manage.py test + + build-with-es: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - uses: miyataka/elasticsearch-github-actions@1 + + with: + stack-version: '7.12.1' + plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip' + + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200 + run: | + python manage.py makemigrations + python manage.py migrate + coverage run manage.py test + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: false + tags: djangoblog/djangoblog:dev diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml new file mode 100644 index 00000000..a312e2fa --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml @@ -0,0 +1,43 @@ +name: docker + +on: + push: + paths-ignore: + - '**/*.md' + - '**/*.yml' + branches: + - 'master' + - 'dev' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set env to docker dev tag + if: endsWith(github.ref, '/dev') + run: | + echo "DOCKER_TAG=test" >> $GITHUB_ENV + - name: Set env to docker latest tag + if: endsWith(github.ref, '/master') + run: | + echo "DOCKER_TAG=latest" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}} + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml new file mode 100644 index 00000000..5eb08539 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml @@ -0,0 +1,39 @@ +name: publish release + +on: + release: + types: [ published ] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: name/app + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + platforms: | + linux/amd64 + linux/arm64 + linux/arm/v7 + linux/arm/v6 + linux/386 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }} diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitignore b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitignore new file mode 100644 index 00000000..30158169 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/.gitignore @@ -0,0 +1,80 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.pot + +# Django stuff: +*.log +logs/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +# PyCharm +# http://www.jetbrains.com/pycharm/webhelp/project.html +.idea +.iml +static/ +# virtualenv +venv/ + +collectedstatic/ +djangoblog/whoosh_index/ +google93fd32dbd906620a.html +baidu_verify_FlHL7cUyC9.html +BingSiteAuth.xml +cb9339dbe2ff86a5aa169d28dba5f615.txt +werobot_session.* +django.jpg +uploads/ +settings_production.py +werobot_session.db +bin/datas/ diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/Dockerfile b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/Dockerfile new file mode 100644 index 00000000..80b46acc --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 +ENV PYTHONUNBUFFERED 1 +WORKDIR /code/djangoblog/ +RUN apt-get update && \ + apt-get install default-libmysqlclient-dev gettext -y && \ + rm -rf /var/lib/apt/lists/* +ADD requirements.txt requirements.txt +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir gunicorn[gevent] && \ + pip cache purge + +ADD . . +RUN chmod +x /code/djangoblog/deploy/entrypoint.sh +ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/LICENSE b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/LICENSE new file mode 100644 index 00000000..3b08474a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 车亮亮 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/README.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/README.md new file mode 100644 index 00000000..56aa4cc5 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/admin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/admin.py new file mode 100644 index 00000000..32e483c0 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/admin.py @@ -0,0 +1,59 @@ +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.forms import UsernameField +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import BlogUser + + +class BlogUserCreationForm(forms.ModelForm): + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) + + class Meta: + model = BlogUser + fields = ('email',) + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("passwords do not match")) + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.source = 'adminsite' + user.save() + return user + + +class BlogUserChangeForm(UserChangeForm): + class Meta: + model = BlogUser + fields = '__all__' + field_classes = {'username': UsernameField} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class BlogUserAdmin(UserAdmin): + form = BlogUserChangeForm + add_form = BlogUserCreationForm + list_display = ( + 'id', + 'nickname', + 'username', + 'email', + 'last_login', + 'date_joined', + 'source') + list_display_links = ('id', 'username') + ordering = ('-id',) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/apps.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/apps.py new file mode 100644 index 00000000..9b3fc5a4 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/forms.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/forms.py new file mode 100644 index 00000000..fce4137e --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/forms.py @@ -0,0 +1,117 @@ +from django import forms +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.core.exceptions import ValidationError +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ +from . import utils +from .models import BlogUser + + +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['password'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + + +class RegisterForm(UserCreationForm): + def __init__(self, *args, **kwargs): + super(RegisterForm, self).__init__(*args, **kwargs) + + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) + self.fields['password1'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + self.fields['password2'].widget = widgets.PasswordInput( + attrs={'placeholder': "repeat password", "class": "form-control"}) + + def clean_email(self): + email = self.cleaned_data['email'] + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError(_("email already exists")) + return email + + class Meta: + model = get_user_model() + fields = ("username", "email") + + +class ForgetPasswordForm(forms.Form): + new_password1 = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("New password") + } + ), + ) + + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") + } + ), + ) + + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") + } + ), + ) + + code = forms.CharField( + label=_('Code'), + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") + } + ), + ) + + def clean_new_password2(self): + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) + password_validation.validate_password(password2) + + return password2 + + def clean_email(self): + user_email = self.cleaned_data.get("email") + if not BlogUser.objects.filter( + email=user_email + ).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + raise ValidationError(_("email does not exist")) + return user_email + + def clean_code(self): + code = self.cleaned_data.get("code") + error = utils.verify( + email=self.cleaned_data.get("email"), + code=code, + ) + if error: + raise ValidationError(error) + return code + + +class ForgetPasswordCodeForm(forms.Form): + email = forms.EmailField( + label=_('Email'), + ) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0001_initial.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0001_initial.py new file mode 100644 index 00000000..d2fbcab5 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='BlogUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + 'ordering': ['-id'], + 'get_latest_by': 'id', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py new file mode 100644 index 00000000..1a9f5095 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bloguser', + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, + ), + migrations.RemoveField( + model_name='bloguser', + name='created_time', + ), + migrations.RemoveField( + model_name='bloguser', + name='last_mod_time', + ), + migrations.AddField( + model_name='bloguser', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='bloguser', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='bloguser', + name='nickname', + field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), + ), + migrations.AlterField( + model_name='bloguser', + name='source', + field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + ), + ] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/models.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/models.py new file mode 100644 index 00000000..3baddbb2 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/models.py @@ -0,0 +1,35 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from djangoblog.utils import get_current_site + + +# Create your models here. + +class BlogUser(AbstractUser): + nickname = models.CharField(_('nick name'), max_length=100, blank=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + source = models.CharField(_('create source'), max_length=100, blank=True) + + def get_absolute_url(self): + return reverse( + 'blog:author_detail', kwargs={ + 'author_name': self.username}) + + def __str__(self): + return self.email + + def get_full_url(self): + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + ordering = ['-id'] + verbose_name = _('user') + verbose_name_plural = verbose_name + get_latest_by = 'id' diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/templatetags/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/tests.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/tests.py new file mode 100644 index 00000000..6893411c --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/tests.py @@ -0,0 +1,207 @@ +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from accounts.models import BlogUser +from blog.models import Article, Category +from djangoblog.utils import * +from . import utils + + +# Create your tests here. + +class AccountTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + self.blog_user = BlogUser.objects.create_user( + username="test", + email="admin@admin.com", + password="12345678" + ) + self.new_test = "xxx123--=" + + def test_validate_account(self): + site = get_current_site().domain + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="qwer!@#$ggg") + testuser = BlogUser.objects.get(username='liangliangyy1') + + loginresult = self.client.login( + username='liangliangyy1', + password='qwer!@#$ggg') + self.assertEqual(loginresult, True) + response = self.client.get('/admin/') + self.assertEqual(response.status_code, 200) + + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.title = "nicetitleaaa" + article.body = "nicecontentaaa" + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_register(self): + self.assertEquals( + 0, len( + BlogUser.objects.filter( + email='user123@user.com'))) + response = self.client.post(reverse('account:register'), { + 'username': 'user1233', + 'email': 'user123@user.com', + 'password1': 'password123!q@wE#R$T', + 'password2': 'password123!q@wE#R$T', + }) + self.assertEquals( + 1, len( + BlogUser.objects.filter( + email='user123@user.com'))) + user = BlogUser.objects.filter(email='user123@user.com')[0] + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + path = reverse('accounts:result') + url = '{path}?type=validation&id={id}&sign={sign}'.format( + path=path, id=user.id, sign=sign) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.client.login(username='user1233', password='password123!q@wE#R$T') + user = BlogUser.objects.filter(email='user123@user.com')[0] + user.is_superuser = True + user.is_staff = True + user.save() + delete_sidebar_cache() + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.category = category + article.title = "nicetitle333" + article.body = "nicecontentttt" + article.author = user + + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse('account:logout')) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.post(reverse('account:login'), { + 'username': 'user1233', + 'password': 'password123' + }) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + def test_verify_email_code(self): + to_email = "admin@admin.com" + code = generate_code() + utils.set_code(to_email, code) + utils.send_verify_email(to_email, code) + + err = utils.verify("admin@admin.com", code) + self.assertEqual(err, None) + + err = utils.verify("admin@123.com", code) + self.assertEqual(type(err), str) + + def test_forget_password_email_code_success(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@admin.com") + ) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content.decode("utf-8"), "ok") + + def test_forget_password_email_code_fail(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict() + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@com") + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + def test_forget_password_email_success(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code=code, + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + self.assertEqual(resp.status_code, 302) + + # 验证用户密码是否修改成功 + blog_user = BlogUser.objects.filter( + email=self.blog_user.email, + ).first() # type: BlogUser + self.assertNotEqual(blog_user, None) + self.assertEqual(blog_user.check_password(data["new_password1"]), True) + + def test_forget_password_email_not_user(self): + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email="123@123.com", + code="123456", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + + + def test_forget_password_email_code_error(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code="111111", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/urls.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/urls.py new file mode 100644 index 00000000..107a801d --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/urls.py @@ -0,0 +1,28 @@ +from django.urls import path +from django.urls import re_path + +from . import views +from .forms import LoginForm + +app_name = "accounts" + +urlpatterns = [re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + path(r'account/result.html', + views.account_result, + name='result'), + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), + ] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/user_login_backend.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/user_login_backend.py new file mode 100644 index 00000000..73cdca1b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/user_login_backend.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailOrUsernameModelBackend(ModelBackend): + """ + 允许使用用户名或邮箱登录 + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + if '@' in username: + kwargs = {'email': username} + else: + kwargs = {'username': username} + try: + user = get_user_model().objects.get(**kwargs) + if user.check_password(password): + return user + except get_user_model().DoesNotExist: + return None + + def get_user(self, username): + try: + return get_user_model().objects.get(pk=username) + except get_user_model().DoesNotExist: + return None diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/utils.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/utils.py new file mode 100644 index 00000000..4b94bdfe --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/utils.py @@ -0,0 +1,49 @@ +import typing +from datetime import timedelta + +from django.core.cache import cache +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ + +from djangoblog.utils import send_email + +_code_ttl = timedelta(minutes=5) + + +def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): + """发送重设密码验证码 + Args: + to_mail: 接受邮箱 + subject: 邮件主题 + code: 验证码 + """ + html_content = _( + "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " + "properly") % {'code': code} + send_email([to_mail], subject, html_content) + + +def verify(email: str, code: str) -> typing.Optional[str]: + """验证code是否有效 + Args: + email: 请求邮箱 + code: 验证码 + Return: + 如果有错误就返回错误str + Node: + 这里的错误处理不太合理,应该采用raise抛出 + 否测调用方也需要对error进行处理 + """ + cache_code = get_code(email) + if cache_code != code: + return gettext("Verification code error") + + +def set_code(email: str, code: str): + """设置code""" + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """获取code""" + return cache.get(email) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/views.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/views.py new file mode 100644 index 00000000..ae67aec4 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/accounts/views.py @@ -0,0 +1,204 @@ +import logging +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import get_user_model +from django.contrib.auth import logout +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.hashers import make_password +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +from django.views import View +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic import FormView, RedirectView + +from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache +from . import utils +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from .models import BlogUser + +logger = logging.getLogger(__name__) + + +# Create your views here. + +class RegisterView(FormView): + form_class = RegisterForm + template_name = 'account/registration_form.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(RegisterView, self).dispatch(*args, **kwargs) + + def form_valid(self, form): + if form.is_valid(): + user = form.save(False) + user.is_active = False + user.source = 'Register' + user.save(True) + site = get_current_site().domain + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('account:result') + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( + site=site, path=path, id=user.id, sign=sign) + + content = """ +

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

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

Thank you very much for your comments on this site

+ You can visit %(article_title)s + to review your comments, + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} + tomail = comment.author.email + send_email([tomail], subject, html_content) + try: + if comment.parent_comment: + html_content = _("""Your comment on %(article_title)s
has + received a reply.
%(comment_body)s +
+ go check it out! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s + """) % {'article_url': article_url, 'article_title': comment.article.title, + 'comment_body': comment.parent_comment.body} + tomail = comment.parent_comment.author.email + send_email([tomail], subject, html_content) + except Exception as e: + logger.error(e) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/views.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/views.py new file mode 100644 index 00000000..ad9b2b94 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/comments/views.py @@ -0,0 +1,63 @@ +# Create your views here. +from django.core.exceptions import ValidationError +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect +from django.views.generic.edit import FormView + +from accounts.models import BlogUser +from blog.models import Article +from .forms import CommentForm +from .models import Comment + + +class CommentPostView(FormView): + form_class = CommentForm + template_name = 'blog/article_detail.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(CommentPostView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + url = article.get_absolute_url() + return HttpResponseRedirect(url + "#comments") + + def form_invalid(self, form): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + return self.render_to_response({ + 'form': form, + 'article': article + }) + + def form_valid(self, form): + """提交的数据验证合法后的逻辑""" + user = self.request.user + author = BlogUser.objects.get(pk=user.pk) + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + if article.comment_status == 'c' or article.status == 'c': + raise ValidationError("该文章评论已关闭.") + comment = form.save(False) + comment.article = article + from djangoblog.utils import get_blog_setting + settings = get_blog_setting() + if not settings.comment_need_review: + comment.is_enable = True + comment.author = author + + if form.cleaned_data['parent_comment_id']: + parent_comment = Comment.objects.get( + pk=form.cleaned_data['parent_comment_id']) + comment.parent_comment = parent_comment + + comment.save(True) + return HttpResponseRedirect( + "%s#div-comment-%d" % + (article.get_absolute_url(), comment.pk)) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml new file mode 100644 index 00000000..83e35ffd --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml @@ -0,0 +1,48 @@ +version: '3' + +services: + es: + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + container_name: es + restart: always + environment: + - discovery.type=single-node + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - 9200:9200 + volumes: + - ./bin/datas/es/:/usr/share/elasticsearch/data/ + + kibana: + image: kibana:8.6.1 + restart: always + container_name: kibana + ports: + - 5601:5601 + environment: + - ELASTICSEARCH_HOSTS=http://es:9200 + + djangoblog: + build: . + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_MEMCACHED_LOCATION=memcached:11211 + - DJANGO_ELASTICSEARCH_HOST=es:9200 + links: + - db + - memcached + depends_on: + - db + container_name: djangoblog + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.yml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.yml new file mode 100644 index 00000000..9609af3f --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/docker-compose/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3' + +services: + db: + image: mysql:latest + restart: always + environment: + - MYSQL_DATABASE=djangoblog + - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E + ports: + - 3306:3306 + volumes: + - ./bin/datas/mysql/:/var/lib/mysql + depends_on: + - redis + container_name: db + + djangoblog: + build: + context: ../../ + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./logs:/code/djangoblog/logs + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_REDIS_URL=redis:6379 + links: + - db + - redis + depends_on: + - db + container_name: djangoblog + nginx: + restart: always + image: nginx:latest + ports: + - "80:80" + - "443:443" + volumes: + - ./bin/nginx.conf:/etc/nginx/nginx.conf + - ./collectedstatic:/code/djangoblog/collectedstatic + links: + - djangoblog:djangoblog + container_name: nginx + + redis: + restart: always + image: redis:latest + container_name: redis + ports: + - "6379:6379" diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/entrypoint.sh b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/entrypoint.sh new file mode 100644 index 00000000..2fb64919 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/entrypoint.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +NAME="djangoblog" +DJANGODIR=/code/djangoblog +USER=root +GROUP=root +NUM_WORKERS=1 +DJANGO_WSGI_MODULE=djangoblog.wsgi + + +echo "Starting $NAME as `whoami`" + +cd $DJANGODIR + +export PYTHONPATH=$DJANGODIR:$PYTHONPATH + +python manage.py makemigrations && \ + python manage.py migrate && \ + python manage.py collectstatic --noinput && \ + python manage.py compress --force && \ + python manage.py build_index && \ + python manage.py compilemessages || exit 1 + +exec gunicorn ${DJANGO_WSGI_MODULE}:application \ +--name $NAME \ +--workers $NUM_WORKERS \ +--user=$USER --group=$GROUP \ +--bind 0.0.0.0:8000 \ +--log-level=debug \ +--log-file=- \ +--worker-class gevent \ +--threads 4 diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/configmap.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/configmap.yaml new file mode 100644 index 00000000..835d4ad0 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/configmap.yaml @@ -0,0 +1,119 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: web-nginx-config + namespace: djangoblog +data: + nginx.conf: | + user nginx; + worker_processes auto; + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + multi_accept on; + use epoll; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + gzip on; + gzip_disable "msie6"; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 8; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; + + # Include server configurations + include /etc/nginx/conf.d/*.conf; + } + djangoblog.conf: | + server { + server_name lylinux.net; + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + + location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ { + root /resource/djangopub; + expires 1d; + access_log off; + error_log off; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } + server { + server_name www.lylinux.net; + listen 80; + return 301 https://lylinux.net$request_uri; + } + resource.lylinux.net.conf: | + server { + index index.html index.htm; + server_name resource.lylinux.net; + root /resource/; + + location /djangoblog/ { + alias /code/djangoblog/collectedstatic/; + } + + access_log off; + error_log off; + include lylinux/resource.conf; + } + lylinux.resource.conf: | + expires max; + access_log off; + log_not_found off; + add_header Pragma public; + add_header Cache-Control "public"; + add_header "Access-Control-Allow-Origin" "*"; + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: djangoblog-env + namespace: djangoblog +data: + DJANGO_MYSQL_DATABASE: djangoblog + DJANGO_MYSQL_USER: db_user + DJANGO_MYSQL_PASSWORD: db_password + DJANGO_MYSQL_HOST: db_host + DJANGO_MYSQL_PORT: db_port + DJANGO_REDIS_URL: "redis:6379" + DJANGO_DEBUG: "False" + MYSQL_ROOT_PASSWORD: db_password + MYSQL_DATABASE: djangoblog + MYSQL_PASSWORD: db_password + DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/deployment.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/deployment.yaml new file mode 100644 index 00000000..414fdcc5 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/deployment.yaml @@ -0,0 +1,274 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + replicas: 3 + selector: + matchLabels: + app: djangoblog + template: + metadata: + labels: + app: djangoblog + spec: + containers: + - name: djangoblog + image: liangliangyy/djangoblog:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: djangoblog + mountPath: /code/djangoblog/collectedstatic + - name: resource + mountPath: /resource + volumes: + - name: djangoblog + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: 200m + memory: 2Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + replicas: 1 + selector: + matchLabels: + app: db + template: + metadata: + labels: + app: db + spec: + containers: + - name: db + image: mysql:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3306 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: db-data + mountPath: /var/lib/mysql + volumes: + - name: db-data + persistentVolumeClaim: + claimName: db-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: djangoblog.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf + subPath: resource.lylinux.net.conf + - name: nginx-config + mountPath: /etc/nginx/lylinux/resource.conf + subPath: lylinux.resource.conf + - name: djangoblog-pvc + mountPath: /code/djangoblog/collectedstatic + - name: resource-pvc + mountPath: /resource + volumes: + - name: nginx-config + configMap: + name: web-nginx-config + - name: djangoblog-pvc + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource-pvc + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + replicas: 1 + selector: + matchLabels: + app: elasticsearch + template: + metadata: + labels: + app: elasticsearch + spec: + containers: + - name: elasticsearch + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + imagePullPolicy: IfNotPresent + env: + - name: discovery.type + value: single-node + - name: ES_JAVA_OPTS + value: "-Xms256m -Xmx256m" + - name: xpack.security.enabled + value: "false" + - name: xpack.monitoring.templates.enabled + value: "false" + ports: + - containerPort: 9200 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + readinessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + volumeMounts: + - name: elasticsearch-data + mountPath: /usr/share/elasticsearch/data/ + volumes: + - name: elasticsearch-data + persistentVolumeClaim: + claimName: elasticsearch-pvc diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/gateway.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/gateway.yaml new file mode 100644 index 00000000..a8de073b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/gateway.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + namespace: djangoblog +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + number: 80 \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pv.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pv.yaml new file mode 100644 index 00000000..874b72f3 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pv.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-db +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-db + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-djangoblog +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-djangoblog + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-resource +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/resource/ + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-elasticsearch +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-elasticsearch + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pvc.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pvc.yaml new file mode 100644 index 00000000..ef238c5a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/pvc.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: db-pvc + namespace: djangoblog +spec: + storageClassName: local-storage + volumeName: local-pv-db + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: djangoblog-pvc + namespace: djangoblog +spec: + volumeName: local-pv-djangoblog + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: resource-pvc + namespace: djangoblog +spec: + volumeName: local-pv-resource + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elasticsearch-pvc + namespace: djangoblog +spec: + volumeName: local-pv-elasticsearch + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/service.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/service.yaml new file mode 100644 index 00000000..4ef2931e --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/service.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + selector: + app: djangoblog + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + selector: + app: nginx + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + selector: + app: db + ports: + - protocol: TCP + port: 3306 + targetPort: 3306 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + selector: + app: elasticsearch + ports: + - protocol: TCP + port: 9200 + targetPort: 9200 + type: ClusterIP + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/storageclass.yaml b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/storageclass.yaml new file mode 100644 index 00000000..5d5a14cd --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/k8s/storageclass.yaml @@ -0,0 +1,10 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: local-storage + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: Immediate + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/nginx.conf b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/nginx.conf new file mode 100644 index 00000000..32161d86 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/deploy/nginx.conf @@ -0,0 +1,50 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + server { + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } +} diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/__init__.py new file mode 100644 index 00000000..1e205f40 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/__init__.py @@ -0,0 +1 @@ +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/admin_site.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/admin_site.py new file mode 100644 index 00000000..f1204059 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/admin_site.py @@ -0,0 +1,64 @@ +from django.contrib.admin import AdminSite +from django.contrib.admin.models import LogEntry +from django.contrib.sites.admin import SiteAdmin +from django.contrib.sites.models import Site + +from accounts.admin import * +from blog.admin import * +from blog.models import * +from comments.admin import * +from comments.models import * +from djangoblog.logentryadmin import LogEntryAdmin +from oauth.admin import * +from oauth.models import * +from owntracks.admin import * +from owntracks.models import * +from servermanager.admin import * +from servermanager.models import * + + +class DjangoBlogAdminSite(AdminSite): + site_header = 'djangoblog administration' + site_title = 'djangoblog site admin' + + def __init__(self, name='admin'): + super().__init__(name) + + def has_permission(self, request): + return request.user.is_superuser + + # def get_urls(self): + # urls = super().get_urls() + # from django.urls import path + # from blog.views import refresh_memcache + # + # my_urls = [ + # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), + # ] + # return urls + my_urls + + +admin_site = DjangoBlogAdminSite(name='admin') + +admin_site.register(Article, ArticlelAdmin) +admin_site.register(Category, CategoryAdmin) +admin_site.register(Tag, TagAdmin) +admin_site.register(Links, LinksAdmin) +admin_site.register(SideBar, SideBarAdmin) +admin_site.register(BlogSettings, BlogSettingsAdmin) + +admin_site.register(commands, CommandsAdmin) +admin_site.register(EmailSendLog, EmailSendLogAdmin) + +admin_site.register(BlogUser, BlogUserAdmin) + +admin_site.register(Comment, CommentAdmin) + +admin_site.register(OAuthUser, OAuthUserAdmin) +admin_site.register(OAuthConfig, OAuthConfigAdmin) + +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) + +admin_site.register(Site, SiteAdmin) + +admin_site.register(LogEntry, LogEntryAdmin) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/apps.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/apps.py new file mode 100644 index 00000000..d29e318a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + +class DjangoblogAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'djangoblog' + + def ready(self): + super().ready() + # Import and load plugins here + from .plugin_manage.loader import load_plugins + load_plugins() \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/blog_signals.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/blog_signals.py new file mode 100644 index 00000000..393f441c --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/blog_signals.py @@ -0,0 +1,122 @@ +import _thread +import logging + +import django.dispatch +from django.conf import settings +from django.contrib.admin.models import LogEntry +from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.core.mail import EmailMultiAlternatives +from django.db.models.signals import post_save +from django.dispatch import receiver + +from comments.models import Comment +from comments.utils import send_comment_email +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser + +logger = logging.getLogger(__name__) + +oauth_user_login_signal = django.dispatch.Signal(['id']) +send_email_signal = django.dispatch.Signal( + ['emailto', 'title', 'content']) + + +@receiver(send_email_signal) +def send_email_signal_handler(sender, **kwargs): + emailto = kwargs['emailto'] + title = kwargs['title'] + content = kwargs['content'] + + msg = EmailMultiAlternatives( + title, + content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=emailto) + msg.content_subtype = "html" + + from servermanager.models import EmailSendLog + log = EmailSendLog() + log.title = title + log.content = content + log.emailto = ','.join(emailto) + + try: + result = msg.send() + log.send_result = result > 0 + except Exception as e: + logger.error(f"失败邮箱号: {emailto}, {e}") + log.send_result = False + log.save() + + +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + id = kwargs['id'] + oauthuser = OAuthUser.objects.get(id=id) + site = get_current_site().domain + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: + from djangoblog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) + oauthuser.save() + + delete_sidebar_cache() + + +@receiver(post_save) +def model_post_save_callback( + sender, + instance, + created, + raw, + using, + update_fields, + **kwargs): + clearcache = False + if isinstance(instance, LogEntry): + return + if 'get_full_url' in dir(instance): + is_update_views = update_fields == {'views'} + if not settings.TESTING and not is_update_views: + try: + notify_url = instance.get_full_url() + SpiderNotify.baidu_notify([notify_url]) + except Exception as ex: + logger.error("notify sipder", ex) + if not is_update_views: + clearcache = True + + if isinstance(instance, Comment): + if instance.is_enable: + path = instance.article.get_absolute_url() + site = get_current_site().domain + if site.find(':') > 0: + site = site[0:site.find(':')] + + expire_view_cache( + path, + servername=site, + serverport=80, + key_prefix='blogdetail') + if cache.get('seo_processor'): + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format( + id=instance.article.id) + cache.delete(comment_cache_key) + delete_sidebar_cache() + delete_view_cache('article_comments', [str(instance.article.pk)]) + + _thread.start_new_thread(send_comment_email, (instance,)) + + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: + logger.info(user) + delete_sidebar_cache() + # cache.clear() diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/elasticsearch_backend.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/elasticsearch_backend.py new file mode 100644 index 00000000..4afe4981 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/elasticsearch_backend.py @@ -0,0 +1,183 @@ +from django.utils.encoding import force_str +from elasticsearch_dsl import Q +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +from haystack.forms import ModelSearchForm +from haystack.models import SearchResult +from haystack.utils import log as logging + +from blog.documents import ArticleDocument, ArticleDocumentManager +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ElasticSearchBackend(BaseSearchBackend): + def __init__(self, connection_alias, **connection_options): + super( + ElasticSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.manager = ArticleDocumentManager() + self.include_spelling = True + + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() + docs = self.manager.convert_to_doc(models) + return docs + + def _create(self, models): + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) + + def _delete(self, models): + for m in models: + m.delete() + return True + + def _rebuild(self, models): + models = models if models else Article.objects.all() + docs = self.manager.convert_to_doc(models) + self.manager.update_docs(docs) + + def update(self, index, iterable, commit=True): + + models = self._get_models(iterable) + self.manager.update_docs(models) + + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) + self._delete(models) + + def clear(self, models=None, commit=True): + self.remove(None) + + @staticmethod + def get_suggestion(query: str) -> str: + """获取推荐词, 如果没有找到添加原搜索词""" + + search = ArticleDocument.search() \ + .query("match", body=query) \ + .suggest('suggest_search', query, term={'field': 'body'}) \ + .execute() + + keywords = [] + for suggest in search.suggest.suggest_search: + if suggest["options"]: + keywords.append(suggest["options"][0]["text"]) + else: + keywords.append(suggest["text"]) + + return ' '.join(keywords) + + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) + + start_offset = kwargs.get('start_offset') + end_offset = kwargs.get('end_offset') + + # 推荐词搜索 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) + else: + suggestion = query_string + + q = Q('bool', + should=[Q('match', body=suggestion), Q('match', title=suggestion)], + minimum_should_match="70%") + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] + + results = search.execute() + hits = results['hits'].total + raw_results = [] + for raw_result in results['hits']['hits']: + app_label = 'blog' + model_name = 'Article' + additional_fields = {} + + result_class = SearchResult + + result = result_class( + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields) + raw_results.append(result) + facets = {} + spelling_suggestion = None if query_string == suggestion else suggestion + + return { + 'results': raw_results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + +class ElasticSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + + +class ElasticSearchModelSearchForm(ModelSearchForm): + + def search(self): + # 是否建议搜索 + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + sqs = super().search() + return sqs + + +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/feeds.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/feeds.py new file mode 100644 index 00000000..8c4e851c --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/feeds.py @@ -0,0 +1,40 @@ +from django.contrib.auth import get_user_model +from django.contrib.syndication.views import Feed +from django.utils import timezone +from django.utils.feedgenerator import Rss201rev2Feed + +from blog.models import Article +from djangoblog.utils import CommonMarkdown + + +class DjangoBlogFeed(Feed): + feed_type = Rss201rev2Feed + + description = '大巧无工,重剑无锋.' + title = "且听风吟 大巧无工,重剑无锋. " + link = "/feed/" + + def author_name(self): + return get_user_model().objects.first().nickname + + def author_link(self): + return get_user_model().objects.first().get_absolute_url() + + def items(self): + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + + def item_title(self, item): + return item.title + + def item_description(self, item): + return CommonMarkdown.get_markdown(item.body) + + def feed_copyright(self): + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) + + def item_link(self, item): + return item.get_absolute_url() + + def item_guid(self, item): + return diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/logentryadmin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/logentryadmin.py new file mode 100644 index 00000000..2f6a5353 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/logentryadmin.py @@ -0,0 +1,91 @@ +from django.contrib import admin +from django.contrib.admin.models import DELETION +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse, NoReverseMatch +from django.utils.encoding import force_str +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + + +class LogEntryAdmin(admin.ModelAdmin): + list_filter = [ + 'content_type' + ] + + search_fields = [ + 'object_repr', + 'change_message' + ] + + list_display_links = [ + 'action_time', + 'get_change_message', + ] + list_display = [ + 'action_time', + 'user_link', + 'content_type', + 'object_link', + 'get_change_message', + ] + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return ( + request.user.is_superuser or + request.user.has_perm('admin.change_logentry') + ) and request.method != 'POST' + + def has_delete_permission(self, request, obj=None): + return False + + def object_link(self, obj): + object_link = escape(obj.object_repr) + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: + # try returning an actual link instead of object repr string + try: + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) + except NoReverseMatch: + pass + return mark_safe(object_link) + + object_link.admin_order_field = 'object_repr' + object_link.short_description = _('object') + + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) + user_link = escape(force_str(obj.user)) + try: + # try returning an actual link instead of object repr string + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) + except NoReverseMatch: + pass + return mark_safe(user_link) + + user_link.admin_order_field = 'user' + user_link.short_description = _('user') + + def get_queryset(self, request): + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') + + def get_actions(self, request): + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py new file mode 100644 index 00000000..2b4be5cb --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py @@ -0,0 +1,41 @@ +import logging + +logger = logging.getLogger(__name__) + + +class BasePlugin: + # 插件元数据 + PLUGIN_NAME = None + PLUGIN_DESCRIPTION = None + PLUGIN_VERSION = None + + def __init__(self): + if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): + raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") + self.init_plugin() + self.register_hooks() + + def init_plugin(self): + """ + 插件初始化逻辑 + 子类可以重写此方法来实现特定的初始化操作 + """ + logger.info(f'{self.PLUGIN_NAME} initialized.') + + def register_hooks(self): + """ + 注册插件钩子 + 子类可以重写此方法来注册特定的钩子 + """ + pass + + def get_plugin_info(self): + """ + 获取插件信息 + :return: 包含插件元数据的字典 + """ + return { + 'name': self.PLUGIN_NAME, + 'description': self.PLUGIN_DESCRIPTION, + 'version': self.PLUGIN_VERSION + } diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py new file mode 100644 index 00000000..6685b7ce --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py @@ -0,0 +1,7 @@ +ARTICLE_DETAIL_LOAD = 'article_detail_load' +ARTICLE_CREATE = 'article_create' +ARTICLE_UPDATE = 'article_update' +ARTICLE_DELETE = 'article_delete' + +ARTICLE_CONTENT_HOOK_NAME = "the_content" + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hooks.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hooks.py new file mode 100644 index 00000000..d7125402 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/hooks.py @@ -0,0 +1,44 @@ +import logging + +logger = logging.getLogger(__name__) + +_hooks = {} + + +def register(hook_name: str, callback: callable): + """ + 注册一个钩子回调。 + """ + if hook_name not in _hooks: + _hooks[hook_name] = [] + _hooks[hook_name].append(callback) + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") + + +def run_action(hook_name: str, *args, **kwargs): + """ + 执行一个 Action Hook。 + 它会按顺序执行所有注册到该钩子上的回调函数。 + """ + if hook_name in _hooks: + logger.debug(f"Running action hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + callback(*args, **kwargs) + except Exception as e: + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + + +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行一个 Filter Hook。 + 它会把 value 依次传递给所有注册的回调函数进行处理。 + """ + if hook_name in _hooks: + logger.debug(f"Applying filter hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + value = callback(value, *args, **kwargs) + except Exception as e: + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + return value diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/loader.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/loader.py new file mode 100644 index 00000000..12e824ba --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/plugin_manage/loader.py @@ -0,0 +1,19 @@ +import os +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + +def load_plugins(): + """ + Dynamically loads and initializes plugins from the 'plugins' directory. + This function is intended to be called when the Django app registry is ready. + """ + for plugin_name in settings.ACTIVE_PLUGINS: + plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): + try: + __import__(f'plugins.{plugin_name}.plugin') + logger.info(f"Successfully loaded plugin: {plugin_name}") + except ImportError as e: + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/settings.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/settings.py new file mode 100644 index 00000000..beece6c3 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/settings.py @@ -0,0 +1,343 @@ +""" +Django settings for djangoblog project. + +Generated by 'django-admin startproject' using Django 1.10.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" +import os +import sys +from pathlib import Path + +from django.utils.translation import gettext_lazy as _ + + +def env_to_bool(env, default): + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get( + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# DEBUG = False +TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' + +# ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# django 4.0新增配置 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# Application definition + + +INSTALLED_APPS = [ + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.sitemaps', + 'mdeditor', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + 'servermanager', + 'owntracks', + 'compressor', + 'djangoblog' +] + +MIDDLEWARE = [ + + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' +] + +ROOT_URLCONF = 'djangoblog.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' + ], + }, + }, +] + +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': 'Zyl123456789', + 'HOST': '127.0.0.1', + 'PORT': int( + 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/src/DjangoBlog-master/DjangoBlog-master/djangoblog/sitemap.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/sitemap.py new file mode 100644 index 00000000..8b7d4460 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/sitemap.py @@ -0,0 +1,59 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + + +class StaticViewSitemap(Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['blog:index', ] + + def location(self, item): + return reverse(item) + + +class ArticleSiteMap(Sitemap): + changefreq = "monthly" + priority = "0.6" + + def items(self): + return Article.objects.filter(status='p') + + def lastmod(self, obj): + return obj.last_modify_time + + +class CategorySiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.6" + + def items(self): + return Category.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class TagSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return Tag.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class UserSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + return obj.date_joined diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/spider_notify.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/spider_notify.py new file mode 100644 index 00000000..7b909e96 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/spider_notify.py @@ -0,0 +1,21 @@ +import logging + +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class SpiderNotify(): + @staticmethod + def baidu_notify(urls): + try: + data = '\n'.join(urls) + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + logger.info(result.text) + except Exception as e: + logger.error(e) + + @staticmethod + def notify(url): + SpiderNotify.baidu_notify(url) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/tests.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/tests.py new file mode 100644 index 00000000..01237d9a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/tests.py @@ -0,0 +1,32 @@ +from django.test import TestCase + +from djangoblog.utils import * + + +class DjangoBlogTest(TestCase): + def setUp(self): + pass + + def test_utils(self): + md5 = get_sha256('test') + self.assertIsNotNone(md5) + c = CommonMarkdown.get_markdown(''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) + self.assertIsNotNone(data) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/urls.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/urls.py new file mode 100644 index 00000000..4aae58a6 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/urls.py @@ -0,0 +1,64 @@ +"""djangoblog URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory + +from blog.views import EsSearchView +from djangoblog.admin_site import admin_site +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm +from djangoblog.feeds import DjangoBlogFeed +from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap + +sitemaps = { + + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), +] +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), + re_path(r'', include('blog.urls', namespace='blog')), + re_path(r'mdeditor/', include('mdeditor.urls')), + re_path(r'', include('comments.urls', namespace='comment')), + re_path(r'', include('accounts.urls', namespace='account')), + re_path(r'', include('oauth.urls', namespace='oauth')), + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), + re_path(r'^rss/$', DjangoBlogFeed()), + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), + re_path(r'', include('owntracks.urls', namespace='owntracks')) + , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/utils.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/utils.py new file mode 100644 index 00000000..57f63dca --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/utils.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +import logging +import os +import random +import string +import uuid +from hashlib import sha256 + +import bleach +import markdown +import requests +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.cache import cache +from django.templatetags.static import static + +logger = logging.getLogger(__name__) + + +def get_max_articleid_commentid(): + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) + + +def get_sha256(str): + m = sha256(str.encode('utf-8')) + return m.hexdigest() + + +def cache_decorator(expiration=3 * 60): + def wrapper(func): + def news(*args, **kwargs): + try: + view = args[0] + key = view.get_cache_key() + except: + key = None + if not key: + unique_str = repr((func, args, kwargs)) + + m = sha256(unique_str.encode('utf-8')) + key = m.hexdigest() + value = cache.get(key) + if value is not None: + # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) + if str(value) == '__default_cache_value__': + return None + else: + return value + else: + logger.debug( + 'cache_decorator set cache:%s key:%s' % + (func.__name__, key)) + value = func(*args, **kwargs) + if value is None: + cache.set(key, '__default_cache_value__', expiration) + else: + cache.set(key, value, expiration) + return value + + return news + + return wrapper + + +def expire_view_cache(path, servername, serverport, key_prefix=None): + ''' + 刷新视图缓存 + :param path:url路径 + :param servername:host + :param serverport:端口 + :param key_prefix:前缀 + :return:是否成功 + ''' + from django.http import HttpRequest + from django.utils.cache import get_cache_key + + request = HttpRequest() + request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.path = path + + key = get_cache_key(request, key_prefix=key_prefix, cache=cache) + if key: + logger.info('expire_view_cache:get key:{path}'.format(path=path)) + if cache.get(key): + cache.delete(key) + return True + return False + + +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + + +class CommonMarkdown: + @staticmethod + def _convert_markdown(value): + md = markdown.Markdown( + extensions=[ + 'extra', + 'codehilite', + 'toc', + 'tables', + ] + ) + body = md.convert(value) + toc = md.toc + return body, toc + + @staticmethod + def get_markdown_with_toc(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body + + +def send_email(emailto, title, content): + from djangoblog.blog_signals import send_email_signal + send_email_signal.send( + send_email.__class__, + emailto=emailto, + title=title, + content=content) + + +def generate_code() -> str: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + +def parse_dict_to_url(dict): + from urllib.parse import quote + url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) + for k, v in dict.items()]) + return url + + +def get_blog_setting(): + value = cache.get('get_blog_setting') + if value: + return value + else: + from blog.models import BlogSettings + if not BlogSettings.objects.count(): + setting = BlogSettings() + setting.site_name = 'djangoblog' + setting.site_description = '基于Django的博客系统' + setting.site_seo_description = '基于Django的博客系统' + setting.site_keywords = 'Django,Python' + setting.article_sub_length = 300 + setting.sidebar_article_count = 10 + setting.sidebar_comment_count = 5 + setting.show_google_adsense = False + setting.open_site_comment = True + setting.analytics_code = '' + setting.beian_code = '' + setting.show_gongan_code = False + setting.comment_need_review = False + setting.save() + value = BlogSettings.objects.first() + logger.info('set cache get_blog_setting') + cache.set('get_blog_setting', value) + return value + + +def save_user_avatar(url): + ''' + 保存用户头像 + :param url:头像url + :return: 本地路径 + ''' + logger.info(url) + + try: + basedir = os.path.join(settings.STATICFILES, 'avatar') + rsp = requests.get(url, timeout=2) + if rsp.status_code == 200: + if not os.path.exists(basedir): + os.makedirs(basedir) + + image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] + isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 + ext = os.path.splitext(url)[1] if isimage else '.jpg' + save_filename = str(uuid.uuid4().hex) + ext + logger.info('保存用户头像:' + basedir + save_filename) + with open(os.path.join(basedir, save_filename), 'wb+') as file: + file.write(rsp.content) + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + + +def delete_sidebar_cache(): + from blog.models import LinkShowType + keys = ["sidebar" + x for x in LinkShowType.values] + for k in keys: + logger.info('delete sidebar key:' + k) + cache.delete(k) + + +def delete_view_cache(prefix, keys): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) + + +def get_resource_url(): + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + + +ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', + 'h2', 'p'] +ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + + +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/whoosh_cn_backend.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/whoosh_cn_backend.py new file mode 100644 index 00000000..04e3f7fd --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/whoosh_cn_backend.py @@ -0,0 +1,1044 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +import json +import os +import re +import shutil +import threading +import warnings + +import six +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from datetime import datetime +from django.utils.encoding import force_str +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query +from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument +from haystack.inputs import Clean, Exact, PythonData, Raw +from haystack.models import SearchResult +from haystack.utils import get_identifier, get_model_ct +from haystack.utils import log as logging +from haystack.utils.app_loading import haystack_get_model +from jieba.analyse import ChineseAnalyzer +from whoosh import index +from whoosh.analysis import StemmingAnalyzer +from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT +from whoosh.fields import ID as WHOOSH_ID +from whoosh.filedb.filestore import FileStorage, RamStorage +from whoosh.highlight import ContextFragmenter, HtmlFormatter +from whoosh.highlight import highlight as whoosh_highlight +from whoosh.qparser import QueryParser +from whoosh.searching import ResultsPage +from whoosh.writing import AsyncWriter + +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +# Handle minimum requirement. +if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): + raise MissingDependency( + "The 'whoosh' backend requires version 2.5.0 or greater.") + +# Bubble up the correct error. + +DATETIME_REGEX = re.compile( + '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') +LOCALS = threading.local() +LOCALS.RAM_STORE = None + + +class WhooshHtmlFormatter(HtmlFormatter): + """ + This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. + We use it to have consistent results across backends. Specifically, + Solr, Xapian and Elasticsearch are using this formatting. + """ + template = '<%(tag)s>%(t)s' + + +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/src/DjangoBlog-master/DjangoBlog-master/djangoblog/wsgi.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/wsgi.py new file mode 100644 index 00000000..2295efd5 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/djangoblog/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for djangoblog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +application = get_wsgi_application() diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/README-en.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/README-en.md new file mode 100644 index 00000000..37ea0699 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config-en.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config-en.md new file mode 100644 index 00000000..b877efbd --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config-en.md @@ -0,0 +1,64 @@ +# Introduction to main features settings + +## Cache: +Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`. +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', + 'TIMEOUT': 60 * 60 * 10 + }, + 'locmemcache': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +``` + +## OAuth Login: +QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration. + +### Callback address examples: +QQ: http://your-domain-name/oauth/authorize?type=qq +Weibo: http://your-domain-name/oauth/authorize?type=weibo +type is in the type field of `oauthmanager`. + +## owntracks: +owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap. + +## Email feature: +Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +with your email account information. + +## WeChat Official Account +Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account. + +## Introduction to website configuration +You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc. +OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default. + +## Source code highlighting +If the code block in your article didn't show hightlight, please write the code blocks as following: + +![](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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config.md new file mode 100644 index 00000000..24673a37 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/config.md @@ -0,0 +1,58 @@ +# 主要功能配置介绍: + +## 缓存: +缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。 +https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199 + + +## oauth登录: + +现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 +**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 +### 回调地址示例: +qq:http://你的域名/oauth/authorize?type=qq +微博:http://你的域名/oauth/authorize?type=weibo +type对应在`oauthmanager`中的type字段。 + +## owntracks: +owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: +`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。 + +## 邮件功能: +同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +为你自己的邮箱配置。 + +## 微信公众号 +集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。 +然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。 +## 网站配置介绍 +在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 +其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 +## 代码高亮 +如果你发现你文章的代码没有高亮,请这样书写代码块: + +![](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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker-en.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker-en.md new file mode 100644 index 00000000..8d5d59ed --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/docker.md new file mode 100644 index 00000000..e7c255aa --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/es.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/es.md new file mode 100644 index 00000000..97226c53 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/es.md @@ -0,0 +1,28 @@ +# 集成Elasticsearch +如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单, +首先需要注意如下几点: +1. 你的`Elasticsearch`支持`ik`中文分词 +2. 你的`Elasticsearch`版本>=7.3.0 + +接下来在`settings.py`做如下改动即可: +- 增加es链接,如下所示: +```python +ELASTICSEARCH_DSL = { + 'default': { + 'hosts': '127.0.0.1:9200' + }, +} +``` +- 修改`HAYSTACK`配置: +```python +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, +} +``` +然后终端执行: +```shell script +./manage.py build_index +``` +这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。 \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/alipay.jpg b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/alipay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..424d70a2ffbb629b481e0c27d72d6076727e8041 GIT binary patch literal 17961 zcmcJ%1z1$w*Y`hk4B$wLgu@LetspQo3`$A2q)LZ$NQk5;NOyN5ASECmq9ENJ(p`cQ zD)pb4GkAO7&+~hq>-oRe3+I|^=Ir5|9qa79)@OY;=i}#dAjmywIcX3E1_*?K_ywI$ zf$o43WvM?$PLN;@xT5#{{Xs-kI{uOfQ9iF2=g)q z)@6+I7SL4?2onog+rNKsu&{A4k!5fQ2m=cPlMoLN3mX#)goSJpCKfi%Wl{e36aR6Wg z2=J#H4*&7Lt&InbnnzMz%c1mDyZ3W;?fm)p*bIn6y{af(4(q`cOXe|8ol0^lNCdh? zO=jBiI)l!k51Zd@U9QH$zDXhxvG^|X9EAVBZ0_(rf6f7)V$fCy@2uB{*$5kndHj3q zHgfAl>0~t2*0S$*Jzun9IWztHYf2D&jcvC4L5Z=?SLIx0Mjy9wp!@t%|3xLyeU7!y zgzokbpwA^v32=dK&jtprUtAK79=rc?ksTVdEV+b_?LJOEFsvS|7G$^JW0%ej7=Qmo zKEPhHfB)p{IVL#+tfFAPAdf4uCgm>}HX)>{wlrDWeknv-vAEU|o+LTA@Xp5IQxvxW zf9ZS&t|eGLc)j`wiEOL$Wc>>!>Kslk_Pmj4K{l-tCf?!Zfj5S$29j$zYLuJu=gs-0`X+gm94OHxcN|W1iINFu+ zdk+p{Dp>xNWv}b(UhSp3MyNUv4N}Ha3|Z~aMgn!XEvu`Ks-~O`B@bK8=g-qL(NC7~ z-YPdRj_=@D#z|5?AoAUU3Ah#u4Cd>(ulOrGPy8G!G}Xszd@t2zHHnx<$963Q(HoUl zH8$-;MvAB(JYdf*fK``#tQRLIR8Pj@J=WVO1#X`I;2ae8VPg@G-+jV}s%wNu)F5LvHIN>&@_N(_(VxD!u*Q1Ly5c{b5%R zuEsvRXr@`mQGh4ihY}u~xSf}M;+e8!$4~B>r}yXJV{_v0r7Hb$o7Rb`B8zy!>hOOa z#NQSEmkp>5S*p6XQCN)x3b{|VPlB4?pM#E!3gvU9R%unNBLbk104Qm_w^VrFn)+R| zf#ItkgYP^^*(JdAeq>?$Jq-hks{6k@1c%D4xm&Vu(EW|K&f=GL$JbYmhw|eaRCqMXXA+S zF+<_l`s;4%8?@`hAKq@yYrh^``CcESk}(K$hJm$4s~RJBEiFy!>`qGwK1K@X%bt_h zZoQNr%+6Fi0vIsCPZaDT9N1DxQt`U&C>*~G(Jay4%fFKk|4!42TUh5;V$rf*F!LEa zExYi#=9Xbb_rkkWgEqv2i>!@hgf9gS(XM8N%$w<(xO1M89sy9a@%xI`I7_>q5=!`^ zpVd6JtD>aW7~OE#E6K_CQu*KShFB_=G&t#Gr{eS04kqilt)NfjK!^AT@}2y37v#36 zC>%^#oY%yR-AdG*u<>oaaKDGQ6!A`K7&^2X);NFuiAP$1vLo`V!~bC_I{VoFWmN=I zB7Iz!reW0w?)ck4bl+{`ddKa6S+^ZV{fdM2(>Y0$85tu0mkElsjyZxLa- zwP+}ngS2UEWDSD@$gozuf4%?d=RLR`nWF16+L46olzKwcwoEBEkJgf5w`76FWz>ab z>@0Q|>RlrBpVIp>Xk!PdWLLEIe5Y8lKxvALE-Ee3Qm)S$ zcZ+qw6x>S8N&Sk)n4)a{Y~(X@deaNuQ+$Y#&hZZ?>EE9$1NDA5X>+$Q3Q(O8EF}ta z-(O6{O^d3oHXSF1!34Ditx>a7N`|(j&NodByf;h4SixmDvApU$3O`72w^D{$pu1*< znfN%;Swq2T=6C1ptQso$YZ^=^1+a1IqCz+OZ7 zW2sUcv`hZ92i$6WNesk+UW9n>ZJ$z#i)TtaM?ZHFHx4NSzY5Ml^h^8Af7;Gfg!O8@ z%?#AFWV9J;%)yQsABs+Y!3%~YF8tkBV2Z9AGncoXVCJ_}9__B=XIGl#7n$Q01C3c>XR}&qI@kegI0gia4aYW2WaI*E|-6jR-3{_TFOLyl?rE7*b4^+wDB<1k2Pi zu|?I06_**St~>KZ@%(kQU*_Qs_->50PSmibm{v-dm@j6w`t9efKS>li$@@tAWvNCPAB{9PKcIBxw2cCf&Y)3KbSSjehi~3PL?(q>+ zW;AbXnq0}j=~YleN=}|YlIxt_iW&*Hd`VAPExVTUH+el8 z!_Ck7J_h6U|8+Rf)`ydKkNtIr6LiB_&P({B zk_t9W+dC(vS=HSY75r}u7eEcqRA7=A4a^6l;Tj1t{@xYDN6?1>$@JcKPnNl}Q-;-p zWI@6_V0;zLzLCN&oMT^}kk_CJao7O|BQNEE!}uJuH0%E&UVQJ%t~BG>$~ovGuCoV| znDj28N%B<}yS0b&&d+*&h)xKP1hd~A>G8P&!u;kN3@Owea}l{@cEaAT?`l-uCgc?M zwZk`k{--o^6BorS5&J3Rx@6-6NtRD`4$Pgwc);UL##;f*_)Q|IM3faQsIQ}6{JmGb0vTfr_!5YNIc`yZ!{U>!+^4N;*gf_u z+)Gq^rS*LCxXPLFpS&l-0v`~cgRDlKx!oQgtM<3fS8l_j*GzY353^C_)u}{=G-AW3 zVil8L)K6ij1*qZ@^E*_5ebscRVrntqGD67mVja|FWKiY*<84Z$=wDGa``D_XZlNs! z;=-KsK@HG&J#rzz@+9bam`w1&)YhpQc3aBfEL&_j%6fWsaDksDG zXWXxH)eo`3hWA;(Xjd1s9GoZU2b>J&#Bgwf2c1l^BFk$9TS7u%DVw7GUGq)bT3cg6 zy5m!fuae;Hi*DUjqNwJRZ*^ShWSLy$E2Wwts13>0U(ed3KH@mdXPnSu)LrGvc;y4b zh+y;Leb0EdM5fuwP!#iW8twq~O=qhyD(^0KrIEKY{kZ4yQ_@n7+$`=dV@u|YDCwuc zuAe_APk#2_#TS;_9u-`nrHqc82yB^mT;ZE7%-F`~F`~A+Hsn27&6)PsP11_Rmi{Mz!YOo3%p63Zbg)S=VstJ2w`3CY0gGouNm!5AnftgC-X`F8>lA80Oi&~R~q=e z3Q*-49s9^DsiMktdr{W`ig~<{ml+0bREfGw*gtL~r%3LX_m}aLO}can;9_M&+?O4K z;@u?)5YEHD4yv1O#_}n);9MoZwT32_G?&$8BVS%8L9B(QuO0_ShFebs47uGuKtJhl z)^070K}GMi8OjoTo|}HMtzT_ES%sJr^faXrhwaNnShecIo;9|5j?RruYboOMrP>mE zwKBamoM6*Egh=Dly63hdo@Tghx08;3o4_=e2x0$0E#DDEv?MNNfO1lW93w{0jq4A5 zG(B>DC4clbIey3aB1l_BJugjQebIUvP7Hb#?wDGAS+u#tWof0ok|Ife=7|H__Ve7P zjtz*oB*gzy!t27Eq;t?f1E(&8rQGEt_$| zYlmKcZDdzzY_bC3g;jC_G)!(WWcoG- zor4DLD@UD4rSxc0rULb|ZyNPyPzVFQbiT`71qbU?hif=${c(bbNJoH*lxu4no~q3; zX*Qy?BC>+q`$6vN1h8$++qiAN7_8wmefS#MQ-yD7WgV&)RxYx zl@4hH*zGC@1ujPv?E19*E6L5>Dq6)^riLWcr?8^WpZ&H@-1DqIC1kVDpk$yk{g`R(rjXHWGKF2nS{|#3mvsc7~L2C@%^54rm3iUmidV-jaIICiIDpP z1IDDTX2zO12$y7Xk#b=JBk~1Wu58(*3^ul);*$@{-8V>7{z=)u10qfD<~ z<$kiKirnyBApd7$5oS&9QtNU=PJs5v$+t*{WoW`=1TYhHL;3g^_B`)k6CKfy7{;aD z-^lVzu-DyHobXh+P034TzS!fyN$yO<$DMAO$YatM{r2Fg<{kUPBMRMrjL022tPf-W5{54<&F&|33lqgqHodXps z-?P!lE=Vm4{SZP^`yFnG4v>hNP1X&^5xpAPT!wwB<)iC7cFEdp#czWG_$z4?hsZ$0 zjGqm%eWZY?U43LoPDf>Z{)&Rjn2G2ZR{@K;UrCf`;Jh0QYZ4-QK|_l8t4KA}tIJ=t zyR5FMnDryD)!*Si4*x3H=(aaIKSyVTOl9piU4EKL;uf-T73b&J2de51_rvPJJTIt+ z5YVOHKts{=4kQrMA zNhlJHr=l{auZQqU=7zFX*MwOBM2AYHNX7!r;Tysfoq$RB)k+O8X{uB|-^T*b7Id-C0(1;!-o(K@D4Q14QUZXu)b) z_;GFO8Ho!&svK$#a3*xfSfJ85iLanZ-}UNMWw=4GPH{#WUQ3&&8gG^B>bSp{x2o0_ zFt_ec#4^qL*M8Gt`DedgV7DQTo|3QBu3;D$CdVH|!{)RS%^m}PIRu4drMaz7(lOMM zIhFyf=E_w26J>!5^wGUIh=I=*cRO!v24e8<9x4Q|*S@aC4vaTyF#;rQu*QYvS^w|sa(H+qzzI;oW2P}O?c%}7M*JrGlw)O z_zQl6l;$Y(IS|U`Qj=!E+pd#hsb13XU7ELWbjCenh{5c6p}P0RjC%=>+=P35cE%HTcpxT5MVeZb!wY(<4N+0K;14l@{kP_&gs;peAZD|#x#s7f z@@sc`FuTxUN?Kfh6Gt_XXShwX`+!awyO@6zwq@$`+>WIK!agZ8x7WW&| z|1u*^1+|JsRapuq+jkB!5_^%fbf6=CG{P@&+fZ=Emz1nGuxC$apElN9WuUGlc$ELF zo7Ufh;585m$LmxzYUgl9R+KO4ubkXefHy$lz{;ndIh64;bfFX-Gy*YZ1_FW@meC}z zjfRv4hJJli_aTgh^TL9zpeS@klDX0e?91~>KTq`h*LC~Cv@n6tFo)n2y?|rxn-iS= zgW)PBD&Wi`&^r~b-LgmJ{mu+CmsxPffgu`cvsY4t366A|BS%q_);>iE?(KL9lACL6 zQWO+MRow?dOglO;W0%bBm;8Bi^N@=-I~>62ls3YO+t>nTP@?1r14)Pv@*wT9+zCsA z-SXLDFRQOuBI>X3vi0(67Al`vXH7YZG6;~K^Nj-H-*D5gnb*FsO9jB~}z%)6> zDwD5T)C!=kSi_X+&}Ive8HPqfsdmx2pwj@xIS5k6ed~}lIWoV|QCgEZH_~AEwB#3y zyey1%)rU^@KAn`yX02{FqS6!uQXRJGo?BR&+uT)*%$nO;tUY`^uf0aVOA*?% zUxRN``wje900=7!0lug&cYC>>cx<{JFr}<&#cm3Dz}qQ7AY9SR?PmK1XdAlM`X(Rp zRBu~Q@z3Gy3TB{PL&WrS!A-yb7mwh48u?ZNYsyXEMb-rtg9C6-1iIS`v8M6s2N@!K zz%apn|5W~R29qp({#geqJBeJdmgS?`W17w`k$8bo+)DDxZjDCd z*yEOiZ;Y-YAX0?Jy`PKLDfmu4P~UhqVoAUZvMtrcimr3}EV&#lX-D|tB0(mf5yA7g z-ax&e8{pGl*Vl3C^I+`)F;t1xQcR)7dhQOj;ByecOZGp!%>#T%BJ{jV3B*2>0I>wf zFZoqd98Gyk<4bPsik;4A=1b&)_k{l536FDm^&EuurVhnLc))kWoUlUjwd5rF2GSkK zFt!|R)4HPL#@Te^xSVTSa^D>P$^wcW8H#np@eA&_^jI_`H?d3H_GY_^0?+t7&xPKs z*n%_8K@p8(Cp=80z3Revir!r!l7!`NW(W@-{3yeEr5*0>(j|58GfHZ1~nX4|P|@0fR4T1ei%jLJ}3Z1bs%(}>LE&~BaNL}TVt6FwIaZBuZ<^iSe( zDxAK(froKlA_~$r!8&nh zoh!X77`3?-=^H{j`=ia`$LF9kFKxTFYl9m_+G-8lJBKgdFH;D&XmYo2?il;BodP3J zE+tOc4KkZ?XNCUez*-J}Lv<;aL{2I7Wy|)(H8N+Y3 zy79~P(LFGazr3OaPmw+@jB{*DTMZ1BkM7tY+XpXKXNJk#3MpZ_cDUU9_EGK9V#oE1 z)W`bRBU&=IEvD;-xR!@CBiEH4qvl*Z5@gy&j|>%v)MoQ^wogUW`rOpsLn6|m7{G%= zBs_>34cH3}uN;~FbsXb7sUh~C@(c|4IOGvB42|0jD~a>~gu2UR>^bL`SHiJVX*cwX^i%G(F9wBOE2w%zUNVPg(*3&2pYh7{Uqh}B zf?c-z5@RiUu|sGx9giLoy-KpDjvy*dyva0E_fBN}i%VD35k)(-il)p%S`7bG(%Jr* zd8a_BPo|OK3?&U*R%!AQf}L{PHv0wYo>=FgrgKnVky`S76ATq4JM)nz_UP49E!)Dy z1ko@mADM4uQ=B{+)dN+HwIIXktdpWtM*7a$loi1e>t?1`rG&(goWS`lJN5xZu;P58 zOsn(t;Q3Q&udW%?(2$IJ3>C!24)T?@<8S@IK1K~|A%WT`0@3F163K3nwwouQG*kG-|!%R z0BLYx8U4&$m0uiLYEIg>g^FweJ&fd^@JpHqqfyT&1q|!*)SuwF-UhOfSC@0y2WQbz zqwpUr8d5kmk|fO|Uxxj~Wj2Ni5^davfWXDWeA1;bacm5)cBYd4{GaB2-PE}a=OEQ{ zkmJx8%9+=ZF=r88&)vIr#F!V~;yqm}N%G6`smy6R>KqsOaD+=zV5`^IY%?Am3Zyv? z{s=#gek^m^a}EkH65p{qfvOM5FNp8x>dI#GmXp!Qabic^Uee&cb_6@S94b_^f6M-z zxY-H)R_YoJKpP)jN73&q0C;V1HHuzDQrD0t&lHHxL6+#8P|NUO18zy1sPyIF=T{|hC34B;lDL7AHt}gYt9(6q1BQCP9)WR`g&q#~<%!Q)y zIqr|vGrN1t)p-ng7;U&d^PedizP7PN*xo*(m3Xc7Tu%`S3dA@ns`0;xJyNiM`tEtnK(Yn`65H1EyAbDKYcTEv%@%Ae)S={$inbWS`qz9J~gYx*XeBb8m4>VxM5k>a+az-Vi@mjW1(wKn1`(NK2W3>_A zbn>3(&h{CS`0M)abUQ;;(s)6RvkkF5BfC_aP#>z_;=(Rc(0>jnYk{T5bg3D|82{;d zAR6OqcrZPn^siHjHdZP*C8FgDpJ>9Xzu^JoE2gr--}0Z5zH%se_>vdb-)qjK3C6c0 zayIj|Q*>{_e_*N4)w|3Qdl6;5a(m1Z_TQg+G&e2!KPkf1d`Er`;@H*k=dDTK z5Z_UB$k#mw?d^&m9@9+>1l{a|}PhK<52nyU#D%8*uf>__Hjq7dHS zSweNH$-xeqgu|IJ@XMy%cv2t9@;hKFwq&w0kbg6*<;|156<^Fcz}r?Ak;n6@3N_`R25uwzIZIQ_6a&M3dQTUdd8VNiWffn(4M$wnEdu52(|pKV!+adX0m`P z`y&$Cx8FFAom93khy8Xb@MZOn;&U$NG7R=@G`_*-Hu76oMFmuPyq;wxs z6$vHYdbIZSRA(r?aIDqnBgL_Zft10wQ^GUiG}Zq>uF70yrQ({zJbWe?mOb5jMuL)0(afa+?<+blAg z3M^Ms85DIRbft{H5j}ik{q8P*m$~9)0VM&k#+oCxAINTwP(%9y`LF-kwQJ-32I=9v z_+$W_j1k(pX`(NTY4^}9-P%h<;Exmr_Wo~&pcC+_bu$y7=uCtv%CguFQ0`8wayGoHK{XGa)$1&vZe;Kxx~N!Y7TmZ@I7Pm52@dU zoS2AU5-J*S-6p+cbSoZqf4|gL>L@wTqHpy# z?6q4zL8)*yX1I$e+%DsO`~YI}546v%?wS%k`jSvpWMFX)>Zm!<@`$VMQ}eoqVK7~s zRJu}1Cp^1*4JZBTSjznuYF8ZYSEsB$=wOlRNCjK-vt(}!e1;P53 zGC}#bnxh>>l5f5>c8=*L!ajbo!+K34UENK$1?m@dRt)KKv%ozKMxecs`EC{CFlV4U za}Zfaw0Q^ycP7b|ozi?9$;34SIA-Ej^;~krVg5o9&5N4aEVQyDWf==>x*e5U_6Yl- zZ(C9>O46iZrHe`j`|Bd2P2eaTL4aOa`tJ_!mL2fP`%LeUUy2q3%hliH4OwE>HpuAv z_NCdBE+qp!wfxD5x+eiE;d2~43aOU?6=sm{$8zOHG$zVXu|Ry&gwheg{7ULh6s~BC z;T(x_@ZXA+m-5`abmt(wpLrG%Nf|~d^e=A8rC0J#9S`G9ByCN#F^d8OCc+uvVPejY z3&*oaVu+RVGR_9NVP9w|{MaQ^2O7FR$kWA~5M~Edh1r~gdcA{H)tRx?-w8hQG@SlH z9rxHuqpF+-|895}W93tdLn2DwDM#(Gf}6PO6;H{3EJPotTgmFarz(EQY>J3&CN=_Kq^#>$8&UsxPZm9rX>VNBQ(o|F|NCmWkMAjZ^cx9zsoB>13J zXpvha*4}Jx9h@L%iuDY;&B{_n6X%ZnLu_1quD_P()CQ&QbtUi|)~>R-Y&Ua6jpjJn zir(DilDe(INh6{5usqR3Q27I!lO7C)o5V9aeUi)WIekPW5Ve>Rb0>j;GF!;96r1*1 z{%IjM2i=K#7LZ#u^EG4kZwlvYsZz)Y8UE~*_vERg>r676YfXL5ns2G`J`T6hpVeh5SO>Kz=%e$^z|b+z15lb%k|YI@ zbkrBda|9+SH50%r5h-T|Y>G*Q$HO^IsXEa)f_BKXpJn-`CY!fuD;a)|5eOq(eEc$D zZl(4S@61iSq;KfDqZkatM}FIKR+-2yWLh2(cV0+1!2nwWpi*HmM72kyTVTrV{bs-H z=Do~qPA6YH0a%e0Ku;MdhBtOZBD4avKS4c-d+CZsc?-v8+{*xU*pZzYz1PQ zBa#C>6yqH!$RWKYSTpU}?}`wzSp$MAqIvP^b<@JNcl@*dUdK@bW*Yv_NESP=e9hFY zZ1DkosWIE5)G@k@UuAk0Lv_t14orvO;7f?R=FNAgl*SH|;jf>@#l0{WOD1c{8p%$| z$`Rt9xdm$a$-)BQFh9#w;oVtRDywu<`y+!lT%OR&3RGr29tjCuIE-++k$`Gs+@M;Q zoXW(qO}EWa%0%mz!Z?7$mtR2Bds%^Iz1{o|{Ks?QXrDlfBZ0A?z{!LPsl{PC08)Of z(IU^@7eQ)5h)Ote@R@sQC%osVwF&I*9?Z`?^b_0?pIf}fgrF}*sIck4PGmGR zYsJ@{$u(^xh0;`(abnt?lZJR3cY$Dwh2YA>l@$TQF>r7(3QgWyfZV|mW^KMagR!L! zhzI{WDf7SdgAzU9Ad|~?f6~S98rsV5bu(7FFZm`>;ByMB#+H}Y)Xh?O;hLc*<$RY> zlWGd(*nGzD3{4bh$haTyr!wn)bqq=dL%@$x=B*@#h2e64Fzm-w_9KW0fKt8S)-2l8 zk@F9{`LR)}?N&-W;(5a8%||n0Blj>B#aW18X`)$|!eQ?MSI7&RGkkgkE6z<=!HC+L za%b0wRV;esM6>1Zvc{1~7%G)dA<`T4VP=?4cV;Kr+UIvHh6nK@H{iKw zd1+B>^v;jB^vnhqW~owG%v;y6v!KC(&7hN$3w zzYXuCNV<1pWd0$|8`}e|a+Lh64sB@npb0vU*Gy}p?3-q@6631WWJmdG;%R)Ec721< zn3v*%rpKD16#If@ZInPpXXb5`%&So2gXJ=J-G|8ZIX`k%%Xyc(ANiNAA9$i9ZkQhj zm%e!foHF_0>Db-A=M_&x|25q7wDYr&e|NFWD=!vJ{yg2}F7vX<*oB{@65C zs+Ap?J)EpbRrxi_#bzQv2RB5cXoIZkx(y@&;fjP`5=P(Gh1xghyK!DZsu1Bg=poma zp)tnht|1wjBuX`lhow&6dv1-!6y)#60|g3o?@tA}w5_QyqRE;3eJhJG7qc!A|D0##? zZAYu3cab-HQPNjd%f%Eq2r=C3GXh^Y({Z9<9Ij-Ft#U|&3=u@HrMX^r%Pg(aBM$Ym zByLLEH%JmA#Ls0~>PR`^^*E03n9kKGdE0sq++GwKWX40{6L>Q4bRnoYxu_OGZ>fY{ zE)j_f4I4J|k-ML7(T2T03H}CwqO3DSUfi3+^4GsU-S}H6dION9s#g`JYGU2*o*&y` zTR?w1Wr%y=!)F2TB&cIoodLtE!%Wb>e$$(&VtvT-tO#2 z-hNY6Cz6(lfx6!nR0Z~-5*(IgeV>v%rgo(vphF*RS-4hdbxeYQ(Fyy=W7JJ(&<1NK za(y^DFL)Q!d=7e6+|E&$l^SKn|8&GLlpSGkSCeob0cS%Y}7>XECBda#?AYw#(t#=+-AnYLjn@;v7-xc%|pZB z-@25Zf6@E?iqT-1>QI}V|2}Pi)D#-7z5T*_tVDNm_fWm2J7~E{0EUH zaTS1=8oR}i)Yu5IzhuaJUQpk;{71-fgn99r)D@c@raD#Jg%rK8OP47l=598G)R@Wj z3sTPhZiV`Q96~rJBXt8Q=)!u8yhE%_RpnofgHAR5AyUT@l4pNCRG}~Y=Dj!tKLCypG0l6F-03=k8_xdFVtKtN$L;0@ zdllSZ8bb4G@{$AlXnPw?(qZ&2i;za~=DRe!%4ZZC8;W+TFN_-@m{VN{HK)4kjPA{M zXH#(!=3rgs@D?*K6N=(Bj}?rM^|%Mhm(NZSlyM4$ZhUriJrnEe) zRTrlvJqpsdr3&x#o3PF)w9dA4vIjNRNjnw|N*`)|T;=Y;n2VBrT)?}?qB7Dv%KC-k z=)n_;TisLzWGH;eJQ6`@!No#iqD9T-fedVB88xA zo;JC|uZgi!#Co3ph0}|BB{faa&67DZQZ{0o-XKATf{#ZZ%+&fF-U+AAOtAa?aLb}0l5WnG!>`?^H zx;`p{5TRCD<&pDIgyuBDu1=!sVf*15{)I(*0r6PfNrn+cCs{@i`14?)I&y!`f8JMd z*kb%Z+CGVHJ7sZ>lnL!6;R51P0M#nlTO&kwSc0l3X%OB1-+EPj7eFzDkPZFU(}W57 z5|=uiOyIL9nIqTRgi$pZPG-%!Mf-Or&w2l|+#ZpQ?4UM5&}dG?LIg_IU1eTafvAC3jwBAh% z9S0LF>sJsKN_hHwa+N=0)+^Uv9jPa19pl+^f4`NfU6>zvKq?rDO+|{&*r;1RAH`Nf zRpp~+Uw_j=Vh`;lbgJ95{8YG4e0|Of!f3Ex;VS#J=xb3=VFOFb;234CzO=o6DZq?{ zkhs2UeEwu79L22C4qkbyps>(LLEx{S&C9! zqFx9jK>9e%B;6VYNcEhXRc7t6XE0G_Z}PaJb<4OS9=lU$p?WhSV2HkccLqhdGHg(S zWrF|~p<%J>3qJo&Pz-2lNFnq^)@59eu$a4eoMPzfcAjiF2;bX5(Yk`+D%2LD|MixU zd_mo!|NBit2-hAGKj~7Iv$l)#L*f@3=0wuTv>^0Z2EcD~O@A=r&v+`CF1;pg`O#~R zdFh)!bZZ&CjU`0!k~h6o!*?3ig)3#y5Dr9yl!a%FcI?y_3y*6ZwT!Y&Gp`gzGp6qf z3!mXrO#mC4@-(282uWdko4($zc0XG5OPDmMp%ba0uuiMEql&`da93afQHT^9j7r>j zo_L1@zagwUwX-$#0jB}nxD*8LIW4%Fi4R{y%0fola}$zC&Os$7{dZ8LOg&aDDkvt* z6{|v6K&v164{d@r;P~b4$r{B*5p6*OuFfai)OxCSo1~XEHuz7)CLS2wBwbVKtnT0} zbJ+zuN1{ox6C_`^KtuQc_7gu!Q;3NTkdj=Idhi4l$%o`|>io%B4U#{JDTAC+Gv{50 z`r9cOO06j7F5xS$h}?jv{u+hxM<*_GH}}|ov}`aD>E{8|E9h-Sis-$GDh$e7|8%#h z*sG||^lCY>D`Sg)=Ma!M`(m@9#t2%1?XU1Z?;%RWm&DZEc742@DRG(@p4~zzl*2hQqqGixEB@_XUwvGfowJb6dIkeE}_PtEnQ>*cqZ@ z`u%SM63p&Hl*CkhPZP<5Rnat6d4q+|3pvTSk7jPUpIy~b1;$&&8bRd{H~AkooZ@Wc z3Ek7VKj7|!e@!Z#-&AoGi5S=*W)7$wkpT0mYKJGbx(L;WGn@GZy0J|#j@wfC+lyw3 zN@Ae*m6)Li9uo&GC(+@%(3S8&0wBto6#SQDH{VpUDCR3jjq4$J$7~--n+MaP0Njak zIHjlIG2_4!VxnDz4#Qc~FcQ(kw6`mi`Svqf62mZRuzHr__N zX|⁡O0bI!3Bi@jSTgKn>Bg&Q53II7XEA`NhoOY$25L(eK?<1>~>hujoRs}yUHVX zQTt}~MN1Zw0!xe7-aUbnm3fxd*KjwLLobx-SSZdGT`vUtB-=hS^~#Rs9K<#Wao{Zp z{kmB)_tVwwglj-W2qla4$Dx zz53|611jp`Ro8}#TnY+1OdA}SN;lGY+!=)=QY;q0b}yA~i0O^|s1#r34{5DET&Qh` z+yAF+h#H($?p6{{X zz@F@jlhUjuyW%|!{Q!~$q>$)y1Hsl$FMYS;#r=c7U7SN-8Uv-lv<@mZOXKOlXLy4C z3v?;Yh1KbtsO!ynC%cyr-`4MR8{Y~e28GXK7yRZ9{3d5(xoP=XYdX*Uy?#pU$a5>& z`JUOV_ zRl}`OH8`y;U%XD+MFBD!HxL@B`JSH_G!;0V9-A+_4Hhf%=wJ%txE{qNaHkfkLsS+h zfe@JrYreKrFKhU2`1kD)K_#LCd!v$sI$is2ee^7Am=@kq2Z-`cU(l{<8*ON_J?@@& zOR+4E3%sN-vIK-%81R}RdPK$bCrBoGF zRu$>N^0s*R^!6R919|j@|cY|A}(o`$`Onha=;(7(EQNg;0 z^sK+j)DQ>i-wCKpkXH^lH{QNw|FWJ%bUle^xtPfLdv#(gNpA7lv59GyIOXp*E+vz_!y|AkM=lB7mYXSWGfxr6#WlV5UW{v7tVn1!e zwg=v}kG!o+0qXfY-FyUozk}%&PS$ZRL^NoDuz%nVw(p)FVMPG6Ph*mgs4z0~C)P`9 zWsg_X!-)T2FkH=VqaRBu8Zkq=AX6oLSWpg(yQ^NIftdmxjx literal 0 HcmV?d00001 diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/imgs/pycharm_logo.png b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s-en.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s-en.md new file mode 100644 index 00000000..20e95272 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s-en.md @@ -0,0 +1,141 @@ +# Deploying DjangoBlog with Kubernetes + +This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch. + +## Architecture Overview + +This deployment utilizes a microservices-based, cloud-native architecture: + +- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`. +- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.** +- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names. +- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application. +- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC). + +## 1. Prerequisites + +Before you begin, please ensure you have the following: + +- A running Kubernetes cluster. +- The `kubectl` command-line tool configured to connect to your cluster. +- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster. +- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories. + +## 2. Deployment Steps + +### Step 1: Create a Namespace + +We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management. + +```bash +# Create a namespace named 'djangoblog' +kubectl create namespace djangoblog +``` + +### Step 2: Configure Persistent Storage + +This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`). + +```bash +# Log in to your master node +ssh user@master-node + +# Create the required storage directories +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# Log out from the node +exit +``` +**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file. + +After creating the directories, apply the storage-related configurations: + +```bash +# Apply the StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# Apply the PersistentVolumes (PVs) +kubectl apply -f deploy/k8s/pv.yaml + +# Apply the PersistentVolumeClaims (PVCs) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### Step 3: Configure the Application + +Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings. + +**It is strongly recommended to change the following fields:** +- `DJANGO_SECRET_KEY`: Change to a random, complex string. +- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password. + +```bash +# Edit the ConfigMap file +vim deploy/k8s/configmap.yaml + +# Apply the configuration +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### Step 4: Deploy the Application Stack + +Now, we can deploy all the core services. + +```bash +# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# Deploy the Services (to create internal endpoints for the Deployments) +kubectl apply -f deploy/k8s/service.yaml +``` + +The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### Step 5: Expose the Application Externally + +Finally, expose the Nginx service to external traffic by applying the `Ingress` rule. + +```bash +# Apply the Ingress rule +kubectl apply -f deploy/k8s/gateway.yaml +``` + +Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address: + +```bash +kubectl get ingress -n djangoblog +``` + +### Step 6: First-Time Initialization + +Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run. + +```bash +# First, get the name of a djangoblog pod +kubectl get pods -n djangoblog | grep djangoblog + +# Exec into one of the Pods (replace [pod-name] with the name from the previous step) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# Inside the Pod, run the following commands: +# Create a superuser account (follow the prompts) +python manage.py createsuperuser + +# (Optional) Create some test data +python manage.py create_testdata + +# (Optional, if ES is enabled) Create the search index +python manage.py rebuild_index + +# Exit the Pod +exit +``` + +Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster. \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s.md b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s.md new file mode 100644 index 00000000..9da3c289 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/docs/k8s.md @@ -0,0 +1,141 @@ +# 使用 Kubernetes 部署 DjangoBlog + +本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。 + +## 架构概览 + +本次部署采用的是微服务化的云原生架构: + +- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。 +- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。** +- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。 +- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。 +- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。 + +## 1. 环境准备 + +在开始之前,请确保您已具备以下环境: + +- 一个正在运行的 Kubernetes 集群。 +- `kubectl` 命令行工具已配置并能够连接到您的集群。 +- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。 +- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。 + +## 2. 部署步骤 + +### 步骤 1: 创建命名空间 + +我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。 + +```bash +# 创建一个名为 djangoblog 的命名空间 +kubectl create namespace djangoblog +``` + +### 步骤 2: 配置持久化存储 + +此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。 + +```bash +# 登录到您的 master 节点 +ssh user@master-node + +# 创建所需的存储目录 +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# 退出节点 +exit +``` +**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。 + +创建目录后,应用存储相关的配置文件: + +```bash +# 应用 StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# 应用 PersistentVolume (PV) +kubectl apply -f deploy/k8s/pv.yaml + +# 应用 PersistentVolumeClaim (PVC) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### 步骤 3: 配置应用 + +在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。 + +**强烈建议修改以下字段:** +- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。 +- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。 + +```bash +# 编辑 ConfigMap 文件 +vim deploy/k8s/configmap.yaml + +# 应用配置 +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### 步骤 4: 部署应用服务栈 + +现在,我们可以部署所有的核心服务了。 + +```bash +# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# 部署 Services (为 Deployments 创建内部访问端点) +kubectl apply -f deploy/k8s/service.yaml +``` + +部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### 步骤 5: 暴露应用到外部 + +最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。 + +```bash +# 应用 Ingress 规则 +kubectl apply -f deploy/k8s/gateway.yaml +``` + +部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址: + +```bash +kubectl get ingress -n djangoblog +``` + +### 步骤 6: 首次运行的初始化操作 + +与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。 + +```bash +# 首先,获取 djangoblog pod 的名称 +kubectl get pods -n djangoblog | grep djangoblog + +# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# 在 Pod 内部执行以下命令: +# 创建超级管理员账户 (请按照提示操作) +python manage.py createsuperuser + +# (可选) 创建测试数据 +python manage.py create_testdata + +# (可选,如果启用了 ES) 创建索引 +python manage.py rebuild_index + +# 退出 Pod +exit +``` + +至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署! \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.po b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..c80b30ac --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,685 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "password" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "Enter password again" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "passwords do not match" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "email already exists" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "New password" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "Confirm password" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "Email" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "Code" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "email does not exist" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "nick name" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "creation time" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "last modify time" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "create source" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "user" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "Verification code error" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "Verify Email" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "author" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "Publish selected articles" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "Draft selected articles" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "Close article comments" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "Open article comments" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "category" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "index" + +#: .\blog\models.py:21 +msgid "list" +msgstr "list" + +#: .\blog\models.py:22 +msgid "post" +msgstr "post" + +#: .\blog\models.py:23 +msgid "all" +msgstr "all" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "slide" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "modify time" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "Draft" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "Published" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "Open" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "Close" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "Article" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "Page" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "title" + +#: .\blog\models.py:75 +msgid "body" +msgstr "body" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "publish time" + +#: .\blog\models.py:79 +msgid "status" +msgstr "status" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "comment status" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "type" + +#: .\blog\models.py:89 +msgid "views" +msgstr "views" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "order" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "show toc" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "tag" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "article" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "category name" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "parent category" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "tag name" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "link name" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "link" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "is show" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "show type" + +#: .\blog\models.py:281 +msgid "content" +msgstr "content" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "is enable" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "sidebar" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "site name" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "site description" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "site seo description" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "site keywords" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "article sub length" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "sidebar article count" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "sidebar comment count" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "article comment count" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "show adsense" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "adsense code" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "open site comment" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "Website configuration" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "There can only be one configuration" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "Sorry, the server is busy, please click the home page to see other?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "Sorry, you do not have permission to access this page?" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "Disable comments" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "Enable comments" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "User" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "parent comment" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "enable" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "comment" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "Thanks for your comment" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

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

Thank you very much for your comments on this site

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

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

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

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

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

Please click the link below to bind your email

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

Please click the link below to bind your email

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

Thank you very much for your comments on this site

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

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

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

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

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

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

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

Please click the link below to bind your email

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

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

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

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

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

Please click the link below to bind your email

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

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

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

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

' + + return reading_time_html + content + + +plugin = ReadingTimePlugin() \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/__init__.py new file mode 100644 index 00000000..e88afca2 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/plugin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/plugin.py new file mode 100644 index 00000000..b5b19a33 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/seo_optimizer/plugin.py @@ -0,0 +1,142 @@ +import json +from django.utils.html import strip_tags +from django.template.defaultfilters import truncatewords +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from blog.models import Article, Category, Tag +from djangoblog.utils import get_blog_setting + + +class SeoOptimizerPlugin(BasePlugin): + PLUGIN_NAME = 'SEO 优化器' + PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。' + PLUGIN_VERSION = '0.2.0' + PLUGIN_AUTHOR = 'liuangliangyy' + + def register_hooks(self): + hooks.register('head_meta', self.dispatch_seo_generation) + + def _get_article_seo_data(self, context, request, blog_setting): + article = context.get('article') + if not isinstance(article, Article): + return None + + description = strip_tags(article.body)[:150] + keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords + + meta_tags = f''' + + + + + + + + + ''' + for tag in article.tags.all(): + meta_tags += f'' + meta_tags += f'' + + structured_data = { + "@context": "https://schema.org", + "@type": "Article", + "mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()}, + "headline": article.title, + "description": description, + "image": request.build_absolute_uri(article.get_first_image_url()), + "datePublished": article.pub_time.isoformat(), + "dateModified": article.last_modify_time.isoformat(), + "author": {"@type": "Person", "name": article.author.username}, + "publisher": {"@type": "Organization", "name": blog_setting.site_name} + } + if not structured_data.get("image"): + del structured_data["image"] + + return { + "title": f"{article.title} | {blog_setting.site_name}", + "description": description, + "keywords": keywords, + "meta_tags": meta_tags, + "json_ld": structured_data + } + + def _get_category_seo_data(self, context, request, blog_setting): + category_name = context.get('tag_name') + if not category_name: + return None + + category = Category.objects.filter(name=category_name).first() + if not category: + return None + + title = f"{category.name} | {blog_setting.site_name}" + description = strip_tags(category.name) or blog_setting.site_description + keywords = category.name + + # BreadcrumbList structured data for category page + breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}] + breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()}) + + structured_data = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": breadcrumb_items + } + + return { + "title": title, + "description": description, + "keywords": keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def _get_default_seo_data(self, context, request, blog_setting): + # Homepage and other default pages + structured_data = { + "@context": "https://schema.org", + "@type": "WebSite", + "url": request.build_absolute_uri('/'), + "potentialAction": { + "@type": "SearchAction", + "target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}", + "query-input": "required name=search_term_string" + } + } + return { + "title": f"{blog_setting.site_name} | {blog_setting.site_description}", + "description": blog_setting.site_description, + "keywords": blog_setting.site_keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def dispatch_seo_generation(self, metas, context): + request = context.get('request') + if not request: + return metas + + view_name = request.resolver_match.view_name + blog_setting = get_blog_setting() + + seo_data = None + if view_name == 'blog:detailbyid': + seo_data = self._get_article_seo_data(context, request, blog_setting) + elif view_name == 'blog:category_detail': + seo_data = self._get_category_seo_data(context, request, blog_setting) + + if not seo_data: + seo_data = self._get_default_seo_data(context, request, blog_setting) + + json_ld_script = f'' + + return f""" + {seo_data.get("title", "")} + + + {seo_data.get("meta_tags", "")} + {json_ld_script} + """ + +plugin = SeoOptimizerPlugin() diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/__init__.py new file mode 100644 index 00000000..8804fdf8 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/plugin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/plugin.py new file mode 100644 index 00000000..15e9d94e --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/plugins/view_count/plugin.py @@ -0,0 +1,18 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks + + +class ViewCountPlugin(BasePlugin): + PLUGIN_NAME = '文章浏览次数统计' + PLUGIN_DESCRIPTION = '统计文章的浏览次数' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register('after_article_body_get', self.record_view) + + def record_view(self, article, *args, **kwargs): + article.viewed() + + +plugin = ViewCountPlugin() \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/requirements.txt b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/MemcacheStorage.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/MemcacheStorage.py new file mode 100644 index 00000000..38a7990b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/MemcacheStorage.py @@ -0,0 +1,32 @@ +from werobot.session import SessionStorage +from werobot.utils import json_loads, json_dumps + +from djangoblog.utils import cache + + +class MemcacheStorage(SessionStorage): + def __init__(self, prefix='ws_'): + self.prefix = prefix + self.cache = cache + + @property + def is_available(self): + value = "1" + self.set('checkavaliable', value=value) + return value == self.get('checkavaliable') + + def key_name(self, s): + return '{prefix}{s}'.format(prefix=self.prefix, s=s) + + def get(self, id): + id = self.key_name(id) + session_json = self.cache.get(id) or '{}' + return json_loads(session_json) + + def set(self, id, value): + id = self.key_name(id) + self.cache.set(id, json_dumps(value)) + + def delete(self, id): + id = self.key_name(id) + self.cache.delete(id) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/admin.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/admin.py new file mode 100644 index 00000000..f26f4f6b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +# Register your models here. + + +class CommandsAdmin(admin.ModelAdmin): + list_display = ('title', 'command', 'describe') + + +class EmailSendLogAdmin(admin.ModelAdmin): + list_display = ('title', 'emailto', 'send_result', 'creation_time') + readonly_fields = ( + 'title', + 'emailto', + 'send_result', + 'creation_time', + 'content') + + def has_add_permission(self, request): + return False diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/__init__.py @@ -0,0 +1 @@ + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/blogapi.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/blogapi.py new file mode 100644 index 00000000..8a4d6ac4 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/blogapi.py @@ -0,0 +1,27 @@ +from haystack.query import SearchQuerySet + +from blog.models import Article, Category + + +class BlogApi: + def __init__(self): + self.searchqueryset = SearchQuerySet() + self.searchqueryset.auto_query('') + self.__max_takecount__ = 8 + + def search_articles(self, query): + sqs = self.searchqueryset.auto_query(query) + sqs = sqs.load_all() + return sqs[:self.__max_takecount__] + + def get_category_lists(self): + return Category.objects.all() + + def get_category_articles(self, categoryname): + articles = Article.objects.filter(category__name=categoryname) + if articles: + return articles[:self.__max_takecount__] + return None + + def get_recent_articles(self): + return Article.objects.all()[:self.__max_takecount__] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/commonapi.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/commonapi.py new file mode 100644 index 00000000..83ad9ff2 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/api/commonapi.py @@ -0,0 +1,64 @@ +import logging +import os + +import openai + +from servermanager.models import commands + +logger = logging.getLogger(__name__) + +openai.api_key = os.environ.get('OPENAI_API_KEY') +if os.environ.get('HTTP_PROXY'): + openai.proxy = os.environ.get('HTTP_PROXY') + + +class ChatGPT: + + @staticmethod + def chat(prompt): + try: + completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}]) + return completion.choices[0].message.content + except Exception as e: + logger.error(e) + return "服务器出错了" + + +class CommandHandler: + def __init__(self): + self.commands = commands.objects.all() + + def run(self, title): + """ + 运行命令 + :param title: 命令 + :return: 返回命令执行结果 + """ + cmd = list( + filter( + lambda x: x.title.upper() == title.upper(), + self.commands)) + if cmd: + return self.__run_command__(cmd[0].command) + else: + return "未找到相关命令,请输入hepme获得帮助。" + + def __run_command__(self, cmd): + try: + res = os.popen(cmd).read() + return res + except BaseException: + return '命令执行出错!' + + def get_help(self): + rsp = '' + for cmd in self.commands: + rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) + return rsp + + +if __name__ == '__main__': + chatbot = ChatGPT() + prompt = "写一篇1000字关于AI的论文" + print(chatbot.chat(prompt)) diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/apps.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/apps.py new file mode 100644 index 00000000..03cc38d5 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ServermanagerConfig(AppConfig): + name = 'servermanager' diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0001_initial.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0001_initial.py new file mode 100644 index 00000000..bbdbf775 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='commands', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=300, verbose_name='命令标题')), + ('command', models.CharField(max_length=2000, verbose_name='命令')), + ('describe', models.CharField(max_length=300, verbose_name='命令描述')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '命令', + 'verbose_name_plural': '命令', + }, + ), + migrations.CreateModel( + name='EmailSendLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('emailto', models.CharField(max_length=300, verbose_name='收件人')), + ('title', models.CharField(max_length=2000, verbose_name='邮件标题')), + ('content', models.TextField(verbose_name='邮件内容')), + ('send_result', models.BooleanField(default=False, verbose_name='结果')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '邮件发送log', + 'verbose_name_plural': '邮件发送log', + 'ordering': ['-created_time'], + }, + ), + ] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py new file mode 100644 index 00000000..48588574 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servermanager', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='emailsendlog', + options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'}, + ), + migrations.RenameField( + model_name='commands', + old_name='created_time', + new_name='creation_time', + ), + migrations.RenameField( + model_name='commands', + old_name='last_mod_time', + new_name='last_modify_time', + ), + migrations.RenameField( + model_name='emailsendlog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/__init__.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/models.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/models.py new file mode 100644 index 00000000..4326c658 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/models.py @@ -0,0 +1,33 @@ +from django.db import models + + +# Create your models here. +class commands(models.Model): + title = models.CharField('命令标题', max_length=300) + command = models.CharField('命令', max_length=2000) + describe = models.CharField('命令描述', max_length=300) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + last_modify_time = models.DateTimeField('修改时间', auto_now=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '命令' + verbose_name_plural = verbose_name + + +class EmailSendLog(models.Model): + emailto = models.CharField('收件人', max_length=300) + title = models.CharField('邮件标题', max_length=2000) + content = models.TextField('邮件内容') + send_result = models.BooleanField('结果', default=False) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '邮件发送log' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/robot.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/robot.py new file mode 100644 index 00000000..7b457364 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/robot.py @@ -0,0 +1,187 @@ +import logging +import os +import re + +import jsonpickle +from django.conf import settings +from werobot import WeRoBot +from werobot.replies import ArticlesReply, Article +from werobot.session.filestorage import FileStorage + +from djangoblog.utils import get_sha256 +from servermanager.api.blogapi import BlogApi +from servermanager.api.commonapi import ChatGPT, CommandHandler +from .MemcacheStorage import MemcacheStorage + +robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN') + or 'lylinux', enable_session=True) +memstorage = MemcacheStorage() +if memstorage.is_available: + robot.config['SESSION_STORAGE'] = memstorage +else: + if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')): + os.remove(os.path.join(settings.BASE_DIR, 'werobot_session')) + robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session') + +blogapi = BlogApi() +cmd_handler = CommandHandler() +logger = logging.getLogger(__name__) + + +def convert_to_article_reply(articles, message): + reply = ArticlesReply(message=message) + from blog.templatetags.blog_tags import truncatechars_content + for post in articles: + imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body) + imgurl = '' + if imgs: + imgurl = imgs[0] + article = Article( + title=post.title, + description=truncatechars_content(post.body), + img=imgurl, + url=post.get_full_url() + ) + reply.add_article(article) + return reply + + +@robot.filter(re.compile(r"^\?.*")) +def search(message, session): + s = message.content + searchstr = str(s).replace('?', '') + result = blogapi.search_articles(searchstr) + if result: + articles = list(map(lambda x: x.object, result)) + reply = convert_to_article_reply(articles, message) + return reply + else: + return '没有找到相关文章。' + + +@robot.filter(re.compile(r'^category\s*$', re.I)) +def category(message, session): + categorys = blogapi.get_category_lists() + content = ','.join(map(lambda x: x.name, categorys)) + return '所有文章分类目录:' + content + + +@robot.filter(re.compile(r'^recent\s*$', re.I)) +def recents(message, session): + articles = blogapi.get_recent_articles() + if articles: + reply = convert_to_article_reply(articles, message) + return reply + else: + return "暂时还没有文章" + + +@robot.filter(re.compile('^help$', re.I)) +def help(message, session): + return '''欢迎关注! + 默认会与图灵机器人聊天~~ + 你可以通过下面这些命令来获得信息 + ?关键字搜索文章. + 如?python. + category获得文章分类目录及文章数. + category-***获得该分类目录文章 + 如category-python + recent获得最新文章 + help获得帮助. + weather:获得天气 + 如weather:西安 + idcard:获得身份证信息 + 如idcard:61048119xxxxxxxxxx + music:音乐搜索 + 如music:阴天快乐 + PS:以上标点符号都不支持中文标点~~ + ''' + + +@robot.filter(re.compile(r'^weather\:.*$', re.I)) +def weather(message, session): + return "建设中..." + + +@robot.filter(re.compile(r'^idcard\:.*$', re.I)) +def idcard(message, session): + return "建设中..." + + +@robot.handler +def echo(message, session): + handler = MessageHandler(message, session) + return handler.handler() + + +class MessageHandler: + def __init__(self, message, session): + userid = message.source + self.message = message + self.session = session + self.userid = userid + try: + info = session[userid] + self.userinfo = jsonpickle.decode(info) + except Exception as e: + userinfo = WxUserInfo() + self.userinfo = userinfo + + @property + def is_admin(self): + return self.userinfo.isAdmin + + @property + def is_password_set(self): + return self.userinfo.isPasswordSet + + def save_session(self): + info = jsonpickle.encode(self.userinfo) + self.session[self.userid] = info + + def handler(self): + info = self.message.content + + if self.userinfo.isAdmin and info.upper() == 'EXIT': + self.userinfo = WxUserInfo() + self.save_session() + return "退出成功" + if info.upper() == 'ADMIN': + self.userinfo.isAdmin = True + self.save_session() + return "输入管理员密码" + if self.userinfo.isAdmin and not self.userinfo.isPasswordSet: + passwd = settings.WXADMIN + if settings.TESTING: + passwd = '123' + if passwd.upper() == get_sha256(get_sha256(info)).upper(): + self.userinfo.isPasswordSet = True + self.save_session() + return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" + else: + if self.userinfo.Count >= 3: + self.userinfo = WxUserInfo() + self.save_session() + return "超过验证次数" + self.userinfo.Count += 1 + self.save_session() + return "验证失败,请重新输入管理员密码:" + if self.userinfo.isAdmin and self.userinfo.isPasswordSet: + if self.userinfo.Command != '' and info.upper() == 'Y': + return cmd_handler.run(self.userinfo.Command) + else: + if info.upper() == 'HELPME': + return cmd_handler.get_help() + self.userinfo.Command = info + self.save_session() + return "确认执行: " + info + " 命令?" + + return ChatGPT.chat(info) + + +class WxUserInfo(): + def __init__(self): + self.isAdmin = False + self.isPasswordSet = False + self.Count = 0 + self.Command = '' diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/tests.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/tests.py new file mode 100644 index 00000000..22a66892 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/tests.py @@ -0,0 +1,79 @@ +from django.test import Client, RequestFactory, TestCase +from django.utils import timezone +from werobot.messages.messages import TextMessage + +from accounts.models import BlogUser +from blog.models import Category, Article +from servermanager.api.commonapi import ChatGPT +from .models import commands +from .robot import MessageHandler, CommandHandler +from .robot import search, category, recents + + +# Create your tests here. +class ServerManagerTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_chat_gpt(self): + content = ChatGPT.chat("你好") + self.assertIsNotNone(content) + + def test_validate_comment(self): + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + self.client.login(username='liangliangyy1', password='liangliangyy1') + + c = Category() + c.name = "categoryccc" + c.save() + + article = Article() + article.title = "nicetitleccc" + article.body = "nicecontentccc" + article.author = user + article.category = c + article.type = 'a' + article.status = 'p' + article.save() + s = TextMessage([]) + s.content = "nice" + rsp = search(s, None) + rsp = category(None, None) + self.assertIsNotNone(rsp) + rsp = recents(None, None) + self.assertTrue(rsp != '暂时还没有文章') + + cmd = commands() + cmd.title = "test" + cmd.command = "ls" + cmd.describe = "test" + cmd.save() + + cmdhandler = CommandHandler() + rsp = cmdhandler.run('test') + self.assertIsNotNone(rsp) + s.source = 'u' + s.content = 'test' + msghandler = MessageHandler(s, {}) + + # msghandler.userinfo.isPasswordSet = True + # msghandler.userinfo.isAdmin = True + msghandler.handler() + s.content = 'y' + msghandler.handler() + s.content = 'idcard:12321233' + msghandler.handler() + s.content = 'weather:上海' + msghandler.handler() + s.content = 'admin' + msghandler.handler() + s.content = '123' + msghandler.handler() + + s.content = 'exit' + msghandler.handler() diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/urls.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/urls.py new file mode 100644 index 00000000..8d134d27 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from werobot.contrib.django import make_view + +from .robot import robot + +app_name = "servermanager" +urlpatterns = [ + path(r'robot', make_view(robot)), + +] diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/views.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/views.py new file mode 100644 index 00000000..60f00ef0 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/servermanager/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/forget_password.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/forget_password.html new file mode 100644 index 00000000..33845315 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/login.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/login.html new file mode 100644 index 00000000..cff8d334 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/registration_form.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/registration_form.html new file mode 100644 index 00000000..65e7549b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/result.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/result.html new file mode 100644 index 00000000..23c90943 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/account/result.html @@ -0,0 +1,27 @@ +{% extends 'share_layout/base.html' %} +{% load i18n %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_archives.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_archives.html new file mode 100644 index 00000000..959319ee --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_archives.html @@ -0,0 +1,60 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% load i18n %} +{% block header %} + + {% trans 'article archive' %} | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+ +

{% 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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_detail.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_detail.html new file mode 100644 index 00000000..a74a0dbb --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_detail.html @@ -0,0 +1,52 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} + +{% block header %} +{% endblock %} +{% block content %} +
+
+ {% load_article_detail article False user %} + + {% if article.type == 'a' %} + + {% 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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_index.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_index.html new file mode 100644 index 00000000..0ee6150f --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/article_index.html @@ -0,0 +1,42 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ {% if page_type and tag_name %} +
+ +

{{ 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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/error_page.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/error_page.html new file mode 100644 index 00000000..d41cfb60 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/error_page.html @@ -0,0 +1,45 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {% if statuscode == '404' %} + 404 NotFound + {% elif statuscode == '403' %} + Permission Denied + {% elif statuscode == '500' %} + 500 Error + {% else %} + + {% endif %} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+

{{ message }}

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

友情链接

+
+ +
+ +
+
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_info.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_info.html new file mode 100644 index 00000000..3deec44f --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_meta_info.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_meta_info.html new file mode 100644 index 00000000..cb6111c7 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_meta_info.html @@ -0,0 +1,59 @@ +{% load i18n %} +{% load blog_tags %} + + + + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_pagination.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_pagination.html new file mode 100644 index 00000000..95514ff3 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_pagination.html @@ -0,0 +1,17 @@ +{% load i18n %} + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_tag_list.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/article_tag_list.html new file mode 100644 index 00000000..c8ba4740 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/breadcrumb.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/breadcrumb.html new file mode 100644 index 00000000..67087d5d --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/breadcrumb.html @@ -0,0 +1,19 @@ + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/sidebar.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/sidebar.html new file mode 100644 index 00000000..f70544c6 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/blog/tags/sidebar.html @@ -0,0 +1,136 @@ +{% load blog_tags %} +{% load i18n %} + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item.html new file mode 100644 index 00000000..ebb03888 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_item_tree.html new file mode 100644 index 00000000..a9decd1b --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_list.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/comment_list.html new file mode 100644 index 00000000..40921612 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/post_comment.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/comments/tags/post_comment.html new file mode 100644 index 00000000..3ae5a27e --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/bindsuccess.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/bindsuccess.html new file mode 100644 index 00000000..4bee77c6 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/oauth_applications.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/oauth_applications.html new file mode 100644 index 00000000..a841ad26 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/oauth_applications.html @@ -0,0 +1,13 @@ +{% load i18n %} + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/require_email.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/oauth/require_email.html new file mode 100644 index 00000000..3adef121 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_log_dates.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_log_dates.html new file mode 100644 index 00000000..7dbba218 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_log_dates.html @@ -0,0 +1,17 @@ + + + + + 记录日期 + + + +
      + {% for date in results %} +
    • + {{ date }} +
    • + {% endfor %} +
    + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_maps.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_maps.html new file mode 100644 index 00000000..3aeda362 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/owntracks/show_maps.html @@ -0,0 +1,135 @@ + + + + + + + 运动轨迹 + + + +
    + + + + + + + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/indexes/blog/article_text.txt b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/indexes/blog/article_text.txt new file mode 100644 index 00000000..4f9ca767 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/search.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/search/search.html new file mode 100644 index 00000000..1404c604 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/adsense.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/adsense.html new file mode 100644 index 00000000..8f99c55a --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/adsense.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base.html new file mode 100644 index 00000000..75d0df56 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base_account.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/base_account.html new file mode 100644 index 00000000..c00d8421 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/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/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/footer.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/footer.html new file mode 100644 index 00000000..cd86a295 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/footer.html @@ -0,0 +1,56 @@ + + + diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav.html new file mode 100644 index 00000000..24d4da63 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav.html @@ -0,0 +1,30 @@ +{% load i18n %} + + \ No newline at end of file diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav_node.html b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav_node.html new file mode 100644 index 00000000..c2668807 --- /dev/null +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/templates/share_layout/nav_node.html @@ -0,0 +1,19 @@ + + +