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

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

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

Thank you very much for your comments on this site

- You can visit %(article_title)s - to review your comments, - Thank you again! -
- If the link above cannot be opened, please copy this link to your browser. - %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} - tomail = comment.author.email - send_email([tomail], subject, html_content) - try: - if comment.parent_comment: - html_content = _("""Your comment on %(article_title)s
has - received a reply.
%(comment_body)s -
- go check it out! -
- If the link above cannot be opened, please copy this link to your browser. - %(article_url)s - """) % {'article_url': article_url, 'article_title': comment.article.title, - 'comment_body': comment.parent_comment.body} - tomail = comment.parent_comment.author.email - send_email([tomail], subject, html_content) - except Exception as e: - logger.error(e) diff --git a/src/DjangoBlog-master/comments/views.py b/src/DjangoBlog-master/comments/views.py deleted file mode 100644 index ad9b2b9..0000000 --- a/src/DjangoBlog-master/comments/views.py +++ /dev/null @@ -1,63 +0,0 @@ -# Create your views here. -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect -from django.views.generic.edit import FormView - -from accounts.models import BlogUser -from blog.models import Article -from .forms import CommentForm -from .models import Comment - - -class CommentPostView(FormView): - form_class = CommentForm - template_name = 'blog/article_detail.html' - - @method_decorator(csrf_protect) - def dispatch(self, *args, **kwargs): - return super(CommentPostView, self).dispatch(*args, **kwargs) - - def get(self, request, *args, **kwargs): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - url = article.get_absolute_url() - return HttpResponseRedirect(url + "#comments") - - def form_invalid(self, form): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - - return self.render_to_response({ - 'form': form, - 'article': article - }) - - def form_valid(self, form): - """提交的数据验证合法后的逻辑""" - user = self.request.user - author = BlogUser.objects.get(pk=user.pk) - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - - if article.comment_status == 'c' or article.status == 'c': - raise ValidationError("该文章评论已关闭.") - comment = form.save(False) - comment.article = article - from djangoblog.utils import get_blog_setting - settings = get_blog_setting() - if not settings.comment_need_review: - comment.is_enable = True - comment.author = author - - if form.cleaned_data['parent_comment_id']: - parent_comment = Comment.objects.get( - pk=form.cleaned_data['parent_comment_id']) - comment.parent_comment = parent_comment - - comment.save(True) - return HttpResponseRedirect( - "%s#div-comment-%d" % - (article.get_absolute_url(), comment.pk)) diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml deleted file mode 100644 index 83e35ff..0000000 --- a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: '3' - -services: - es: - image: liangliangyy/elasticsearch-analysis-ik:8.6.1 - container_name: es - restart: always - environment: - - discovery.type=single-node - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - ports: - - 9200:9200 - volumes: - - ./bin/datas/es/:/usr/share/elasticsearch/data/ - - kibana: - image: kibana:8.6.1 - restart: always - container_name: kibana - ports: - - 5601:5601 - environment: - - ELASTICSEARCH_HOSTS=http://es:9200 - - djangoblog: - build: . - restart: always - command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' - ports: - - "8000:8000" - volumes: - - ./collectedstatic:/code/djangoblog/collectedstatic - - ./uploads:/code/djangoblog/uploads - environment: - - DJANGO_MYSQL_DATABASE=djangoblog - - DJANGO_MYSQL_USER=root - - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - - DJANGO_MYSQL_HOST=db - - DJANGO_MYSQL_PORT=3306 - - DJANGO_MEMCACHED_LOCATION=memcached:11211 - - DJANGO_ELASTICSEARCH_HOST=es:9200 - links: - - db - - memcached - depends_on: - - db - container_name: djangoblog - diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml deleted file mode 100644 index 9609af3..0000000 --- a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml +++ /dev/null @@ -1,60 +0,0 @@ -version: '3' - -services: - db: - image: mysql:latest - restart: always - environment: - - MYSQL_DATABASE=djangoblog - - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E - ports: - - 3306:3306 - volumes: - - ./bin/datas/mysql/:/var/lib/mysql - depends_on: - - redis - container_name: db - - djangoblog: - build: - context: ../../ - restart: always - command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' - ports: - - "8000:8000" - volumes: - - ./collectedstatic:/code/djangoblog/collectedstatic - - ./logs:/code/djangoblog/logs - - ./uploads:/code/djangoblog/uploads - environment: - - DJANGO_MYSQL_DATABASE=djangoblog - - DJANGO_MYSQL_USER=root - - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - - DJANGO_MYSQL_HOST=db - - DJANGO_MYSQL_PORT=3306 - - DJANGO_REDIS_URL=redis:6379 - links: - - db - - redis - depends_on: - - db - container_name: djangoblog - nginx: - restart: always - image: nginx:latest - ports: - - "80:80" - - "443:443" - volumes: - - ./bin/nginx.conf:/etc/nginx/nginx.conf - - ./collectedstatic:/code/djangoblog/collectedstatic - links: - - djangoblog:djangoblog - container_name: nginx - - redis: - restart: always - image: redis:latest - container_name: redis - ports: - - "6379:6379" diff --git a/src/DjangoBlog-master/deploy/entrypoint.sh b/src/DjangoBlog-master/deploy/entrypoint.sh deleted file mode 100644 index 2fb6491..0000000 --- a/src/DjangoBlog-master/deploy/entrypoint.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -NAME="djangoblog" -DJANGODIR=/code/djangoblog -USER=root -GROUP=root -NUM_WORKERS=1 -DJANGO_WSGI_MODULE=djangoblog.wsgi - - -echo "Starting $NAME as `whoami`" - -cd $DJANGODIR - -export PYTHONPATH=$DJANGODIR:$PYTHONPATH - -python manage.py makemigrations && \ - python manage.py migrate && \ - python manage.py collectstatic --noinput && \ - python manage.py compress --force && \ - python manage.py build_index && \ - python manage.py compilemessages || exit 1 - -exec gunicorn ${DJANGO_WSGI_MODULE}:application \ ---name $NAME \ ---workers $NUM_WORKERS \ ---user=$USER --group=$GROUP \ ---bind 0.0.0.0:8000 \ ---log-level=debug \ ---log-file=- \ ---worker-class gevent \ ---threads 4 diff --git a/src/DjangoBlog-master/deploy/k8s/configmap.yaml b/src/DjangoBlog-master/deploy/k8s/configmap.yaml deleted file mode 100644 index 835d4ad..0000000 --- a/src/DjangoBlog-master/deploy/k8s/configmap.yaml +++ /dev/null @@ -1,119 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: web-nginx-config - namespace: djangoblog -data: - nginx.conf: | - user nginx; - worker_processes auto; - error_log /var/log/nginx/error.log notice; - pid /var/run/nginx.pid; - - events { - worker_connections 1024; - multi_accept on; - use epoll; - } - - http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - keepalive_timeout 65; - gzip on; - gzip_disable "msie6"; - - gzip_vary on; - gzip_proxied any; - gzip_comp_level 8; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; - - # Include server configurations - include /etc/nginx/conf.d/*.conf; - } - djangoblog.conf: | - server { - server_name lylinux.net; - root /code/djangoblog/collectedstatic/; - listen 80; - keepalive_timeout 70; - location /static/ { - expires max; - alias /code/djangoblog/collectedstatic/; - } - - location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ { - root /resource/djangopub; - expires 1d; - access_log off; - error_log off; - } - - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - proxy_redirect off; - if (!-f $request_filename) { - proxy_pass http://djangoblog:8000; - break; - } - } - } - server { - server_name www.lylinux.net; - listen 80; - return 301 https://lylinux.net$request_uri; - } - resource.lylinux.net.conf: | - server { - index index.html index.htm; - server_name resource.lylinux.net; - root /resource/; - - location /djangoblog/ { - alias /code/djangoblog/collectedstatic/; - } - - access_log off; - error_log off; - include lylinux/resource.conf; - } - lylinux.resource.conf: | - expires max; - access_log off; - log_not_found off; - add_header Pragma public; - add_header Cache-Control "public"; - add_header "Access-Control-Allow-Origin" "*"; - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: djangoblog-env - namespace: djangoblog -data: - DJANGO_MYSQL_DATABASE: djangoblog - DJANGO_MYSQL_USER: db_user - DJANGO_MYSQL_PASSWORD: db_password - DJANGO_MYSQL_HOST: db_host - DJANGO_MYSQL_PORT: db_port - DJANGO_REDIS_URL: "redis:6379" - DJANGO_DEBUG: "False" - MYSQL_ROOT_PASSWORD: db_password - MYSQL_DATABASE: djangoblog - MYSQL_PASSWORD: db_password - DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx - diff --git a/src/DjangoBlog-master/deploy/k8s/deployment.yaml b/src/DjangoBlog-master/deploy/k8s/deployment.yaml deleted file mode 100644 index 414fdcc..0000000 --- a/src/DjangoBlog-master/deploy/k8s/deployment.yaml +++ /dev/null @@ -1,274 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: djangoblog - namespace: djangoblog - labels: - app: djangoblog -spec: - replicas: 3 - selector: - matchLabels: - app: djangoblog - template: - metadata: - labels: - app: djangoblog - spec: - containers: - - name: djangoblog - image: liangliangyy/djangoblog:latest - imagePullPolicy: Always - ports: - - containerPort: 8000 - envFrom: - - configMapRef: - name: djangoblog-env - readinessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 10 - periodSeconds: 30 - livenessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 10 - periodSeconds: 30 - resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: "2" - memory: 2Gi - volumeMounts: - - name: djangoblog - mountPath: /code/djangoblog/collectedstatic - - name: resource - mountPath: /resource - volumes: - - name: djangoblog - persistentVolumeClaim: - claimName: djangoblog-pvc - - name: resource - persistentVolumeClaim: - claimName: resource-pvc - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - namespace: djangoblog - labels: - app: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: redis:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 6379 - resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: 200m - memory: 2Gi - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: db - namespace: djangoblog - labels: - app: db -spec: - replicas: 1 - selector: - matchLabels: - app: db - template: - metadata: - labels: - app: db - spec: - containers: - - name: db - image: mysql:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 3306 - envFrom: - - configMapRef: - name: djangoblog-env - readinessProbe: - exec: - command: - - mysqladmin - - ping - - "-h" - - "127.0.0.1" - - "-u" - - "root" - - "-p$MYSQL_ROOT_PASSWORD" - initialDelaySeconds: 10 - periodSeconds: 10 - livenessProbe: - exec: - command: - - mysqladmin - - ping - - "-h" - - "127.0.0.1" - - "-u" - - "root" - - "-p$MYSQL_ROOT_PASSWORD" - initialDelaySeconds: 10 - periodSeconds: 10 - resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: "2" - memory: 2Gi - volumeMounts: - - name: db-data - mountPath: /var/lib/mysql - volumes: - - name: db-data - persistentVolumeClaim: - claimName: db-pvc - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - namespace: djangoblog - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: "2" - memory: 2Gi - volumeMounts: - - name: nginx-config - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - - name: nginx-config - mountPath: /etc/nginx/conf.d/default.conf - subPath: djangoblog.conf - - name: nginx-config - mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf - subPath: resource.lylinux.net.conf - - name: nginx-config - mountPath: /etc/nginx/lylinux/resource.conf - subPath: lylinux.resource.conf - - name: djangoblog-pvc - mountPath: /code/djangoblog/collectedstatic - - name: resource-pvc - mountPath: /resource - volumes: - - name: nginx-config - configMap: - name: web-nginx-config - - name: djangoblog-pvc - persistentVolumeClaim: - claimName: djangoblog-pvc - - name: resource-pvc - persistentVolumeClaim: - claimName: resource-pvc - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: elasticsearch - namespace: djangoblog - labels: - app: elasticsearch -spec: - replicas: 1 - selector: - matchLabels: - app: elasticsearch - template: - metadata: - labels: - app: elasticsearch - spec: - containers: - - name: elasticsearch - image: liangliangyy/elasticsearch-analysis-ik:8.6.1 - imagePullPolicy: IfNotPresent - env: - - name: discovery.type - value: single-node - - name: ES_JAVA_OPTS - value: "-Xms256m -Xmx256m" - - name: xpack.security.enabled - value: "false" - - name: xpack.monitoring.templates.enabled - value: "false" - ports: - - containerPort: 9200 - resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: "2" - memory: 2Gi - readinessProbe: - httpGet: - path: / - port: 9200 - initialDelaySeconds: 15 - periodSeconds: 30 - livenessProbe: - httpGet: - path: / - port: 9200 - initialDelaySeconds: 15 - periodSeconds: 30 - volumeMounts: - - name: elasticsearch-data - mountPath: /usr/share/elasticsearch/data/ - volumes: - - name: elasticsearch-data - persistentVolumeClaim: - claimName: elasticsearch-pvc diff --git a/src/DjangoBlog-master/deploy/k8s/gateway.yaml b/src/DjangoBlog-master/deploy/k8s/gateway.yaml deleted file mode 100644 index a8de073..0000000 --- a/src/DjangoBlog-master/deploy/k8s/gateway.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nginx - namespace: djangoblog -spec: - ingressClassName: nginx - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: nginx - port: - number: 80 \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/pv.yaml b/src/DjangoBlog-master/deploy/k8s/pv.yaml deleted file mode 100644 index 874b72f..0000000 --- a/src/DjangoBlog-master/deploy/k8s/pv.yaml +++ /dev/null @@ -1,94 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: local-pv-db -spec: - capacity: - storage: 10Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage - local: - path: /mnt/local-storage-db - nodeAffinity: - required: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: In - values: - - master ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: local-pv-djangoblog -spec: - capacity: - storage: 5Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage - local: - path: /mnt/local-storage-djangoblog - nodeAffinity: - required: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: In - values: - - master - - ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: local-pv-resource -spec: - capacity: - storage: 5Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage - local: - path: /mnt/resource/ - nodeAffinity: - required: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: In - values: - - master - ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: local-pv-elasticsearch -spec: - capacity: - storage: 5Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage - local: - path: /mnt/local-storage-elasticsearch - nodeAffinity: - required: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: In - values: - - master \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/pvc.yaml b/src/DjangoBlog-master/deploy/k8s/pvc.yaml deleted file mode 100644 index ef238c5..0000000 --- a/src/DjangoBlog-master/deploy/k8s/pvc.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: db-pvc - namespace: djangoblog -spec: - storageClassName: local-storage - volumeName: local-pv-db - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - - ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: djangoblog-pvc - namespace: djangoblog -spec: - volumeName: local-pv-djangoblog - storageClassName: local-storage - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi - ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: resource-pvc - namespace: djangoblog -spec: - volumeName: local-pv-resource - storageClassName: local-storage - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi - ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: elasticsearch-pvc - namespace: djangoblog -spec: - volumeName: local-pv-elasticsearch - storageClassName: local-storage - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi - \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/service.yaml b/src/DjangoBlog-master/deploy/k8s/service.yaml deleted file mode 100644 index 4ef2931..0000000 --- a/src/DjangoBlog-master/deploy/k8s/service.yaml +++ /dev/null @@ -1,80 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: djangoblog - namespace: djangoblog - labels: - app: djangoblog -spec: - selector: - app: djangoblog - ports: - - protocol: TCP - port: 8000 - targetPort: 8000 - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx - namespace: djangoblog - labels: - app: nginx -spec: - selector: - app: nginx - ports: - - protocol: TCP - port: 80 - targetPort: 80 - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: redis - namespace: djangoblog - labels: - app: redis -spec: - selector: - app: redis - ports: - - protocol: TCP - port: 6379 - targetPort: 6379 - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: db - namespace: djangoblog - labels: - app: db -spec: - selector: - app: db - ports: - - protocol: TCP - port: 3306 - targetPort: 3306 - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - name: elasticsearch - namespace: djangoblog - labels: - app: elasticsearch -spec: - selector: - app: elasticsearch - ports: - - protocol: TCP - port: 9200 - targetPort: 9200 - type: ClusterIP - diff --git a/src/DjangoBlog-master/deploy/k8s/storageclass.yaml b/src/DjangoBlog-master/deploy/k8s/storageclass.yaml deleted file mode 100644 index 5d5a14c..0000000 --- a/src/DjangoBlog-master/deploy/k8s/storageclass.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: local-storage - annotations: - storageclass.kubernetes.io/is-default-class: "true" -provisioner: kubernetes.io/no-provisioner -volumeBindingMode: Immediate - - diff --git a/src/DjangoBlog-master/deploy/nginx.conf b/src/DjangoBlog-master/deploy/nginx.conf deleted file mode 100644 index 32161d8..0000000 --- a/src/DjangoBlog-master/deploy/nginx.conf +++ /dev/null @@ -1,50 +0,0 @@ -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log notice; -pid /var/run/nginx.pid; - - -events { - worker_connections 1024; -} - - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - #gzip on; - - server { - root /code/djangoblog/collectedstatic/; - listen 80; - keepalive_timeout 70; - location /static/ { - expires max; - alias /code/djangoblog/collectedstatic/; - } - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - proxy_redirect off; - if (!-f $request_filename) { - proxy_pass http://djangoblog:8000; - break; - } - } - } -} diff --git a/src/DjangoBlog-master/djangoblog/__init__.py b/src/DjangoBlog-master/djangoblog/__init__.py deleted file mode 100644 index 1e205f4..0000000 --- a/src/DjangoBlog-master/djangoblog/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/src/DjangoBlog-master/djangoblog/admin_site.py b/src/DjangoBlog-master/djangoblog/admin_site.py deleted file mode 100644 index f120405..0000000 --- a/src/DjangoBlog-master/djangoblog/admin_site.py +++ /dev/null @@ -1,64 +0,0 @@ -from django.contrib.admin import AdminSite -from django.contrib.admin.models import LogEntry -from django.contrib.sites.admin import SiteAdmin -from django.contrib.sites.models import Site - -from accounts.admin import * -from blog.admin import * -from blog.models import * -from comments.admin import * -from comments.models import * -from djangoblog.logentryadmin import LogEntryAdmin -from oauth.admin import * -from oauth.models import * -from owntracks.admin import * -from owntracks.models import * -from servermanager.admin import * -from servermanager.models import * - - -class DjangoBlogAdminSite(AdminSite): - site_header = 'djangoblog administration' - site_title = 'djangoblog site admin' - - def __init__(self, name='admin'): - super().__init__(name) - - def has_permission(self, request): - return request.user.is_superuser - - # def get_urls(self): - # urls = super().get_urls() - # from django.urls import path - # from blog.views import refresh_memcache - # - # my_urls = [ - # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), - # ] - # return urls + my_urls - - -admin_site = DjangoBlogAdminSite(name='admin') - -admin_site.register(Article, ArticlelAdmin) -admin_site.register(Category, CategoryAdmin) -admin_site.register(Tag, TagAdmin) -admin_site.register(Links, LinksAdmin) -admin_site.register(SideBar, SideBarAdmin) -admin_site.register(BlogSettings, BlogSettingsAdmin) - -admin_site.register(commands, CommandsAdmin) -admin_site.register(EmailSendLog, EmailSendLogAdmin) - -admin_site.register(BlogUser, BlogUserAdmin) - -admin_site.register(Comment, CommentAdmin) - -admin_site.register(OAuthUser, OAuthUserAdmin) -admin_site.register(OAuthConfig, OAuthConfigAdmin) - -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) - -admin_site.register(Site, SiteAdmin) - -admin_site.register(LogEntry, LogEntryAdmin) diff --git a/src/DjangoBlog-master/djangoblog/apps.py b/src/DjangoBlog-master/djangoblog/apps.py deleted file mode 100644 index d29e318..0000000 --- a/src/DjangoBlog-master/djangoblog/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.apps import AppConfig - -class DjangoblogAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'djangoblog' - - def ready(self): - super().ready() - # Import and load plugins here - from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/blog_signals.py b/src/DjangoBlog-master/djangoblog/blog_signals.py deleted file mode 100644 index 393f441..0000000 --- a/src/DjangoBlog-master/djangoblog/blog_signals.py +++ /dev/null @@ -1,122 +0,0 @@ -import _thread -import logging - -import django.dispatch -from django.conf import settings -from django.contrib.admin.models import LogEntry -from django.contrib.auth.signals import user_logged_in, user_logged_out -from django.core.mail import EmailMultiAlternatives -from django.db.models.signals import post_save -from django.dispatch import receiver - -from comments.models import Comment -from comments.utils import send_comment_email -from djangoblog.spider_notify import SpiderNotify -from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache -from djangoblog.utils import get_current_site -from oauth.models import OAuthUser - -logger = logging.getLogger(__name__) - -oauth_user_login_signal = django.dispatch.Signal(['id']) -send_email_signal = django.dispatch.Signal( - ['emailto', 'title', 'content']) - - -@receiver(send_email_signal) -def send_email_signal_handler(sender, **kwargs): - emailto = kwargs['emailto'] - title = kwargs['title'] - content = kwargs['content'] - - msg = EmailMultiAlternatives( - title, - content, - from_email=settings.DEFAULT_FROM_EMAIL, - to=emailto) - msg.content_subtype = "html" - - from servermanager.models import EmailSendLog - log = EmailSendLog() - log.title = title - log.content = content - log.emailto = ','.join(emailto) - - try: - result = msg.send() - log.send_result = result > 0 - except Exception as e: - logger.error(f"失败邮箱号: {emailto}, {e}") - log.send_result = False - log.save() - - -@receiver(oauth_user_login_signal) -def oauth_user_login_signal_handler(sender, **kwargs): - id = kwargs['id'] - oauthuser = OAuthUser.objects.get(id=id) - site = get_current_site().domain - if oauthuser.picture and not oauthuser.picture.find(site) >= 0: - from djangoblog.utils import save_user_avatar - oauthuser.picture = save_user_avatar(oauthuser.picture) - oauthuser.save() - - delete_sidebar_cache() - - -@receiver(post_save) -def model_post_save_callback( - sender, - instance, - created, - raw, - using, - update_fields, - **kwargs): - clearcache = False - if isinstance(instance, LogEntry): - return - if 'get_full_url' in dir(instance): - is_update_views = update_fields == {'views'} - if not settings.TESTING and not is_update_views: - try: - notify_url = instance.get_full_url() - SpiderNotify.baidu_notify([notify_url]) - except Exception as ex: - logger.error("notify sipder", ex) - if not is_update_views: - clearcache = True - - if isinstance(instance, Comment): - if instance.is_enable: - path = instance.article.get_absolute_url() - site = get_current_site().domain - if site.find(':') > 0: - site = site[0:site.find(':')] - - expire_view_cache( - path, - servername=site, - serverport=80, - key_prefix='blogdetail') - if cache.get('seo_processor'): - cache.delete('seo_processor') - comment_cache_key = 'article_comments_{id}'.format( - id=instance.article.id) - cache.delete(comment_cache_key) - delete_sidebar_cache() - delete_view_cache('article_comments', [str(instance.article.pk)]) - - _thread.start_new_thread(send_comment_email, (instance,)) - - if clearcache: - cache.clear() - - -@receiver(user_logged_in) -@receiver(user_logged_out) -def user_auth_callback(sender, request, user, **kwargs): - if user and user.username: - logger.info(user) - delete_sidebar_cache() - # cache.clear() diff --git a/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py b/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py deleted file mode 100644 index 4afe498..0000000 --- a/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py +++ /dev/null @@ -1,183 +0,0 @@ -from django.utils.encoding import force_str -from elasticsearch_dsl import Q -from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query -from haystack.forms import ModelSearchForm -from haystack.models import SearchResult -from haystack.utils import log as logging - -from blog.documents import ArticleDocument, ArticleDocumentManager -from blog.models import Article - -logger = logging.getLogger(__name__) - - -class ElasticSearchBackend(BaseSearchBackend): - def __init__(self, connection_alias, **connection_options): - super( - ElasticSearchBackend, - self).__init__( - connection_alias, - **connection_options) - self.manager = ArticleDocumentManager() - self.include_spelling = True - - def _get_models(self, iterable): - models = iterable if iterable and iterable[0] else Article.objects.all() - docs = self.manager.convert_to_doc(models) - return docs - - def _create(self, models): - self.manager.create_index() - docs = self._get_models(models) - self.manager.rebuild(docs) - - def _delete(self, models): - for m in models: - m.delete() - return True - - def _rebuild(self, models): - models = models if models else Article.objects.all() - docs = self.manager.convert_to_doc(models) - self.manager.update_docs(docs) - - def update(self, index, iterable, commit=True): - - models = self._get_models(iterable) - self.manager.update_docs(models) - - def remove(self, obj_or_string): - models = self._get_models([obj_or_string]) - self._delete(models) - - def clear(self, models=None, commit=True): - self.remove(None) - - @staticmethod - def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" - - search = ArticleDocument.search() \ - .query("match", body=query) \ - .suggest('suggest_search', query, term={'field': 'body'}) \ - .execute() - - keywords = [] - for suggest in search.suggest.suggest_search: - if suggest["options"]: - keywords.append(suggest["options"][0]["text"]) - else: - keywords.append(suggest["text"]) - - return ' '.join(keywords) - - @log_query - def search(self, query_string, **kwargs): - logger.info('search query_string:' + query_string) - - start_offset = kwargs.get('start_offset') - end_offset = kwargs.get('end_offset') - - # 推荐词搜索 - if getattr(self, "is_suggest", None): - suggestion = self.get_suggestion(query_string) - else: - suggestion = query_string - - q = Q('bool', - should=[Q('match', body=suggestion), Q('match', title=suggestion)], - minimum_should_match="70%") - - search = ArticleDocument.search() \ - .query('bool', filter=[q]) \ - .filter('term', status='p') \ - .filter('term', type='a') \ - .source(False)[start_offset: end_offset] - - results = search.execute() - hits = results['hits'].total - raw_results = [] - for raw_result in results['hits']['hits']: - app_label = 'blog' - model_name = 'Article' - additional_fields = {} - - result_class = SearchResult - - result = result_class( - app_label, - model_name, - raw_result['_id'], - raw_result['_score'], - **additional_fields) - raw_results.append(result) - facets = {} - spelling_suggestion = None if query_string == suggestion else suggestion - - return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, - } - - -class ElasticSearchQuery(BaseSearchQuery): - def _convert_datetime(self, date): - if hasattr(date, 'hour'): - return force_str(date.strftime('%Y%m%d%H%M%S')) - else: - return force_str(date.strftime('%Y%m%d000000')) - - def clean(self, query_fragment): - """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. - - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. - """ - words = query_fragment.split() - cleaned_words = [] - - for word in words: - if word in self.backend.RESERVED_WORDS: - word = word.replace(word, word.lower()) - - for char in self.backend.RESERVED_CHARACTERS: - if char in word: - word = "'%s'" % word - break - - cleaned_words.append(word) - - return ' '.join(cleaned_words) - - def build_query_fragment(self, field, filter_type, value): - return value.query_string - - def get_count(self): - results = self.get_results() - return len(results) if results else 0 - - def get_spelling_suggestion(self, preferred_query=None): - return self._spelling_suggestion - - def build_params(self, spelling_query=None): - kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) - return kwargs - - -class ElasticSearchModelSearchForm(ModelSearchForm): - - def search(self): - # 是否建议搜索 - self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" - sqs = super().search() - return sqs - - -class ElasticSearchEngine(BaseEngine): - backend = ElasticSearchBackend - query = ElasticSearchQuery diff --git a/src/DjangoBlog-master/djangoblog/feeds.py b/src/DjangoBlog-master/djangoblog/feeds.py deleted file mode 100644 index 8c4e851..0000000 --- a/src/DjangoBlog-master/djangoblog/feeds.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.contrib.auth import get_user_model -from django.contrib.syndication.views import Feed -from django.utils import timezone -from django.utils.feedgenerator import Rss201rev2Feed - -from blog.models import Article -from djangoblog.utils import CommonMarkdown - - -class DjangoBlogFeed(Feed): - feed_type = Rss201rev2Feed - - description = '大巧无工,重剑无锋.' - title = "且听风吟 大巧无工,重剑无锋. " - link = "/feed/" - - def author_name(self): - return get_user_model().objects.first().nickname - - def author_link(self): - return get_user_model().objects.first().get_absolute_url() - - def items(self): - return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] - - def item_title(self, item): - return item.title - - def item_description(self, item): - return CommonMarkdown.get_markdown(item.body) - - def feed_copyright(self): - now = timezone.now() - return "Copyright© {year} 且听风吟".format(year=now.year) - - def item_link(self, item): - return item.get_absolute_url() - - def item_guid(self, item): - return diff --git a/src/DjangoBlog-master/djangoblog/logentryadmin.py b/src/DjangoBlog-master/djangoblog/logentryadmin.py deleted file mode 100644 index 2f6a535..0000000 --- a/src/DjangoBlog-master/djangoblog/logentryadmin.py +++ /dev/null @@ -1,91 +0,0 @@ -from django.contrib import admin -from django.contrib.admin.models import DELETION -from django.contrib.contenttypes.models import ContentType -from django.urls import reverse, NoReverseMatch -from django.utils.encoding import force_str -from django.utils.html import escape -from django.utils.safestring import mark_safe -from django.utils.translation import gettext_lazy as _ - - -class LogEntryAdmin(admin.ModelAdmin): - list_filter = [ - 'content_type' - ] - - search_fields = [ - 'object_repr', - 'change_message' - ] - - list_display_links = [ - 'action_time', - 'get_change_message', - ] - list_display = [ - 'action_time', - 'user_link', - 'content_type', - 'object_link', - 'get_change_message', - ] - - def has_add_permission(self, request): - return False - - def has_change_permission(self, request, obj=None): - return ( - request.user.is_superuser or - request.user.has_perm('admin.change_logentry') - ) and request.method != 'POST' - - def has_delete_permission(self, request, obj=None): - return False - - def object_link(self, obj): - object_link = escape(obj.object_repr) - content_type = obj.content_type - - if obj.action_flag != DELETION and content_type is not None: - # try returning an actual link instead of object repr string - try: - url = reverse( - 'admin:{}_{}_change'.format(content_type.app_label, - content_type.model), - args=[obj.object_id] - ) - object_link = '{}'.format(url, object_link) - except NoReverseMatch: - pass - return mark_safe(object_link) - - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') - - def user_link(self, obj): - content_type = ContentType.objects.get_for_model(type(obj.user)) - user_link = escape(force_str(obj.user)) - try: - # try returning an actual link instead of object repr string - url = reverse( - 'admin:{}_{}_change'.format(content_type.app_label, - content_type.model), - args=[obj.user.pk] - ) - user_link = '{}'.format(url, user_link) - except NoReverseMatch: - pass - return mark_safe(user_link) - - user_link.admin_order_field = 'user' - user_link.short_description = _('user') - - def get_queryset(self, request): - queryset = super(LogEntryAdmin, self).get_queryset(request) - return queryset.prefetch_related('content_type') - - def get_actions(self, request): - actions = super(LogEntryAdmin, self).get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] - return actions diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py b/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py deleted file mode 100644 index 2b4be5c..0000000 --- a/src/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - - -class BasePlugin: - # 插件元数据 - PLUGIN_NAME = None - PLUGIN_DESCRIPTION = None - PLUGIN_VERSION = None - - def __init__(self): - if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): - raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") - self.init_plugin() - self.register_hooks() - - def init_plugin(self): - """ - 插件初始化逻辑 - 子类可以重写此方法来实现特定的初始化操作 - """ - logger.info(f'{self.PLUGIN_NAME} initialized.') - - def register_hooks(self): - """ - 注册插件钩子 - 子类可以重写此方法来注册特定的钩子 - """ - pass - - def get_plugin_info(self): - """ - 获取插件信息 - :return: 包含插件元数据的字典 - """ - return { - 'name': self.PLUGIN_NAME, - 'description': self.PLUGIN_DESCRIPTION, - 'version': self.PLUGIN_VERSION - } diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py b/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py deleted file mode 100644 index 6685b7c..0000000 --- a/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py +++ /dev/null @@ -1,7 +0,0 @@ -ARTICLE_DETAIL_LOAD = 'article_detail_load' -ARTICLE_CREATE = 'article_create' -ARTICLE_UPDATE = 'article_update' -ARTICLE_DELETE = 'article_delete' - -ARTICLE_CONTENT_HOOK_NAME = "the_content" - diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py b/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py deleted file mode 100644 index d712540..0000000 --- a/src/DjangoBlog-master/djangoblog/plugin_manage/hooks.py +++ /dev/null @@ -1,44 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - -_hooks = {} - - -def register(hook_name: str, callback: callable): - """ - 注册一个钩子回调。 - """ - if hook_name not in _hooks: - _hooks[hook_name] = [] - _hooks[hook_name].append(callback) - logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") - - -def run_action(hook_name: str, *args, **kwargs): - """ - 执行一个 Action Hook。 - 它会按顺序执行所有注册到该钩子上的回调函数。 - """ - if hook_name in _hooks: - logger.debug(f"Running action hook '{hook_name}'") - for callback in _hooks[hook_name]: - try: - callback(*args, **kwargs) - except Exception as e: - logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) - - -def apply_filters(hook_name: str, value, *args, **kwargs): - """ - 执行一个 Filter Hook。 - 它会把 value 依次传递给所有注册的回调函数进行处理。 - """ - if hook_name in _hooks: - logger.debug(f"Applying filter hook '{hook_name}'") - for callback in _hooks[hook_name]: - try: - value = callback(value, *args, **kwargs) - except Exception as e: - logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) - return value diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py b/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py deleted file mode 100644 index 12e824b..0000000 --- a/src/DjangoBlog-master/djangoblog/plugin_manage/loader.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -import logging -from django.conf import settings - -logger = logging.getLogger(__name__) - -def load_plugins(): - """ - Dynamically loads and initializes plugins from the 'plugins' directory. - This function is intended to be called when the Django app registry is ready. - """ - for plugin_name in settings.ACTIVE_PLUGINS: - plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) - if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): - try: - __import__(f'plugins.{plugin_name}.plugin') - logger.info(f"Successfully loaded plugin: {plugin_name}") - except ImportError as e: - logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/settings.py b/src/DjangoBlog-master/djangoblog/settings.py deleted file mode 100644 index ed7f1e6..0000000 --- a/src/DjangoBlog-master/djangoblog/settings.py +++ /dev/null @@ -1,343 +0,0 @@ -""" -Django settings for djangoblog project. - -Generated by 'django-admin startproject' using Django 1.10.2. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.10/ref/settings/ -""" -import os -import sys -from pathlib import Path - -from django.utils.translation import gettext_lazy as _ - - -def env_to_bool(env, default): - str_val = os.environ.get(env) - return default if str_val is None else str_val == 'True' - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get( - 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env_to_bool('DJANGO_DEBUG', True) -# DEBUG = False -TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' - -# ALLOWED_HOSTS = [] -ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] -# django 4.0新增配置 -CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition - - -INSTALLED_APPS = [ - # 'django.contrib.admin', - 'django.contrib.admin.apps.SimpleAdminConfig', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor', - 'djangoblog' -] - -MIDDLEWARE = [ - - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.gzip.GZipMiddleware', - # 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.common.CommonMiddleware', - # 'django.middleware.cache.FetchFromCacheMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'blog.middleware.OnlineMiddleware' -] - -ROOT_URLCONF = 'djangoblog.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'blog.context_processors.seo_processor' - ], - }, - }, -] - -WSGI_APPLICATION = 'djangoblog.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'LY181828', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int( - os.environ.get('DJANGO_MYSQL_PORT') or 3306), - 'OPTIONS': { - 'charset': 'utf8mb4'}, - }} - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -LANGUAGES = ( - ('en', _('English')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), -) -LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale'), -) - -LANGUAGE_CODE = 'zh-hans' - -TIME_ZONE = 'Asia/Shanghai' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = False - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - - -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', - 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), - }, -} -# Automatically update searching index -HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# Allow user login with username and password -AUTHENTICATION_BACKENDS = [ - 'accounts.user_login_backend.EmailOrUsernameModelBackend'] - -STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') - -STATIC_URL = '/static/' -STATICFILES = os.path.join(BASE_DIR, 'static') - -AUTH_USER_MODEL = 'accounts.BlogUser' -LOGIN_URL = '/login/' - -TIME_FORMAT = '%Y-%m-%d %H:%M:%S' -DATE_TIME_FORMAT = '%Y-%m-%d' - -# bootstrap color styles -BOOTSTRAP_COLOR_TYPES = [ - 'default', 'primary', 'success', 'info', 'warning', 'danger' -] - -# paginate -PAGINATE_BY = 10 -# http cache timeout -CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 10800, - 'LOCATION': 'unique-snowflake', - } -} -# 使用redis作为缓存 -if os.environ.get("DJANGO_REDIS_URL"): - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', - } - } - -SITE_ID = 1 -BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ - or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' - -# Email: -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) -EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) -EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' -EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) -EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') -EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = EMAIL_HOST_USER -# Setting debug=false did NOT handle except email notifications -ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] -# WX ADMIN password(Two times md5) -WXADMIN = os.environ.get( - 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' - -LOG_PATH = os.path.join(BASE_DIR, 'logs') -if not os.path.exists(LOG_PATH): - os.makedirs(LOG_PATH, exist_ok=True) - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'root': { - 'level': 'INFO', - 'handlers': ['console', 'log_file'], - }, - 'formatters': { - 'verbose': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', - } - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'log_file': { - 'level': 'INFO', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), - 'when': 'D', - 'formatter': 'verbose', - 'interval': 1, - 'delay': True, - 'backupCount': 5, - 'encoding': 'utf-8' - }, - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' - }, - 'null': { - 'class': 'logging.NullHandler', - }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'djangoblog': { - 'handlers': ['log_file', 'console'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, - } - } -} - -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other - 'compressor.finders.CompressorFinder', -) -COMPRESS_ENABLED = True -# COMPRESS_OFFLINE = True - - -COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones - 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer - 'compressor.filters.cssmin.CSSMinFilter' -] -COMPRESS_JS_FILTERS = [ - 'compressor.filters.jsmin.JSMinFilter' -] - -MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') -MEDIA_URL = '/media/' -X_FRAME_OPTIONS = 'SAMEORIGIN' - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): - ELASTICSEARCH_DSL = { - 'default': { - 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') - }, - } - HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', - }, - } - -# Plugin System -PLUGINS_DIR = BASE_DIR / 'plugins' -ACTIVE_PLUGINS = [ - 'article_copyright', - 'reading_time', - 'external_links', - 'view_count', - 'seo_optimize', - ] \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/sitemap.py b/src/DjangoBlog-master/djangoblog/sitemap.py deleted file mode 100644 index 8b7d446..0000000 --- a/src/DjangoBlog-master/djangoblog/sitemap.py +++ /dev/null @@ -1,59 +0,0 @@ -from django.contrib.sitemaps import Sitemap -from django.urls import reverse - -from blog.models import Article, Category, Tag - - -class StaticViewSitemap(Sitemap): - priority = 0.5 - changefreq = 'daily' - - def items(self): - return ['blog:index', ] - - def location(self, item): - return reverse(item) - - -class ArticleSiteMap(Sitemap): - changefreq = "monthly" - priority = "0.6" - - def items(self): - return Article.objects.filter(status='p') - - def lastmod(self, obj): - return obj.last_modify_time - - -class CategorySiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.6" - - def items(self): - return Category.objects.all() - - def lastmod(self, obj): - return obj.last_modify_time - - -class TagSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" - - def items(self): - return Tag.objects.all() - - def lastmod(self, obj): - return obj.last_modify_time - - -class UserSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" - - def items(self): - return list(set(map(lambda x: x.author, Article.objects.all()))) - - def lastmod(self, obj): - return obj.date_joined diff --git a/src/DjangoBlog-master/djangoblog/spider_notify.py b/src/DjangoBlog-master/djangoblog/spider_notify.py deleted file mode 100644 index 7b909e9..0000000 --- a/src/DjangoBlog-master/djangoblog/spider_notify.py +++ /dev/null @@ -1,21 +0,0 @@ -import logging - -import requests -from django.conf import settings - -logger = logging.getLogger(__name__) - - -class SpiderNotify(): - @staticmethod - def baidu_notify(urls): - try: - data = '\n'.join(urls) - result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) - logger.info(result.text) - except Exception as e: - logger.error(e) - - @staticmethod - def notify(url): - SpiderNotify.baidu_notify(url) diff --git a/src/DjangoBlog-master/djangoblog/tests.py b/src/DjangoBlog-master/djangoblog/tests.py deleted file mode 100644 index 01237d9..0000000 --- a/src/DjangoBlog-master/djangoblog/tests.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.test import TestCase - -from djangoblog.utils import * - - -class DjangoBlogTest(TestCase): - def setUp(self): - pass - - def test_utils(self): - md5 = get_sha256('test') - self.assertIsNotNone(md5) - c = CommonMarkdown.get_markdown(''' - # Title1 - - ```python - import os - ``` - - [url](https://www.lylinux.net/) - - [ddd](http://www.baidu.com) - - - ''') - self.assertIsNotNone(c) - d = { - 'd': 'key1', - 'd2': 'key2' - } - data = parse_dict_to_url(d) - self.assertIsNotNone(data) diff --git a/src/DjangoBlog-master/djangoblog/urls.py b/src/DjangoBlog-master/djangoblog/urls.py deleted file mode 100644 index 4aae58a..0000000 --- a/src/DjangoBlog-master/djangoblog/urls.py +++ /dev/null @@ -1,64 +0,0 @@ -"""djangoblog URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf import settings -from django.conf.urls.i18n import i18n_patterns -from django.conf.urls.static import static -from django.contrib.sitemaps.views import sitemap -from django.urls import path, include -from django.urls import re_path -from haystack.views import search_view_factory - -from blog.views import EsSearchView -from djangoblog.admin_site import admin_site -from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm -from djangoblog.feeds import DjangoBlogFeed -from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap - -sitemaps = { - - 'blog': ArticleSiteMap, - 'Category': CategorySiteMap, - 'Tag': TagSiteMap, - 'User': UserSiteMap, - 'static': StaticViewSitemap -} - -handler404 = 'blog.views.page_not_found_view' -handler500 = 'blog.views.server_error_view' -handle403 = 'blog.views.permission_denied_view' - -urlpatterns = [ - path('i18n/', include('django.conf.urls.i18n')), -] -urlpatterns += i18n_patterns( - re_path(r'^admin/', admin_site.urls), - re_path(r'', include('blog.urls', namespace='blog')), - re_path(r'mdeditor/', include('mdeditor.urls')), - re_path(r'', include('comments.urls', namespace='comment')), - re_path(r'', include('accounts.urls', namespace='account')), - re_path(r'', include('oauth.urls', namespace='oauth')), - re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, - name='django.contrib.sitemaps.views.sitemap'), - re_path(r'^feed/$', DjangoBlogFeed()), - re_path(r'^rss/$', DjangoBlogFeed()), - re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), - name='search'), - re_path(r'', include('servermanager.urls', namespace='servermanager')), - re_path(r'', include('owntracks.urls', namespace='owntracks')) - , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) diff --git a/src/DjangoBlog-master/djangoblog/utils.py b/src/DjangoBlog-master/djangoblog/utils.py deleted file mode 100644 index 57f63dc..0000000 --- a/src/DjangoBlog-master/djangoblog/utils.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - - -import logging -import os -import random -import string -import uuid -from hashlib import sha256 - -import bleach -import markdown -import requests -from django.conf import settings -from django.contrib.sites.models import Site -from django.core.cache import cache -from django.templatetags.static import static - -logger = logging.getLogger(__name__) - - -def get_max_articleid_commentid(): - from blog.models import Article - from comments.models import Comment - return (Article.objects.latest().pk, Comment.objects.latest().pk) - - -def get_sha256(str): - m = sha256(str.encode('utf-8')) - return m.hexdigest() - - -def cache_decorator(expiration=3 * 60): - def wrapper(func): - def news(*args, **kwargs): - try: - view = args[0] - key = view.get_cache_key() - except: - key = None - if not key: - unique_str = repr((func, args, kwargs)) - - m = sha256(unique_str.encode('utf-8')) - key = m.hexdigest() - value = cache.get(key) - if value is not None: - # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) - if str(value) == '__default_cache_value__': - return None - else: - return value - else: - logger.debug( - 'cache_decorator set cache:%s key:%s' % - (func.__name__, key)) - value = func(*args, **kwargs) - if value is None: - cache.set(key, '__default_cache_value__', expiration) - else: - cache.set(key, value, expiration) - return value - - return news - - return wrapper - - -def expire_view_cache(path, servername, serverport, key_prefix=None): - ''' - 刷新视图缓存 - :param path:url路径 - :param servername:host - :param serverport:端口 - :param key_prefix:前缀 - :return:是否成功 - ''' - from django.http import HttpRequest - from django.utils.cache import get_cache_key - - request = HttpRequest() - request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} - request.path = path - - key = get_cache_key(request, key_prefix=key_prefix, cache=cache) - if key: - logger.info('expire_view_cache:get key:{path}'.format(path=path)) - if cache.get(key): - cache.delete(key) - return True - return False - - -@cache_decorator() -def get_current_site(): - site = Site.objects.get_current() - return site - - -class CommonMarkdown: - @staticmethod - def _convert_markdown(value): - md = markdown.Markdown( - extensions=[ - 'extra', - 'codehilite', - 'toc', - 'tables', - ] - ) - body = md.convert(value) - toc = md.toc - return body, toc - - @staticmethod - def get_markdown_with_toc(value): - body, toc = CommonMarkdown._convert_markdown(value) - return body, toc - - @staticmethod - def get_markdown(value): - body, toc = CommonMarkdown._convert_markdown(value) - return body - - -def send_email(emailto, title, content): - from djangoblog.blog_signals import send_email_signal - send_email_signal.send( - send_email.__class__, - emailto=emailto, - title=title, - content=content) - - -def generate_code() -> str: - """生成随机数验证码""" - return ''.join(random.sample(string.digits, 6)) - - -def parse_dict_to_url(dict): - from urllib.parse import quote - url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) - for k, v in dict.items()]) - return url - - -def get_blog_setting(): - value = cache.get('get_blog_setting') - if value: - return value - else: - from blog.models import BlogSettings - if not BlogSettings.objects.count(): - setting = BlogSettings() - setting.site_name = 'djangoblog' - setting.site_description = '基于Django的博客系统' - setting.site_seo_description = '基于Django的博客系统' - setting.site_keywords = 'Django,Python' - setting.article_sub_length = 300 - setting.sidebar_article_count = 10 - setting.sidebar_comment_count = 5 - setting.show_google_adsense = False - setting.open_site_comment = True - setting.analytics_code = '' - setting.beian_code = '' - setting.show_gongan_code = False - setting.comment_need_review = False - setting.save() - value = BlogSettings.objects.first() - logger.info('set cache get_blog_setting') - cache.set('get_blog_setting', value) - return value - - -def save_user_avatar(url): - ''' - 保存用户头像 - :param url:头像url - :return: 本地路径 - ''' - logger.info(url) - - try: - basedir = os.path.join(settings.STATICFILES, 'avatar') - rsp = requests.get(url, timeout=2) - if rsp.status_code == 200: - if not os.path.exists(basedir): - os.makedirs(basedir) - - image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] - isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 - ext = os.path.splitext(url)[1] if isimage else '.jpg' - save_filename = str(uuid.uuid4().hex) + ext - logger.info('保存用户头像:' + basedir + save_filename) - with open(os.path.join(basedir, save_filename), 'wb+') as file: - file.write(rsp.content) - return static('avatar/' + save_filename) - except Exception as e: - logger.error(e) - return static('blog/img/avatar.png') - - -def delete_sidebar_cache(): - from blog.models import LinkShowType - keys = ["sidebar" + x for x in LinkShowType.values] - for k in keys: - logger.info('delete sidebar key:' + k) - cache.delete(k) - - -def delete_view_cache(prefix, keys): - from django.core.cache.utils import make_template_fragment_key - key = make_template_fragment_key(prefix, keys) - cache.delete(key) - - -def get_resource_url(): - if settings.STATIC_URL: - return settings.STATIC_URL - else: - site = get_current_site() - return 'http://' + site.domain + '/static/' - - -ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', - 'h2', 'p'] -ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} - - -def sanitize_html(html): - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) diff --git a/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py b/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py deleted file mode 100644 index 04e3f7f..0000000 --- a/src/DjangoBlog-master/djangoblog/whoosh_cn_backend.py +++ /dev/null @@ -1,1044 +0,0 @@ -# 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/src/DjangoBlog-master/djangoblog/wsgi.py b/src/DjangoBlog-master/djangoblog/wsgi.py deleted file mode 100644 index 2295efd..0000000 --- a/src/DjangoBlog-master/djangoblog/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for djangoblog project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") - -application = get_wsgi_application() diff --git a/src/DjangoBlog-master/docs/README-en.md b/src/DjangoBlog-master/docs/README-en.md deleted file mode 100644 index 37ea069..0000000 --- a/src/DjangoBlog-master/docs/README-en.md +++ /dev/null @@ -1,158 +0,0 @@ -# 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/src/DjangoBlog-master/docs/config-en.md b/src/DjangoBlog-master/docs/config-en.md deleted file mode 100644 index b877efb..0000000 --- a/src/DjangoBlog-master/docs/config-en.md +++ /dev/null @@ -1,64 +0,0 @@ -# Introduction to main features settings - -## Cache: -Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`. -```python -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', - 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', - 'TIMEOUT': 60 * 60 * 10 - }, - 'locmemcache': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 10800, - 'LOCATION': 'unique-snowflake', - } -} -``` - -## OAuth Login: -QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration. - -### Callback address examples: -QQ: http://your-domain-name/oauth/authorize?type=qq -Weibo: http://your-domain-name/oauth/authorize?type=weibo -type is in the type field of `oauthmanager`. - -## owntracks: -owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap. - -## Email feature: -Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify: -```python -EMAIL_HOST = 'smtp.zoho.com' -EMAIL_PORT = 587 -EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') -EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') -``` -with your email account information. - -## WeChat Official Account -Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account. - -## Introduction to website configuration -You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc. -OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default. - -## Source code highlighting -If the code block in your article didn't show hightlight, please write the code blocks as following: - -![](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/src/DjangoBlog-master/docs/config.md b/src/DjangoBlog-master/docs/config.md deleted file mode 100644 index 24673a3..0000000 --- a/src/DjangoBlog-master/docs/config.md +++ /dev/null @@ -1,58 +0,0 @@ -# 主要功能配置介绍: - -## 缓存: -缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。 -https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199 - - -## oauth登录: - -现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 -**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 -### 回调地址示例: -qq:http://你的域名/oauth/authorize?type=qq -微博:http://你的域名/oauth/authorize?type=weibo -type对应在`oauthmanager`中的type字段。 - -## owntracks: -owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: -`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。 - -## 邮件功能: -同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改: -```python -EMAIL_HOST = 'smtp.zoho.com' -EMAIL_PORT = 587 -EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') -EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') -``` -为你自己的邮箱配置。 - -## 微信公众号 -集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。 -然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。 -## 网站配置介绍 -在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 -其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 -## 代码高亮 -如果你发现你文章的代码没有高亮,请这样书写代码块: - -![](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/src/DjangoBlog-master/docs/docker-en.md b/src/DjangoBlog-master/docs/docker-en.md deleted file mode 100644 index 8d5d59e..0000000 --- a/src/DjangoBlog-master/docs/docker-en.md +++ /dev/null @@ -1,114 +0,0 @@ -# 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/src/DjangoBlog-master/docs/docker.md b/src/DjangoBlog-master/docs/docker.md deleted file mode 100644 index e7c255a..0000000 --- a/src/DjangoBlog-master/docs/docker.md +++ /dev/null @@ -1,114 +0,0 @@ -# 使用 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/src/DjangoBlog-master/docs/es.md b/src/DjangoBlog-master/docs/es.md deleted file mode 100644 index 97226c5..0000000 --- a/src/DjangoBlog-master/docs/es.md +++ /dev/null @@ -1,28 +0,0 @@ -# 集成Elasticsearch -如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单, -首先需要注意如下几点: -1. 你的`Elasticsearch`支持`ik`中文分词 -2. 你的`Elasticsearch`版本>=7.3.0 - -接下来在`settings.py`做如下改动即可: -- 增加es链接,如下所示: -```python -ELASTICSEARCH_DSL = { - 'default': { - 'hosts': '127.0.0.1:9200' - }, -} -``` -- 修改`HAYSTACK`配置: -```python -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', - }, -} -``` -然后终端执行: -```shell script -./manage.py build_index -``` -这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。 \ No newline at end of file diff --git a/src/DjangoBlog-master/docs/imgs/alipay.jpg b/src/DjangoBlog-master/docs/imgs/alipay.jpg deleted file mode 100644 index 424d70a2ffbb629b481e0c27d72d6076727e8041..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17961 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⁡O0bI!3Bi@jSTgKn>Bg&Q53II7XEA`NhoOY$25L(eK?<1>~>hujoRs}yUHVX zQTt}~MN1Zw0!xe7-aUbnm3fxd*KjwLLobx-SSZdGT`vUtB-=hS^~#Rs9K<#Wao{Zp z{kmB)_tVwwglj-W2qla4$Dx zz53|611jp`Ro8}#TnY+1OdA}SN;lGY+!=)=QY;q0b}yA~i0O^|s1#r34{5DET&Qh` z+yAF+h#H($?p6{{X zz@F@jlhUjuyW%|!{Q!~$q>$)y1Hsl$FMYS;#r=c7U7SN-8Uv-lv<@mZOXKOlXLy4C z3v?;Yh1KbtsO!ynC%cyr-`4MR8{Y~e28GXK7yRZ9{3d5(xoP=XYdX*Uy?#pU$a5>& z`JUOV_ zRl}`OH8`y;U%XD+MFBD!HxL@B`JSH_G!;0V9-A+_4Hhf%=wJ%txE{qNaHkfkLsS+h zfe@JrYreKrFKhU2`1kD)K_#LCd!v$sI$is2ee^7Am=@kq2Z-`cU(l{<8*ON_J?@@& zOR+4E3%sN-vIK-%81R}RdPK$bCrBoGF zRu$>N^0s*R^!6R919|j@|cY|A}(o`$`Onha=;(7(EQNg;0 z^sK+j)DQ>i-wCKpkXH^lH{QNw|FWJ%bUle^xtPfLdv#(gNpA7lv59GyIOXp*E+vz_!y|AkM=lB7mYXSWGfxr6#WlV5UW{v7tVn1!e zwg=v}kG!o+0qXfY-FyUozk}%&PS$ZRL^NoDuz%nVw(p)FVMPG6Ph*mgs4z0~C)P`9 zWsg_X!-)T2FkH=VqaRBu8Zkq=AX6oLSWpg(yQ^NIftdmxjx diff --git a/src/DjangoBlog-master/docs/imgs/pycharm_logo.png b/src/DjangoBlog-master/docs/imgs/pycharm_logo.png deleted file mode 100644 index 7f2a4b0ea66469bd218774de8cb3027a9c18b84d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132045 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 z 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

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


AewDOjh!MMu(vjQJBsiX3ZCmAui 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 diff --git a/src/DjangoBlog-master/docs/k8s-en.md b/src/DjangoBlog-master/docs/k8s-en.md deleted file mode 100644 index 20e9527..0000000 --- a/src/DjangoBlog-master/docs/k8s-en.md +++ /dev/null @@ -1,141 +0,0 @@ -# Deploying DjangoBlog with Kubernetes - -This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch. - -## Architecture Overview - -This deployment utilizes a microservices-based, cloud-native architecture: - -- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`. -- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.** -- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names. -- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application. -- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC). - -## 1. Prerequisites - -Before you begin, please ensure you have the following: - -- A running Kubernetes cluster. -- The `kubectl` command-line tool configured to connect to your cluster. -- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster. -- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories. - -## 2. Deployment Steps - -### Step 1: Create a Namespace - -We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management. - -```bash -# Create a namespace named 'djangoblog' -kubectl create namespace djangoblog -``` - -### Step 2: Configure Persistent Storage - -This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`). - -```bash -# Log in to your master node -ssh user@master-node - -# Create the required storage directories -sudo mkdir -p /mnt/local-storage-db -sudo mkdir -p /mnt/local-storage-djangoblog -sudo mkdir -p /mnt/resource/ -sudo mkdir -p /mnt/local-storage-elasticsearch - -# Log out from the node -exit -``` -**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file. - -After creating the directories, apply the storage-related configurations: - -```bash -# Apply the StorageClass -kubectl apply -f deploy/k8s/storageclass.yaml - -# Apply the PersistentVolumes (PVs) -kubectl apply -f deploy/k8s/pv.yaml - -# Apply the PersistentVolumeClaims (PVCs) -kubectl apply -f deploy/k8s/pvc.yaml -``` - -### Step 3: Configure the Application - -Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings. - -**It is strongly recommended to change the following fields:** -- `DJANGO_SECRET_KEY`: Change to a random, complex string. -- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password. - -```bash -# Edit the ConfigMap file -vim deploy/k8s/configmap.yaml - -# Apply the configuration -kubectl apply -f deploy/k8s/configmap.yaml -``` - -### Step 4: Deploy the Application Stack - -Now, we can deploy all the core services. - -```bash -# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) -kubectl apply -f deploy/k8s/deployment.yaml - -# Deploy the Services (to create internal endpoints for the Deployments) -kubectl apply -f deploy/k8s/service.yaml -``` - -The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`): - -```bash -kubectl get pods -n djangoblog -w -``` - -### Step 5: Expose the Application Externally - -Finally, expose the Nginx service to external traffic by applying the `Ingress` rule. - -```bash -# Apply the Ingress rule -kubectl apply -f deploy/k8s/gateway.yaml -``` - -Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address: - -```bash -kubectl get ingress -n djangoblog -``` - -### Step 6: First-Time Initialization - -Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run. - -```bash -# First, get the name of a djangoblog pod -kubectl get pods -n djangoblog | grep djangoblog - -# Exec into one of the Pods (replace [pod-name] with the name from the previous step) -kubectl exec -it [pod-name] -n djangoblog -- bash - -# Inside the Pod, run the following commands: -# Create a superuser account (follow the prompts) -python manage.py createsuperuser - -# (Optional) Create some test data -python manage.py create_testdata - -# (Optional, if ES is enabled) Create the search index -python manage.py rebuild_index - -# Exit the Pod -exit -``` - -Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster. \ No newline at end of file diff --git a/src/DjangoBlog-master/docs/k8s.md b/src/DjangoBlog-master/docs/k8s.md deleted file mode 100644 index 9da3c28..0000000 --- a/src/DjangoBlog-master/docs/k8s.md +++ /dev/null @@ -1,141 +0,0 @@ -# 使用 Kubernetes 部署 DjangoBlog - -本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。 - -## 架构概览 - -本次部署采用的是微服务化的云原生架构: - -- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。 -- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。** -- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。 -- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。 -- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。 - -## 1. 环境准备 - -在开始之前,请确保您已具备以下环境: - -- 一个正在运行的 Kubernetes 集群。 -- `kubectl` 命令行工具已配置并能够连接到您的集群。 -- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。 -- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。 - -## 2. 部署步骤 - -### 步骤 1: 创建命名空间 - -我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。 - -```bash -# 创建一个名为 djangoblog 的命名空间 -kubectl create namespace djangoblog -``` - -### 步骤 2: 配置持久化存储 - -此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。 - -```bash -# 登录到您的 master 节点 -ssh user@master-node - -# 创建所需的存储目录 -sudo mkdir -p /mnt/local-storage-db -sudo mkdir -p /mnt/local-storage-djangoblog -sudo mkdir -p /mnt/resource/ -sudo mkdir -p /mnt/local-storage-elasticsearch - -# 退出节点 -exit -``` -**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。 - -创建目录后,应用存储相关的配置文件: - -```bash -# 应用 StorageClass -kubectl apply -f deploy/k8s/storageclass.yaml - -# 应用 PersistentVolume (PV) -kubectl apply -f deploy/k8s/pv.yaml - -# 应用 PersistentVolumeClaim (PVC) -kubectl apply -f deploy/k8s/pvc.yaml -``` - -### 步骤 3: 配置应用 - -在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。 - -**强烈建议修改以下字段:** -- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。 -- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。 - -```bash -# 编辑 ConfigMap 文件 -vim deploy/k8s/configmap.yaml - -# 应用配置 -kubectl apply -f deploy/k8s/configmap.yaml -``` - -### 步骤 4: 部署应用服务栈 - -现在,我们可以部署所有的核心服务了。 - -```bash -# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) -kubectl apply -f deploy/k8s/deployment.yaml - -# 部署 Services (为 Deployments 创建内部访问端点) -kubectl apply -f deploy/k8s/service.yaml -``` - -部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`): - -```bash -kubectl get pods -n djangoblog -w -``` - -### 步骤 5: 暴露应用到外部 - -最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。 - -```bash -# 应用 Ingress 规则 -kubectl apply -f deploy/k8s/gateway.yaml -``` - -部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址: - -```bash -kubectl get ingress -n djangoblog -``` - -### 步骤 6: 首次运行的初始化操作 - -与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。 - -```bash -# 首先,获取 djangoblog pod 的名称 -kubectl get pods -n djangoblog | grep djangoblog - -# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称) -kubectl exec -it [pod-name] -n djangoblog -- bash - -# 在 Pod 内部执行以下命令: -# 创建超级管理员账户 (请按照提示操作) -python manage.py createsuperuser - -# (可选) 创建测试数据 -python manage.py create_testdata - -# (可选,如果启用了 ES) 创建索引 -python manage.py rebuild_index - -# 退出 Pod -exit -``` - -至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署! \ No newline at end of file diff --git a/src/DjangoBlog-master/foodBlog/food_categories.json b/src/DjangoBlog-master/foodBlog/food_categories.json deleted file mode 100644 index 03467de..0000000 --- a/src/DjangoBlog-master/foodBlog/food_categories.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "model": "blog.category", - "pk": 1, - "fields": { - "name": "中式菜肴" - } - }, - { - "model": "blog.category", - "pk": 2, - "fields": { - "name": "西式美食" - } - }, - { - "model": "blog.category", - "pk": 3, - "fields": { - "name": "甜点烘焙" - } - }, - { - "model": "blog.category", - "pk": 4, - "fields": { - "name": "特色小吃" - } - } -] \ No newline at end of file diff --git a/src/DjangoBlog-master/foodBlog/food_posts.json b/src/DjangoBlog-master/foodBlog/food_posts.json deleted file mode 100644 index 62f692e..0000000 --- a/src/DjangoBlog-master/foodBlog/food_posts.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "$name$", - "version": "$version$", - "description": "$END$", - "main": "$main$", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "https://bdgit.educoder.net/pnry42fjm/DjangoBlog-Maintenance-Analysis.git" - }, - "private": true -} diff --git a/src/DjangoBlog-master/foodBlog/food_tags.json b/src/DjangoBlog-master/foodBlog/food_tags.json deleted file mode 100644 index b650876..0000000 --- a/src/DjangoBlog-master/foodBlog/food_tags.json +++ /dev/null @@ -1,30 +0,0 @@ -[ -  { -    "model": "blog.tag", -    "pk": 1, -    "fields": { -      "name": "探店" -    } -  }, -  { -    "model": "blog.tag", -    "pk": 2, -    "fields": { -      "name": "家常菜" -    } -  }, -  { -    "model": "blog.tag", -    "pk": 3, -    "fields": { -      "name": "烘焙" -    } -  }, -  { -    "model": "blog.tag", -    "pk": 4, -    "fields": { -      "name": "小吃" -    } -  } -] \ No newline at end of file diff --git a/src/DjangoBlog-master/foodBlog/food_users.json b/src/DjangoBlog-master/foodBlog/food_users.json deleted file mode 100644 index 595f31e..0000000 --- a/src/DjangoBlog-master/foodBlog/food_users.json +++ /dev/null @@ -1,13 +0,0 @@ -[ -  { -    "model": "auth.user", -    "pk": 1, -    "fields": { -      "username": "LY", -      "password": "LY181828", -      "email": "yantong268@qq.com", -      "is_staff": false, -      "is_superuser": false -    } -  } -] \ No newline at end of file diff --git a/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo deleted file mode 100644 index f63669f46b3283a84e04098a7338b55f204e7b9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11097 zcmeI1ZHygN8OIOuzGwvm6;V80DQ&fPD^j&CyM<-Dg@xUAX}2J#)VcSby?5x`naj-F zZm$#+#VU{(-w;LPTQou>QKLp83PwZtpfSFp(fGm_@tqi=i7`=s|Cw`lZ+91?#(pr_ z$v*ctbLPx*p7We@p6AT$&rd(@PQ!5+c|J0AoH3t=hfd;$1yOBsD0^;$^2&CzXM9|d!X$9guj2Uzy3VbbN55( ze-O$q4@0J|`Juo67*szVhtmHSD1U13_1r1&Y4CK&)H7#7>0J+{uLAY_D3t%EAVXsI zLg|U1?3;(ue+bI2hdti|WzR>U^xfzA6{z;V1y%q1Q2KufrRO(Les~hfUw`vF?RlkN z)czuz;38uu<#J2yeueHg0#9sd5ieEFT8cl-Mv@%*GO|1`XW zdHY$Yc=6=fWxUwU;#a)*GE}^{@r7kSPFP#^e~srkuvl+U@nQ_hj(t%6bx`&lf|>`n zK*fu9!^7}F_z0XBEXU^=EJoFPK9rscWNMlV{Ph)3{=X8+uK|?5LMVUS3^fjKgKGD^ zP=5b_zy1tl2+S9t{QYex`+p2);qTx$y!hNQUfc>*?*S+~9)?sgKZ9!Ln04iPSObsa zdJwAKdZ_ysLA5^xx4nHMEDJ;et#26-%p|J z{*&i%8_Id|94LRB17*(z@FchuYJH4B*|pu5UkNq;u7xMUCRF=vfB%iX{OwTn-s$;a zsByXvs-3Swwfl9?A3@FA-$TWVC!y>|OM?3skpx7X7_wcmlNe+WwdZBTmN4dsUqLHX+w zo?nLY%Xgs0;W4QCPeA$ouTb`{VR1?S8J=guW4RuLYUeyCzix)Azs27l_T}3=$Nl}w zJa_u?S;(|4<~T5KMs7qdLEeJA4(T0l;$#RJKyE&pF^CMC)P-xfnT& zM94Pe6ol>-hkT-AGqMhmZ{?Tkk*ks2h>k`H^G44}IP1@aoBVkIHzM+h;)0H?CCr6z z0vSVUzU&&vR5ne2y%+98G!`0{OOY7STp^Se$5s3u^5=xvV&3Vp>&pa<;dX!Bf|ny( zkSmaV$SaWEahX3EgxdRAo;G@%w6iZ$Vyvg}fA*Ms!?& zgvA+qQ#gVgK$KgqMcPP)9FOR@6yhYmv)ryHkzAtm*2u+O_L8`0}BklQ)xesDnLm&~o^3SsNNZ z-wuN}p2Uqb$U9Mxhe@1mDty=sa6{Iq)m)a%b)u+it4ZgGQM5t5o^p4{UW3o^n59YI z)Vd9c%#aQH@@^X=G!5!u>S{TvqNHKNSVe^^=Up`mb7$v6Ebrttuo$~j60Qok-Vr`z03GFSfg z7Cw`{99J`*)VY_$b79(ASTZW;U(Oo+!r_aHxroP@v36*!K%ShJI^_?t70g)hg zZ5zx7-688^{o)!fv=@QBSdvZWbHHLz(jt7az?B)i_Mycg9$Ib&D(!z6fry0vqu5g{ ziT@zr(2vvwbaSj$BUa{Sdzb~)h-}guuSj;Kn1yp8 z297tw*f9(DBx%|$r$`$$MVd(64IRhisB5+;WzH!}ET+qa4M9^8yLgN^kU0`7lOesR zUn$9gC?-YKPDVq$l>6`M(4?s+g%!ozX}h!)W*P3$ybOv|Rt7@Zagmt~8kY6dshc9& zWXmEa<&J!#)Rx4`fLP61AnDA7jSeYOo}Wzv<~xavbft@WsNx`6aNAz`Wv+55&a4%W zprG!oOVh-ccWpWJSNlA8WCK?(rn@W;QcHLz#FyKwj6GFN7NIcFF2?6RbB)iHBxskD zA{)ip>&=jI5OMYro5JjguiU+EgVb$gA8;Uu!g`UFN!Bm7Ei!HHDE9-i-w_kY744Kw zQW|yDXxf`<3Ojv?wwEOSgCs(9-@J_|klB?o1P*sZSe6gIZ%4$Xvc9Y@!sEVbQtxhD z%=-P|yR2$gRBRh;D~cFK*Vv7$u#c3gLd8mkS$mXo&*E0EOg<|bVS;+*Vr|KlBiJ?~ z1gCn}x`SbsXC`1Tmj{goOWlu6j|7b}dHM2%5iNsSuQU!0gpG0u^op`h)kZFE*pVmII4!&q-*g#(MMs6BdWlIr#39z*of>rJ7__WVm2qeh#& zM;i-c(Yr3!7Prg&`NB!oi@dEMuQg5E@9x>bv=jQEALWSH_?MQlyNhoym~ul$rk1_c z>ZbXHaq`y-i8Ny*X+}=uHKds7{S zG;9|EUR!Rv-*xpqlJO8%g+Gh4%<)>&UzvzWyXD+uJ+zo#EHg>@6epP}w#r7f>gC83 zW1^jLX69X3O-z^WnVB@vC{-rw6|#rY*sz@$8^2=g(g}Oj#O^(lyLJxjcI_n1E7Msc ztXD4SG_uNUGHeHCc2#z>)z&kW?JTEZd(Fhy?#lG8%JwmP*{9NVFi^sNa-#xKs&o-*YjSf?y)dFftR zGBP!FaTE@*A{Ogl!Sv+x#G={f5507NZK>90WtJWf+uR+@H@73+(=R021}Rx;bnonr z%GSktGHcGIm5G=bMg|+UTdQF{FcXn^qsopXZDqqYZWou?=*uo7E$PnahS;thwdZfV zaA4W1^y-%iyZWVK^-IOl7w4m}VOjlB(ZAnZ>hS)1N5T31>Q}#1^nGpQ4bgvsUj0(B w`lVv^OU3G!3cY2leyPwu>;C8e_#z7}Nk0GgUn*8VRIGleSp873^h3oz0qFQpxc~qF diff --git a/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po b/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po deleted file mode 100644 index c80b30a..0000000 --- a/src/DjangoBlog-master/locale/en/LC_MESSAGES/django.po +++ /dev/null @@ -1,685 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-13 16:02+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: .\accounts\admin.py:12 -msgid "password" -msgstr "password" - -#: .\accounts\admin.py:13 -msgid "Enter password again" -msgstr "Enter password again" - -#: .\accounts\admin.py:24 .\accounts\forms.py:89 -msgid "passwords do not match" -msgstr "passwords do not match" - -#: .\accounts\forms.py:36 -msgid "email already exists" -msgstr "email already exists" - -#: .\accounts\forms.py:46 .\accounts\forms.py:50 -msgid "New password" -msgstr "New password" - -#: .\accounts\forms.py:60 -msgid "Confirm password" -msgstr "Confirm password" - -#: .\accounts\forms.py:70 .\accounts\forms.py:116 -msgid "Email" -msgstr "Email" - -#: .\accounts\forms.py:76 .\accounts\forms.py:80 -msgid "Code" -msgstr "Code" - -#: .\accounts\forms.py:100 .\accounts\tests.py:194 -msgid "email does not exist" -msgstr "email does not exist" - -#: .\accounts\models.py:12 .\oauth\models.py:17 -msgid "nick name" -msgstr "nick name" - -#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 -#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 -#: .\oauth\models.py:53 -msgid "creation time" -msgstr "creation time" - -#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 -#: .\oauth\models.py:54 -msgid "last modify time" -msgstr "last modify time" - -#: .\accounts\models.py:15 -msgid "create source" -msgstr "create source" - -#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 -msgid "user" -msgstr "user" - -#: .\accounts\tests.py:216 .\accounts\utils.py:39 -msgid "Verification code error" -msgstr "Verification code error" - -#: .\accounts\utils.py:13 -msgid "Verify Email" -msgstr "Verify Email" - -#: .\accounts\utils.py:21 -#, python-format -msgid "" -"You are resetting the password, the verification code is:%(code)s, valid " -"within 5 minutes, please keep it properly" -msgstr "" -"You are resetting the password, the verification code is:%(code)s, valid " -"within 5 minutes, please keep it properly" - -#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 -#: .\oauth\models.py:12 -msgid "author" -msgstr "author" - -#: .\blog\admin.py:53 -msgid "Publish selected articles" -msgstr "Publish selected articles" - -#: .\blog\admin.py:54 -msgid "Draft selected articles" -msgstr "Draft selected articles" - -#: .\blog\admin.py:55 -msgid "Close article comments" -msgstr "Close article comments" - -#: .\blog\admin.py:56 -msgid "Open article comments" -msgstr "Open article comments" - -#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 -#: .\templates\blog\tags\sidebar.html:40 -msgid "category" -msgstr "category" - -#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 -msgid "index" -msgstr "index" - -#: .\blog\models.py:21 -msgid "list" -msgstr "list" - -#: .\blog\models.py:22 -msgid "post" -msgstr "post" - -#: .\blog\models.py:23 -msgid "all" -msgstr "all" - -#: .\blog\models.py:24 -msgid "slide" -msgstr "slide" - -#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 -msgid "modify time" -msgstr "modify time" - -#: .\blog\models.py:63 -msgid "Draft" -msgstr "Draft" - -#: .\blog\models.py:64 -msgid "Published" -msgstr "Published" - -#: .\blog\models.py:67 -msgid "Open" -msgstr "Open" - -#: .\blog\models.py:68 -msgid "Close" -msgstr "Close" - -#: .\blog\models.py:71 .\comments\admin.py:47 -msgid "Article" -msgstr "Article" - -#: .\blog\models.py:72 -msgid "Page" -msgstr "Page" - -#: .\blog\models.py:74 .\blog\models.py:280 -msgid "title" -msgstr "title" - -#: .\blog\models.py:75 -msgid "body" -msgstr "body" - -#: .\blog\models.py:77 -msgid "publish time" -msgstr "publish time" - -#: .\blog\models.py:79 -msgid "status" -msgstr "status" - -#: .\blog\models.py:84 -msgid "comment status" -msgstr "comment status" - -#: .\blog\models.py:88 .\oauth\models.py:43 -msgid "type" -msgstr "type" - -#: .\blog\models.py:89 -msgid "views" -msgstr "views" - -#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 -msgid "order" -msgstr "order" - -#: .\blog\models.py:98 -msgid "show toc" -msgstr "show toc" - -#: .\blog\models.py:105 .\blog\models.py:249 -msgid "tag" -msgstr "tag" - -#: .\blog\models.py:115 .\comments\models.py:21 -msgid "article" -msgstr "article" - -#: .\blog\models.py:171 -msgid "category name" -msgstr "category name" - -#: .\blog\models.py:174 -msgid "parent category" -msgstr "parent category" - -#: .\blog\models.py:234 -msgid "tag name" -msgstr "tag name" - -#: .\blog\models.py:256 -msgid "link name" -msgstr "link name" - -#: .\blog\models.py:257 .\blog\models.py:271 -msgid "link" -msgstr "link" - -#: .\blog\models.py:260 -msgid "is show" -msgstr "is show" - -#: .\blog\models.py:262 -msgid "show type" -msgstr "show type" - -#: .\blog\models.py:281 -msgid "content" -msgstr "content" - -#: .\blog\models.py:283 .\oauth\models.py:52 -msgid "is enable" -msgstr "is enable" - -#: .\blog\models.py:289 -msgid "sidebar" -msgstr "sidebar" - -#: .\blog\models.py:299 -msgid "site name" -msgstr "site name" - -#: .\blog\models.py:305 -msgid "site description" -msgstr "site description" - -#: .\blog\models.py:311 -msgid "site seo description" -msgstr "site seo description" - -#: .\blog\models.py:313 -msgid "site keywords" -msgstr "site keywords" - -#: .\blog\models.py:318 -msgid "article sub length" -msgstr "article sub length" - -#: .\blog\models.py:319 -msgid "sidebar article count" -msgstr "sidebar article count" - -#: .\blog\models.py:320 -msgid "sidebar comment count" -msgstr "sidebar comment count" - -#: .\blog\models.py:321 -msgid "article comment count" -msgstr "article comment count" - -#: .\blog\models.py:322 -msgid "show adsense" -msgstr "show adsense" - -#: .\blog\models.py:324 -msgid "adsense code" -msgstr "adsense code" - -#: .\blog\models.py:325 -msgid "open site comment" -msgstr "open site comment" - -#: .\blog\models.py:352 -msgid "Website configuration" -msgstr "Website configuration" - -#: .\blog\models.py:360 -msgid "There can only be one configuration" -msgstr "There can only be one configuration" - -#: .\blog\views.py:348 -msgid "" -"Sorry, the page you requested is not found, please click the home page to " -"see other?" -msgstr "" -"Sorry, the page you requested is not found, please click the home page to " -"see other?" - -#: .\blog\views.py:356 -msgid "Sorry, the server is busy, please click the home page to see other?" -msgstr "Sorry, the server is busy, please click the home page to see other?" - -#: .\blog\views.py:369 -msgid "Sorry, you do not have permission to access this page?" -msgstr "Sorry, you do not have permission to access this page?" - -#: .\comments\admin.py:15 -msgid "Disable comments" -msgstr "Disable comments" - -#: .\comments\admin.py:16 -msgid "Enable comments" -msgstr "Enable comments" - -#: .\comments\admin.py:46 -msgid "User" -msgstr "User" - -#: .\comments\models.py:25 -msgid "parent comment" -msgstr "parent comment" - -#: .\comments\models.py:29 -msgid "enable" -msgstr "enable" - -#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 -msgid "comment" -msgstr "comment" - -#: .\comments\utils.py:13 -msgid "Thanks for your comment" -msgstr "Thanks for your comment" - -#: .\comments\utils.py:15 -#, python-format -msgid "" -"

Thank you very much for your comments on this site

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

Thank you very much for your comments on this site

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

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

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

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

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

Please click the link below to bind your email

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

Please click the link below to bind your email

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

Thank you very much for your comments on this site

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

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

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

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

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

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

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

Please click the link below to bind your email

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

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

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

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

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

Please click the link below to bind your email

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

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

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

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

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

- Sign In -

- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/account/result.html b/src/DjangoBlog-master/templates/account/result.html deleted file mode 100644 index 23c9094..0000000 --- a/src/DjangoBlog-master/templates/account/result.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load i18n %} -{% block header %} - {{ title }} -{% endblock %} -{% block content %} -
- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/blog/article_archives.html b/src/DjangoBlog-master/templates/blog/article_archives.html deleted file mode 100644 index 5343a1d..0000000 --- a/src/DjangoBlog-master/templates/blog/article_archives.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends 'share_layout/base.html' %} {# 继承基础模板,复用基础模板的结构和样式 #} -{% load blog_tags %} {# 加载自定义的blog_tags模板标签库,以便使用其中的模板标签 #} -{% load cache %} {# 加载缓存模板标签库,用于缓存部分页面内容 #} -{% load i18n %} {# 加载国际化模板标签库,支持多语言翻译 #} -{% block header %} {# 定义页面头部的block,用于填充页面头部相关内容 #} - - {% trans 'article archive' %} | {{ SITE_DESCRIPTION }} - {# 设置页面标题,使用国际化翻译显示“文章归档”,并拼接站点描述 #} - - - {# 设置页面描述元数据 #} - - - - - - -{% endblock %} -{% block content %} {# 定义页面主要内容的block #} -
{# 主要内容的外层容器,设置id和class用于样式控制 #} -
{# 主要内容区域,role属性表明其主要内容的角色 #} - -
{# 文章归档部分的头部容器 #} - -

{% trans 'article archive' %}

{# 显示“文章归档”的标题 #} -
- -
{# 文章归档的内容区域 #} - - {% regroup article_list by pub_time.year as year_post_group %} {# 将文章列表按发布时间的年份分组,结果保存到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' %} - {# 显示月份,以及“月”的国际化翻译 #} -
        - {% for article in month.list %} -
      • {{ article.title }} -
      • {# 显示文章标题,并设置链接到文章详情页 #} - {% endfor %} -
      -
    • - {% endfor %} -
    -
  • - {% endfor %} -
-
-
-
- -{% endblock %} - - -{% block sidebar %} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master/templates/blog/article_detail.html b/src/DjangoBlog-master/templates/blog/article_detail.html deleted file mode 100644 index 6def375..0000000 --- a/src/DjangoBlog-master/templates/blog/article_detail.html +++ /dev/null @@ -1,66 +0,0 @@ -{# 继承基础模板,复用基础模板(如导航栏、页脚、通用样式等)的结构和公共内容 #} -{% extends 'share_layout/base.html' %} -{# 加载自定义的blog_tags模板标签库,用于使用库中定义的自定义模板标签 #} -{% load blog_tags %} - -{% block header %} -{% endblock %} -{# 定义页面主要内容block(content),承载当前页面的核心展示内容 #} -{% block content %} - {# 主要内容外层容器,通过id和class控制样式,确保与站点整体布局风格统一 #} -
- {# 核心内容区域,role="main"是ARIA属性,用于辅助设备识别这是页面主要内容区 #} -
- {# 调用blog_tags库中的load_article_detail自定义标签,渲染文章详情内容 #} - {% load_article_detail article False user %} - - {# 条件判断:如果当前文章的类型为'a',则显示文章导航栏 #} - {% if article.type == 'a' %} - {# 文章导航容器,用于展示上一篇/下一篇文章的跳转链接 #} - - {% endif %} - -
- - {# 文章导航容器,用于展示上一篇/下一篇文章的跳转链接 #} - {% if article.comment_status == "o" and OPEN_SITE_COMMENT %} - -{# 引入评论列表模板(comments/tags/comment_list.html),渲染当前文章已有的评论内容 #} - {% include 'comments/tags/comment_list.html' %} - - {# 条件判断:如果用户已登录(user.is_authenticated),则显示“发表评论”的表单模板 #} - {% if user.is_authenticated %} - {% include 'comments/tags/post_comment.html' %} - - {# 若用户未登录,提示登录后才能评论,并提供登录链接和第三方登录入口 #} - {% else %} -
-

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

-{# 登录链接,next参数携带当前页面的完整URL,确保用户登录后能跳转回当前文章页 #} - {% 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/src/DjangoBlog-master/templates/blog/article_index.html b/src/DjangoBlog-master/templates/blog/article_index.html deleted file mode 100644 index 0c4aafd..0000000 --- a/src/DjangoBlog-master/templates/blog/article_index.html +++ /dev/null @@ -1,51 +0,0 @@ -{# 继承基础模板,复用基础模板中的公共结构(如导航栏、页脚)和样式 #} -{% 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 %} - {# 页面元数据:设置站点SEO描述,用于搜索引擎抓取时展示的页面简介 #} - - - - - - - -{% endblock %} -{# 定义页面主要内容(content)的内容块,承载文章列表和分页等核心内容 #} -{% block content %} -
-
- {% if page_type and tag_name %} -
- -

{{ page_type }}:{{ tag_name }}

-
- {% endif %} - - {# 循环遍历文章列表(article_list),逐个渲染文章内容 #} - {% for article in article_list %} - {% load_article_detail article True user %} - {% endfor %} - - {# 条件判断:如果文章列表支持分页(is_paginated为True),则渲染分页控件 #} - {% if is_paginated %} - {% load_pagination_info page_obj page_type tag_name %} - - {% endif %} -
-
- -{% endblock %} - -{# 定义页面侧边栏(sidebar)的内容块,承载侧边栏组件 #} -{% block sidebar %} - {# 调用自定义标签load_sidebar,渲染侧边栏内容 #} - {% load_sidebar user linktype %} -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/blog/error_page.html b/src/DjangoBlog-master/templates/blog/error_page.html deleted file mode 100644 index aad3109..0000000 --- a/src/DjangoBlog-master/templates/blog/error_page.html +++ /dev/null @@ -1,58 +0,0 @@ -{# 继承基础模板 #} -{% extends 'share_layout/base.html' %} - -{# 加载自定义的blog_tags模板标签库,用于调用库中定义的自定义模板标签 #} -{% load blog_tags %} -{% load cache %} -{% block header %} - {# 条件判断:如果存在tag_name(通常用于标记特定页面场景,如错误页、标签页),则进入场景化标题配置 #} - {% if tag_name %} - {# 嵌套条件:根据statuscode(状态码)判断具体页面类型,设置对应的浏览器标签标题 #} - {% 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 %} - {# 页面元数据:设置站点SEO专用描述,用于搜索引擎抓取时展示的页面简介,提升SEO效果 #} - - - - - - - -{% endblock %} - -{# 定义页面主要内容(content)块,承载当前页面的核心展示信息(此处以错误提示为主) #} -{% block content %} - {# 主要内容外层容器,通过id和class控制样式,确保与站点整体布局风格统一 #} -
-
- - {# 页面头部容器(通常用于显示页面标题),此处适配错误页/提示页的标题展示 #} -
- {# 页面核心标题:显示动态传递的message变量(如“页面未找到”“权限不足”等错误提示文本) #} -

{{ message }}

-
- -
-
- -{% endblock %} - -{# 定义页面侧边栏(sidebar)块,承载侧边栏组件 #} -{% block sidebar %} - {# 调用blog_tags库中的load_sidebar自定义标签,渲染侧边栏内容 #} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master/templates/blog/links_list.html b/src/DjangoBlog-master/templates/blog/links_list.html deleted file mode 100644 index 9faf290..0000000 --- a/src/DjangoBlog-master/templates/blog/links_list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# 继承基础模板 #} -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{# 加载自定义的blog_tags模板标签库,后续可调用库中定义的自定义标签 #} -{% load cache %} -{% block header %} - - 友情链接 | {{ SITE_DESCRIPTION }} - - - - {# Open Graph(OG)协议标签:指定内容类型为“博客”,用于社交平台(如微信、微博)分享时识别内容类型 #} - - {# OG标签:社交分享时显示的标题,使用项目全局配置的站点名称(SITE_NAME) #} - - {# OG标签:社交分享时显示的描述,使用站点全局描述(SITE_DESCRIPTION) #} - - {# OG标签:社交分享时关联的页面URL,使用项目全局配置的基础域名(SITE_BASE_URL) #} - - {# OG标签:社交分享时显示的站点名称,与SITE_NAME保持一致 #} - - -{% endblock %} -{# 定义页面主要内容(content)块,承载“友情链接列表”的核心展示逻辑 #} -{% block content %} - {# 主要内容外层容器,通过id(primary)和class(site-content)控制样式,确保与站点整体布局统一 #} -
-
- -
- -

友情链接

-
- -
-
    - {# 循环遍历友情链接数据列表(object_list为视图传递的查询集,包含所有友情链接对象) #} - {% for obj in object_list %} - {# 单个友情链接项(li),每个项对应一条友情链接 #} -
  • - {{ obj.name }} -
  • - {% endfor %}
-
-
-
- -{% endblock %} - -{# 定义页面侧边栏(sidebar)内容块,加载站点侧边栏组件 #} -{% block sidebar %} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master/templates/blog/tags/article_info.html b/src/DjangoBlog-master/templates/blog/tags/article_info.html deleted file mode 100644 index 3deec44..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/article_info.html +++ /dev/null @@ -1,74 +0,0 @@ -{% 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/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html b/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html deleted file mode 100644 index cb6111c..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/article_meta_info.html +++ /dev/null @@ -1,59 +0,0 @@ -{% load i18n %} -{% load blog_tags %} - - - - - diff --git a/src/DjangoBlog-master/templates/blog/tags/article_pagination.html b/src/DjangoBlog-master/templates/blog/tags/article_pagination.html deleted file mode 100644 index 95514ff..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/article_pagination.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load i18n %} - \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html b/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html deleted file mode 100644 index c8ba474..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/article_tag_list.html +++ /dev/null @@ -1,19 +0,0 @@ -{% 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/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html b/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html deleted file mode 100644 index 67087d5..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/breadcrumb.html +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/src/DjangoBlog-master/templates/blog/tags/sidebar.html b/src/DjangoBlog-master/templates/blog/tags/sidebar.html deleted file mode 100644 index f70544c..0000000 --- a/src/DjangoBlog-master/templates/blog/tags/sidebar.html +++ /dev/null @@ -1,136 +0,0 @@ -{% load blog_tags %} -{% load i18n %} - diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_item.html b/src/DjangoBlog-master/templates/comments/tags/comment_item.html deleted file mode 100644 index ebb0388..0000000 --- a/src/DjangoBlog-master/templates/comments/tags/comment_item.html +++ /dev/null @@ -1,34 +0,0 @@ -{% load blog_tags %} -
  • -
    - - - -

    {{ comment_item.body|escape|comment_markdown }}

    - -
    - -
  • \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html deleted file mode 100644 index a9decd1..0000000 --- a/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html +++ /dev/null @@ -1,54 +0,0 @@ -{% 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/src/DjangoBlog-master/templates/comments/tags/comment_list.html b/src/DjangoBlog-master/templates/comments/tags/comment_list.html deleted file mode 100644 index 4092161..0000000 --- a/src/DjangoBlog-master/templates/comments/tags/comment_list.html +++ /dev/null @@ -1,45 +0,0 @@ - -
    - {% 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/src/DjangoBlog-master/templates/comments/tags/post_comment.html b/src/DjangoBlog-master/templates/comments/tags/post_comment.html deleted file mode 100644 index 3ae5a27..0000000 --- a/src/DjangoBlog-master/templates/comments/tags/post_comment.html +++ /dev/null @@ -1,33 +0,0 @@ -
    - -
    -

    发表评论 - -

    -
    {% 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/src/DjangoBlog-master/templates/oauth/bindsuccess.html b/src/DjangoBlog-master/templates/oauth/bindsuccess.html deleted file mode 100644 index 4bee77c..0000000 --- a/src/DjangoBlog-master/templates/oauth/bindsuccess.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% block header %} - {{ title }} -{% endblock %} -{% block content %} -
    -
    - -
    - -

    {{ content }}

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

    - 登录 -

    - -
    -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/owntracks/show_log_dates.html b/src/DjangoBlog-master/templates/owntracks/show_log_dates.html deleted file mode 100644 index 7dbba21..0000000 --- a/src/DjangoBlog-master/templates/owntracks/show_log_dates.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - 记录日期 - - - -
      - {% for date in results %} -
    • - {{ date }} -
    • - {% endfor %} -
    - - \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/owntracks/show_maps.html b/src/DjangoBlog-master/templates/owntracks/show_maps.html deleted file mode 100644 index 3aeda36..0000000 --- a/src/DjangoBlog-master/templates/owntracks/show_maps.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - 运动轨迹 - - - -
    - - - - - - - - \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/search/indexes/blog/article_text.txt b/src/DjangoBlog-master/templates/search/indexes/blog/article_text.txt deleted file mode 100644 index 4f9ca76..0000000 --- a/src/DjangoBlog-master/templates/search/indexes/blog/article_text.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ object.title }} -{{ object.author.username }} -{{ object.body }} \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/search/search.html b/src/DjangoBlog-master/templates/search/search.html deleted file mode 100644 index 1404c60..0000000 --- a/src/DjangoBlog-master/templates/search/search.html +++ /dev/null @@ -1,66 +0,0 @@ -{% 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/src/DjangoBlog-master/templates/share_layout/adsense.html b/src/DjangoBlog-master/templates/share_layout/adsense.html deleted file mode 100644 index 8f99c55..0000000 --- a/src/DjangoBlog-master/templates/share_layout/adsense.html +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/share_layout/base.html b/src/DjangoBlog-master/templates/share_layout/base.html deleted file mode 100644 index 75d0df5..0000000 --- a/src/DjangoBlog-master/templates/share_layout/base.html +++ /dev/null @@ -1,123 +0,0 @@ -{% 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/src/DjangoBlog-master/templates/share_layout/base_account.html b/src/DjangoBlog-master/templates/share_layout/base_account.html deleted file mode 100644 index c00d842..0000000 --- a/src/DjangoBlog-master/templates/share_layout/base_account.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - {% 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/src/DjangoBlog-master/templates/share_layout/footer.html b/src/DjangoBlog-master/templates/share_layout/footer.html deleted file mode 100644 index cd86a29..0000000 --- a/src/DjangoBlog-master/templates/share_layout/footer.html +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/src/DjangoBlog-master/templates/share_layout/nav.html b/src/DjangoBlog-master/templates/share_layout/nav.html deleted file mode 100644 index 24d4da6..0000000 --- a/src/DjangoBlog-master/templates/share_layout/nav.html +++ /dev/null @@ -1,30 +0,0 @@ -{% load i18n %} - - \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/share_layout/nav_node.html b/src/DjangoBlog-master/templates/share_layout/nav_node.html deleted file mode 100644 index c266880..0000000 --- a/src/DjangoBlog-master/templates/share_layout/nav_node.html +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/说明书.txt b/src/说明书.txt deleted file mode 100644 index e69de29..0000000