├── config ├── __init__.py ├── settings │ ├── __init__.py │ └── test.py ├── asgi.py └── wsgi.py ├── blogproject ├── __init__.py ├── alerts │ ├── tests.py │ ├── views.py │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alert_scopes.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ └── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── models.py │ │ └── factories.py │ ├── apps.py │ ├── models.py │ ├── decrators.py │ └── views.py ├── tags │ ├── __init__.py │ ├── views.py │ ├── tests │ │ ├── __init__.py │ │ └── factories.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── users │ ├── views.py │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── factories.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── create_tokens.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0002_auto_20200919_1157.py │ ├── apps.py │ ├── serializers.py │ ├── adapter.py │ ├── models.py │ └── admin.py ├── comments │ ├── utils.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ └── test_forms.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_auto_20200920_1835.py │ │ └── 0002_blogcomment_user.py │ ├── templatetags │ │ ├── __init__.py │ │ └── comments_extras.py │ ├── urls.py │ ├── apps.py │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ └── serializers.py ├── courses │ ├── __init__.py │ ├── signals.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ └── test_templatetags.py │ ├── migrations │ │ └── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── courses_extras.py │ ├── static │ │ └── courses │ │ │ └── images │ │ │ └── pay │ │ │ ├── alipay0.jpg │ │ │ ├── alipay99.jpg │ │ │ ├── alipay199.jpg │ │ │ ├── alipay299.jpg │ │ │ ├── alipay599.jpg │ │ │ ├── wechatpay0.png │ │ │ ├── wechatpay99.png │ │ │ ├── wechatpay199.png │ │ │ ├── wechatpay299.png │ │ │ └── wechatpay599.png │ ├── apps.py │ ├── search_indexes.py │ ├── urls.py │ └── managers.py ├── favorites │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── factories.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto_20200920_1601.py │ │ ├── 0003_auto_20200920_1604.py │ │ └── 0004_auto_20210411_1737.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ └── views.py ├── friendlinks │ ├── tests.py │ ├── views.py │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── notify │ ├── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── notify_tags.py │ ├── context_processors.py │ ├── urls.py │ ├── factories.py │ └── views.py ├── scripts │ ├── __init__.py │ └── fake │ │ ├── _tags.py │ │ ├── __init__.py │ │ ├── _allauth.py │ │ ├── _mediums.py │ │ ├── _users.py │ │ ├── _friend_links.py │ │ ├── _recommendations.py │ │ ├── _course_categories.py │ │ ├── _issues.py │ │ ├── _superuser.py │ │ ├── _post_categories.py │ │ ├── _favorites.py │ │ ├── _courses.py │ │ ├── _posts.py │ │ ├── _materials.py │ │ ├── all.py │ │ ├── _comments.py │ │ └── _clean_db.py ├── taskapp │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ └── __init__.py │ ├── tasks.py │ └── celery.py ├── webtools │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── tests.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── urls.py │ ├── views.py │ └── forms.py ├── blog │ ├── tests │ │ ├── __init__.py │ │ ├── test_utils.py │ │ └── factories.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_remove_post_tags.py │ │ ├── 0003_post_meta_ordering.py │ │ ├── 0006_post_tags.py │ │ ├── 0007_auto_20210411_1737.py │ │ └── 0004_auto_20200306_1047.py │ ├── templatetags │ │ ├── __init__.py │ │ └── blog_extras.py │ ├── __init__.py │ ├── static │ │ └── blog │ │ │ └── images │ │ │ ├── ad.jpg │ │ │ ├── alipay.jpg │ │ │ ├── aliyun.jpg │ │ │ ├── logo.png │ │ │ ├── upyun_logo.png │ │ │ ├── weixinpay.jpg │ │ │ └── tencentcloud.jpg │ ├── search_indexes.py │ ├── apps.py │ ├── urls.py │ ├── feeds.py │ ├── sitemaps.py │ ├── utils.py │ └── managers.py ├── newsletters │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_view.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── forms.py ├── functional_tests │ └── __init__.py ├── templates │ ├── blog │ │ ├── inclusions │ │ │ ├── _toc_content.html │ │ │ ├── _tags.html │ │ │ ├── _ad.html │ │ │ ├── _sidebar_mobile.html │ │ │ ├── _toc.html │ │ │ ├── _friend_link.html │ │ │ ├── _related.html │ │ │ ├── _recommendation.html │ │ │ ├── _medium.html │ │ │ ├── _donate.html │ │ │ ├── _detail.html │ │ │ ├── _pagination.html │ │ │ └── _entry_list_item.html │ │ ├── category.html │ │ ├── index.html │ │ ├── category_list.html │ │ ├── detail.html │ │ ├── archives.html │ │ └── donate.html │ ├── search │ │ ├── indexes │ │ │ ├── blog │ │ │ │ └── post_text.txt │ │ │ └── courses │ │ │ │ └── material_text.txt │ │ ├── search.html │ │ └── _search_entry_list_item.html │ ├── 503.html │ ├── comments │ │ ├── email │ │ │ ├── comment.txt │ │ │ ├── reply.txt │ │ │ ├── reply.html │ │ │ └── comment.html │ │ └── inclusions │ │ │ └── _comments_app.html │ ├── courses │ │ ├── course_detail.html │ │ ├── inclusions │ │ │ ├── _description.html │ │ │ ├── _sidebar_mobile.html │ │ │ ├── _prev_next.html │ │ │ ├── _sidebar_desk.html │ │ │ ├── _course_list_item.html │ │ │ ├── _material_meta.html │ │ │ └── _toc.html │ │ ├── material_detail.html │ │ └── base.html │ ├── notifications │ │ ├── comment.html │ │ ├── inclusions │ │ │ ├── _comment.html │ │ │ └── _reply.html │ │ ├── reply.html │ │ ├── base.html │ │ └── list.html │ ├── account │ │ ├── login.html │ │ └── inclusions │ │ │ └── _login.html │ ├── inclusions │ │ ├── _simple_pagination.html │ │ └── _footer.html │ └── sidebar_mobile_base.html ├── database │ └── README.md └── conftest.py ├── devops └── ansible │ ├── backup.yml │ ├── restore.yml │ ├── roles │ ├── redis │ │ └── tasks │ │ │ └── main.yml │ ├── nginx │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── supervisor │ │ └── tasks │ │ │ └── main.yml │ ├── postgresql │ │ └── tasks │ │ │ └── main.yml │ ├── project │ │ ├── tasks │ │ │ ├── deps.yml │ │ │ ├── envfile.yml │ │ │ ├── main.yml │ │ │ ├── repo.yml │ │ │ ├── management.yml │ │ │ ├── supervisor.yml │ │ │ ├── nginx.yml │ │ │ └── db.yml │ │ ├── templates │ │ │ ├── nginx │ │ │ │ ├── proxy.conf.j2 │ │ │ │ └── blogproject.conf.j2 │ │ │ └── supervisor │ │ │ │ ├── blogproject-celery-worker.conf.j2 │ │ │ │ ├── blogproject-celery-beat.conf.j2 │ │ │ │ └── blogproject.conf.j2 │ │ └── handlers │ │ │ └── main.yml │ ├── poetry │ │ └── tasks │ │ │ └── main.yml │ ├── pipx │ │ └── tasks │ │ │ └── main.yml │ └── pyenv │ │ └── tasks │ │ └── main.yml │ ├── hosts.yml │ ├── site.yml │ └── group_vars │ └── all.yml ├── frontend ├── src │ ├── scripts │ │ ├── donate.js │ │ ├── search.ts │ │ ├── backtop.ts │ │ ├── toc.ts │ │ └── offcanvas.ts │ ├── axiosService.js │ ├── style │ │ ├── _tasklist.scss │ │ ├── _favorite.scss │ │ ├── _pagination.scss │ │ ├── _util.scss │ │ ├── _login.scss │ │ ├── _aside.scss │ │ ├── _literal.scss │ │ ├── _alert.scss │ │ ├── _header.scss │ │ ├── _backtop.scss │ │ ├── _hilite.scss │ │ ├── _widget.scss │ │ ├── _post.scss │ │ ├── _tabbed.scss │ │ ├── _menu.scss │ │ ├── _notification.scss │ │ ├── _offcanvas.scss │ │ ├── _donate.scss │ │ ├── _toc.scss │ │ ├── _sidebar.scss │ │ ├── _navbar.scss │ │ └── _course.scss │ ├── shims-vue.d.ts │ ├── images │ │ ├── error-warning-fill.svg │ │ ├── information-fill.svg │ │ ├── alert-fill.svg │ │ └── lightbulb-line.svg │ ├── main.ts │ ├── api.js │ ├── styles.scss │ └── components │ │ └── CommentList.vue ├── .browserslistrc ├── build │ ├── favicon.ico │ ├── img │ │ ├── error-warning-fill.027f8c93.svg │ │ ├── information-fill.dfcc3b8f.svg │ │ ├── alert-fill.bbcee1b1.svg │ │ └── lightbulb-line.a44c3828.svg │ ├── index.html │ └── manifest.json ├── public │ ├── favicon.ico │ └── index.html ├── .prettierrc ├── manifest-test.json ├── README.md ├── babel.config.js ├── tsconfig.json ├── .eslintrc.js ├── package.json └── vue.config.js ├── .dockerignore ├── docs ├── tag.md ├── friendlink.md ├── img │ ├── logo.png │ ├── add_post.png │ ├── add_course.png │ ├── friendlink.png │ ├── post_brief.png │ ├── add_category.png │ ├── add_material.png │ ├── admin_index.png │ ├── configuration.png │ ├── course_detail.png │ ├── course_list.png │ ├── blog_post_list.png │ ├── course_category.png │ ├── blog_category_list.png │ ├── blog_category_nav.png │ └── admin_course_category_list.png ├── index.md ├── overview.md ├── category.md ├── configuration.md ├── post.md └── course.md ├── screenshot.png ├── compose ├── local │ ├── django │ │ ├── celery │ │ │ ├── worker │ │ │ │ └── start.sh │ │ │ └── beat │ │ │ │ └── start.sh │ │ ├── start.sh │ │ └── Dockerfile │ └── node │ │ └── Dockerfile ├── production │ ├── django │ │ ├── celery │ │ │ ├── worker │ │ │ │ └── start.sh │ │ │ └── beat │ │ │ │ └── start.sh │ │ ├── start.sh │ │ └── entrypoint.sh │ ├── postgres │ │ ├── maintenance │ │ │ ├── _sourced │ │ │ │ ├── constants.sh │ │ │ │ ├── yes_no.sh │ │ │ │ ├── countdown.sh │ │ │ │ └── messages.sh │ │ │ ├── backups │ │ │ └── backup │ │ └── Dockerfile │ ├── nginx │ │ ├── includes │ │ │ └── proxy.conf │ │ ├── Dockerfile │ │ ├── DockerfileMainland │ │ └── conf.d │ │ │ └── blogproject.conf-tmpl │ └── statusok │ │ └── config │ │ └── config.example.json └── external │ └── django │ └── DockerfileMainland ├── .envs └── .local │ ├── .django │ └── .postgres ├── setup.cfg ├── README.md ├── mkdocs.yml └── manage.py /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/alerts/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/alerts/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/core/admin.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/tags/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devops/ansible/backup.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devops/ansible/restore.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/alerts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/comments/utils.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/courses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/courses/signals.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/favorites/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/friendlinks/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/friendlinks/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/notify/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_tags.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/taskapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/webtools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/webtools/admin.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/webtools/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/webtools/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/scripts/donate.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/blog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/comments/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/courses/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/friendlinks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/newsletters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_allauth.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_mediums.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_users.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/tags/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/alerts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/blog/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/comments/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/courses/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/favorites/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/functional_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/newsletters/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/notify/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_friend_links.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/tags/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/taskapp/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/webtools/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | frontend/node_modules/* 3 | -------------------------------------------------------------------------------- /blogproject/comments/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/courses/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/favorites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/friendlinks/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/newsletters/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_recommendations.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/taskapp/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/users/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /blogproject/blog/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "blog.apps.BlogConfig" 2 | -------------------------------------------------------------------------------- /blogproject/comments/urls.py: -------------------------------------------------------------------------------- 1 | app_name = "comments" 2 | urlpatterns = [] 3 | -------------------------------------------------------------------------------- /blogproject/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "core.tests.apps.TestsConfig" 2 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_toc_content.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/templates/search/indexes/blog/post_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.title }} 2 | {{ object.body }} -------------------------------------------------------------------------------- /docs/tag.md: -------------------------------------------------------------------------------- 1 | 类似于发布博客文章,点击 **博客** 板块下的 **标签** 可进入到已有标签列表。 2 | 3 | 在标签列表下,点击右上角的 **增加标签** 进入到新增标签页面。 -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/screenshot.png -------------------------------------------------------------------------------- /blogproject/templates/search/indexes/courses/material_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.title }} 2 | {{ object.body }} -------------------------------------------------------------------------------- /docs/friendlink.md: -------------------------------------------------------------------------------- 1 | 可以在 **博客** 板块的 **友情链接** 下新增友链,友链会显示在博客侧边栏: 2 | 3 | ![](img/friendlink.png) 4 | 5 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/logo.png -------------------------------------------------------------------------------- /docs/img/add_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/add_post.png -------------------------------------------------------------------------------- /compose/local/django/celery/worker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | celery -A blogproject.taskapp worker -l INFO 4 | -------------------------------------------------------------------------------- /docs/img/add_course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/add_course.png -------------------------------------------------------------------------------- /docs/img/friendlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/friendlink.png -------------------------------------------------------------------------------- /docs/img/post_brief.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/post_brief.png -------------------------------------------------------------------------------- /compose/production/django/celery/worker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | celery -A blogproject.taskapp worker -l INFO 4 | -------------------------------------------------------------------------------- /docs/img/add_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/add_category.png -------------------------------------------------------------------------------- /docs/img/add_material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/add_material.png -------------------------------------------------------------------------------- /docs/img/admin_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/admin_index.png -------------------------------------------------------------------------------- /docs/img/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/configuration.png -------------------------------------------------------------------------------- /docs/img/course_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/course_detail.png -------------------------------------------------------------------------------- /docs/img/course_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/course_list.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 本文档为 [django-blog-project](https://github.com/zmrenwu/django-blog-project) 用户使用手册,主要以图文的方式讲解如何使用博客的各项功能。 -------------------------------------------------------------------------------- /frontend/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/frontend/build/favicon.ico -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | USE_DOCKER=yes 2 | DJANGO_SETTINGS_MODULE=config.settings.local 3 | REDIS_URL=redis://redis:6379/0 4 | -------------------------------------------------------------------------------- /docs/img/blog_post_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/blog_post_list.png -------------------------------------------------------------------------------- /docs/img/course_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/course_category.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /blogproject/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "core" 6 | -------------------------------------------------------------------------------- /docs/img/blog_category_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/blog_category_list.png -------------------------------------------------------------------------------- /docs/img/blog_category_nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/blog_category_nav.png -------------------------------------------------------------------------------- /blogproject/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = "users" 6 | -------------------------------------------------------------------------------- /blogproject/core/tests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestsConfig(AppConfig): 5 | name = "core.tests" 6 | -------------------------------------------------------------------------------- /blogproject/favorites/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FavoritesConfig(AppConfig): 5 | name = "favorites" 6 | -------------------------------------------------------------------------------- /blogproject/webtools/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WebtoolsConfig(AppConfig): 5 | name = "webtools" 6 | -------------------------------------------------------------------------------- /blogproject/newsletters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NewslettersConfig(AppConfig): 5 | name = "newsletters" 6 | -------------------------------------------------------------------------------- /docs/img/admin_course_category_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/docs/img/admin_course_category_list.png -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/ad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/ad.jpg -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/alipay.jpg -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/aliyun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/aliyun.jpg -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/logo.png -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /devops/ansible/roles/redis/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Redis is installed 3 | ansible.builtin.package: 4 | name: redis 5 | state: latest -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/upyun_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/upyun_logo.png -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/weixinpay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/weixinpay.jpg -------------------------------------------------------------------------------- /blogproject/blog/static/blog/images/tencentcloud.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/blog/static/blog/images/tencentcloud.jpg -------------------------------------------------------------------------------- /devops/ansible/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: Restart nginx 2 | service: 3 | name: nginx 4 | state: restarted 5 | become: yes 6 | become_method: sudo -------------------------------------------------------------------------------- /devops/ansible/roles/supervisor/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Supervisor is installed 3 | ansible.builtin.package: 4 | name: supervisor 5 | state: present -------------------------------------------------------------------------------- /devops/ansible/roles/postgresql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure PostgreSQL is installed 3 | ansible.builtin.package: 4 | name: postgresql-12 5 | state: present -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/alipay0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/alipay0.jpg -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/alipay99.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/alipay99.jpg -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/deps.yml: -------------------------------------------------------------------------------- 1 | - name: Install dependencies 2 | ansible.builtin.shell: 3 | chdir: "{{ project_path }}" 4 | cmd: ". ~/.profile && poetry install" -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/alipay199.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/alipay199.jpg -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/alipay299.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/alipay299.jpg -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/alipay599.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/alipay599.jpg -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/wechatpay0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/wechatpay0.png -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/wechatpay99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/wechatpay99.png -------------------------------------------------------------------------------- /frontend/src/axiosService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const instance = axios.create({ 4 | baseURL: "/api/v1", 5 | timeout: 10000, 6 | }); 7 | 8 | export default instance -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/wechatpay199.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/wechatpay199.png -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/wechatpay299.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/wechatpay299.png -------------------------------------------------------------------------------- /blogproject/courses/static/courses/images/pay/wechatpay599.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jukanntenn/django-blog-project/HEAD/blogproject/courses/static/courses/images/pay/wechatpay599.png -------------------------------------------------------------------------------- /compose/local/django/celery/beat/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f './celerybeat.pid' 4 | celery -A blogproject.taskapp beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /compose/local/django/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py compilemessages 4 | python manage.py migrate 5 | python manage.py setup_periodic_tasks 6 | python manage.py runserver 0.0.0.0:8000 7 | -------------------------------------------------------------------------------- /compose/production/django/celery/beat/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f './celerybeat.pid' 4 | celery -A blogproject.taskapp beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler 5 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_course_categories.py: -------------------------------------------------------------------------------- 1 | from courses.tests.factories import CategoryFactory 2 | 3 | 4 | def run(): 5 | CategoryFactory.create_batch(3) 6 | print("Course categories created.") 7 | -------------------------------------------------------------------------------- /blogproject/templates/503.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 503 6 | 7 | 8 |

服务暂不可用

9 | 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 4 | exclude = 5 | .tox, 6 | .git, 7 | */migrations/*, 8 | */static/CACHE/*, 9 | docs, 10 | node_modules 11 | -------------------------------------------------------------------------------- /blogproject/tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class TagsConfig(AppConfig): 6 | name = "tags" 7 | verbose_name = _("Tags") 8 | -------------------------------------------------------------------------------- /blogproject/alerts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class AlertsConfig(AppConfig): 6 | name = "alerts" 7 | verbose_name = _("Alerts") 8 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/envfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy envfile 3 | ansible.builtin.template: 4 | src: blogproject.env.j2 5 | dest: "{{ project_path }}/blogproject.env" 6 | notify: 7 | Restart program -------------------------------------------------------------------------------- /blogproject/courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CoursesConfig(AppConfig): 6 | name = "courses" 7 | verbose_name = _("Courses") 8 | -------------------------------------------------------------------------------- /compose/local/node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-stretch-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY ./frontend/package.json /app 6 | 7 | RUN npm install && npm cache clean --force 8 | 9 | ENV PATH ./node_modules/.bin/:$PATH 10 | -------------------------------------------------------------------------------- /blogproject/comments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CommentsConfig(AppConfig): 6 | name = "comments" 7 | verbose_name = _("comments") 8 | -------------------------------------------------------------------------------- /frontend/src/style/_tasklist.scss: -------------------------------------------------------------------------------- 1 | .task-list .task-list-item { 2 | list-style-type: none !important; 3 | } 4 | 5 | .task-list .task-list-item input[type="checkbox"] { 6 | margin: 0 4px 0.25em -20px; 7 | vertical-align: middle; 8 | } -------------------------------------------------------------------------------- /blogproject/comments/__init__.py: -------------------------------------------------------------------------------- 1 | def get_model(): 2 | from .models import BlogComment 3 | 4 | return BlogComment 5 | 6 | 7 | def get_form(): 8 | from .forms import BlogCommentForm 9 | 10 | return BlogCommentForm 11 | -------------------------------------------------------------------------------- /blogproject/friendlinks/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class FriendlinksConfig(AppConfig): 6 | name = "friendlinks" 7 | verbose_name = _("Friend Links") 8 | -------------------------------------------------------------------------------- /blogproject/templates/comments/email/comment.txt: -------------------------------------------------------------------------------- 1 | {{ comment.user.name }} 在 {{ content_object.title }} 中发布了评论: 2 | 3 | {{ comment.comment_html | safe }} 4 | 5 | 请复制以下链接到浏览器打开: 6 | {{ link }} 7 | 8 | ----------------------- 9 | 发自:{{ site.name }} -------------------------------------------------------------------------------- /frontend/src/style/_favorite.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .issue { 4 | .issue-date { 5 | margin-right: $width-gap-half; 6 | } 7 | } 8 | 9 | .tag { 10 | font-size: $font-size-small; 11 | margin-right: $width-gap-half; 12 | } -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | POSTGRES_HOST=postgres 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB=blogproject 6 | POSTGRES_USER=debug 7 | POSTGRES_PASSWORD=debug 8 | -------------------------------------------------------------------------------- /blogproject/templates/comments/email/reply.txt: -------------------------------------------------------------------------------- 1 | {{ comment.user.name }} 在 {{ content_object.title }} 中回复了你: 2 | 3 | {{ comment.comment_html | safe }} 4 | 5 | 请复制以下链接到浏览器打开: 6 | {{ link }} 7 | 8 | ----------------------- 9 | 发自:{{ site.name }} 10 | -------------------------------------------------------------------------------- /blogproject/templates/courses/course_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'courses/base.html' %} 2 | {% load comments static %} 3 | {% load courses_extras %} 4 | 5 | {% block article %} 6 | {% include 'courses/inclusions/_description.html' %} 7 | {% endblock article %} -------------------------------------------------------------------------------- /blogproject/templates/blog/category.html: -------------------------------------------------------------------------------- 1 | {% extends 'blog/index.html' %} 2 | 3 | {% block description %} 4 | {% if category.description %} 5 | 6 | {% endif %} 7 | {% endblock description %} -------------------------------------------------------------------------------- /blogproject/newsletters/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Subscription 4 | 5 | 6 | @admin.register(Subscription) 7 | class SubscriptionAdmin(admin.ModelAdmin): 8 | list_display = ["id", "user", "email", "confirmed", "active"] 9 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_issues.py: -------------------------------------------------------------------------------- 1 | from favorites.tests.factories import IssueFactory 2 | from tags.tests.factories import TagFactory 3 | 4 | 5 | def run(): 6 | IssueFactory.create_batch(40, tags=[TagFactory(), TagFactory()]) 7 | print("Issues created!") 8 | -------------------------------------------------------------------------------- /blogproject/database/README.md: -------------------------------------------------------------------------------- 1 | 为了兼容 Docker,默认的 sqlite 数据库生成在项目根目录的 database 目录下,因此在生成数据库之前需要确保项目根目录下 database 文件夹的存在。否则在生成数据库时会报错: 2 | 3 | ``` 4 | django.db.utils.OperationalError: unable to open database file 5 | ``` 6 | 7 | 如果使用 MySQL、PostgreSQL 等数据库引擎,则 database 文件夹可有可无。 -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | ## 登录博客后台 2 | 假设博客部署域名为 zmrenwu.com,则后台的入口在 zmrenwu.com/admin/。 3 | 4 | 浏览器打开入口地址,输入 superuser 的账户和密码即可登录博客后台,对博客的内容进行管理。 5 | 6 | 下面是登录后的页面: 7 | 8 | ![Django 博客后台概览](img/admin_index.png) 9 | 10 | 博客系统中的大部分数据都可以在后台进行增删改查。红色方框重点标出了博客内容管理模块,下面来逐一介绍。 -------------------------------------------------------------------------------- /blogproject/scripts/fake/_superuser.py: -------------------------------------------------------------------------------- 1 | from users.models import User 2 | 3 | 4 | def run(): 5 | User.objects.create_superuser( 6 | username="admin", password="test123456", email="admin@example.com", name="admin" 7 | ) 8 | print("Superuser 'admin' created.") 9 | -------------------------------------------------------------------------------- /frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue'; 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | 8 | // declare module './scripts/backtop.js'; 9 | -------------------------------------------------------------------------------- /blogproject/tags/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from factory.django import DjangoModelFactory 3 | from tags.models import Tag 4 | 5 | 6 | class TagFactory(DjangoModelFactory): 7 | name = factory.Faker("uuid4") 8 | 9 | class Meta: 10 | model = Tag 11 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_tags.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
-------------------------------------------------------------------------------- /compose/production/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:12.3 2 | 3 | COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /blogproject/comments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_comments.admin import CommentsAdmin 3 | 4 | from .models import BlogComment 5 | 6 | 7 | @admin.register(BlogComment) 8 | class BlogCommentAdmin(CommentsAdmin): 9 | list_select_related = ["user"] 10 | -------------------------------------------------------------------------------- /frontend/src/images/error-warning-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/images/information-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compose/production/django/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py collectstatic --noinput 4 | python manage.py compilemessages 5 | python manage.py migrate 6 | python manage.py setup_periodic_tasks 7 | gunicorn config.asgi:application -w 2 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --chdir=/app -------------------------------------------------------------------------------- /frontend/build/img/error-warning-fill.027f8c93.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/build/img/information-fill.dfcc3b8f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/notify/context_processors.py: -------------------------------------------------------------------------------- 1 | def notification_count(request): 2 | user = request.user 3 | 4 | if user.is_anonymous: 5 | unread_count = None 6 | else: 7 | unread_count = user.notifications.unread().count() 8 | 9 | return {"unread_count": unread_count} 10 | -------------------------------------------------------------------------------- /devops/ansible/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Nginx is installed 3 | ansible.builtin.package: 4 | name: nginx 5 | state: present 6 | 7 | - name: Remove default conf 8 | file: 9 | path: /etc/nginx/sites-enabled/default 10 | state: absent 11 | notify: Restart nginx -------------------------------------------------------------------------------- /frontend/src/images/alert-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/style/_pagination.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .pagination { 4 | list-style: none; 5 | padding-left: 0; 6 | 7 | .pagination-item { 8 | display: inline-block; 9 | padding: 0 $width-gap-half; 10 | } 11 | 12 | .active a { 13 | color: $color-text; 14 | } 15 | } -------------------------------------------------------------------------------- /blogproject/scripts/fake/_post_categories.py: -------------------------------------------------------------------------------- 1 | from blog.tests.factories import CategoryFactory 2 | from users.models import User 3 | 4 | 5 | def run(): 6 | admin_user = User.objects.get(username="admin") 7 | CategoryFactory.create_batch(10, creator=admin_user) 8 | print("Post categories created.") 9 | -------------------------------------------------------------------------------- /frontend/build/img/alert-fill.bbcee1b1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/favorites/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "favorites" 6 | urlpatterns = [ 7 | path("issues/", views.IssueListView.as_view(), name="issue_list"), 8 | path("issues//", views.IssueDetailView.as_view(), name="issue_detail"), 9 | ] 10 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_ad.html: -------------------------------------------------------------------------------- 1 |
2 |

推广

3 |
4 |
5 | 成为赞助商 6 |
7 |
8 |
-------------------------------------------------------------------------------- /blogproject/webtools/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "webtools" 6 | urlpatterns = [ 7 | path( 8 | "django-secret-key-creator", 9 | views.DjangoSecretKeyCreateView.as_view(), 10 | name="create_django_secret_key", 11 | ), 12 | ] 13 | -------------------------------------------------------------------------------- /blogproject/core/tests/models.py: -------------------------------------------------------------------------------- 1 | from core.abstracts import AbstractEntry 2 | from django.db import models 3 | 4 | 5 | class Entry(AbstractEntry): 6 | pass 7 | 8 | 9 | class RankableEntry(AbstractEntry): 10 | rank = models.SmallIntegerField(unique=True) 11 | 12 | class Meta: 13 | ordering = ["rank"] 14 | -------------------------------------------------------------------------------- /compose/production/nginx/includes/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | proxy_set_header Upgrade $http_upgrade; #支持wss 6 | proxy_set_header Connection "upgrade"; #支持wss 7 | -------------------------------------------------------------------------------- /frontend/src/style/_util.scss: -------------------------------------------------------------------------------- 1 | // with and height 2 | .w-100 { 3 | width: 100%; 4 | } 5 | 6 | .h-100 { 7 | height: 100%; 8 | } 9 | 10 | // margin 11 | .ml-auto { 12 | margin-left: auto !important; 13 | } 14 | 15 | .mr-auto { 16 | margin-right: auto !important; 17 | } 18 | 19 | .show { 20 | display: block; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: repo.yml 3 | - import_tasks: deps.yml 4 | - import_tasks: envfile.yml 5 | - import_tasks: db.yml 6 | - import_tasks: management.yml 7 | - import_tasks: supervisor.yml 8 | become: yes 9 | become_method: sudo 10 | - import_tasks: nginx.yml 11 | become: yes 12 | become_method: sudo -------------------------------------------------------------------------------- /frontend/manifest-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "entrypoints": { 3 | "main": { 4 | "assets": { 5 | "css": [ 6 | "/static/css/main.css" 7 | ], 8 | "js": [ 9 | "/static/js/main.js" 10 | ] 11 | } 12 | } 13 | }, 14 | "main.css": "/static/css/main.css", 15 | "main.js": "/static/js/main.js" 16 | } -------------------------------------------------------------------------------- /frontend/src/style/_login.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .login-panel { 4 | a:hover { 5 | text-decoration: none; 6 | } 7 | 8 | .social-icons { 9 | margin-left: $width-gap-half; 10 | } 11 | 12 | .social-icons-weibo { 13 | color: #ff763b; 14 | } 15 | 16 | .social-icons-github { 17 | color: #2680d9; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blogproject/tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Tag, TaggedItem 4 | 5 | 6 | @admin.register(Tag) 7 | class TagAdmin(admin.ModelAdmin): 8 | list_display = ("id", "name", "slug", "created_at", "modified_at") 9 | 10 | 11 | @admin.register(TaggedItem) 12 | class TaggedItemAdmin(admin.ModelAdmin): 13 | pass 14 | -------------------------------------------------------------------------------- /devops/ansible/hosts.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | TencentCloud-CD: &TencentCloud-CD 4 | ansible_host: 118.24.109.106 5 | 6 | asz: &asz 7 | ansible_host: 47.106.168.32 8 | 9 | children: 10 | staging: 11 | hosts: 12 | TencentCloud-CD: *TencentCloud-CD 13 | 14 | production: 15 | hosts: 16 | asz: *asz 17 | -------------------------------------------------------------------------------- /devops/ansible/roles/poetry/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Which python3 3 | ansible.builtin.shell: 4 | cmd: ". ~/.profile && which python3" 5 | register: which_python_result 6 | 7 | - name: Install 8 | ansible.builtin.shell: 9 | cmd: ". ~/.profile && pipx install poetry --python {{ which_python_result.stdout }} --index-url https://pypi.doubanio.com/simple/" -------------------------------------------------------------------------------- /devops/ansible/roles/project/templates/nginx/proxy.conf.j2: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | proxy_set_header Upgrade $http_upgrade; # for websocket 6 | proxy_set_header Connection "upgrade"; # for websocket 7 | -------------------------------------------------------------------------------- /blogproject/friendlinks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import FriendLink 4 | 5 | 6 | @admin.register(FriendLink) 7 | class FriendLinkAdmin(admin.ModelAdmin): 8 | list_display = [ 9 | "id", 10 | "rank", 11 | "site_name", 12 | "site_link", 13 | "created_at", 14 | "modified_at", 15 | ] 16 | -------------------------------------------------------------------------------- /blogproject/notify/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "notify" 6 | urlpatterns = [ 7 | path("", views.AllNotificationsListView.as_view(), name="notification_all"), 8 | path( 9 | "unread/", 10 | views.UnreadNotificationsListView.as_view(), 11 | name="notification_unread", 12 | ), 13 | ] 14 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_favorites.py: -------------------------------------------------------------------------------- 1 | from favorites.models import Issue 2 | from favorites.tests.factories import FavoriteFactory 3 | from tags.tests.factories import TagFactory 4 | 5 | 6 | def run(): 7 | for issue in Issue.objects.all(): 8 | FavoriteFactory.create_batch(5, issue=issue, tags=[TagFactory(), TagFactory()]) 9 | print("Favorites created!") 10 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_sidebar_mobile.html: -------------------------------------------------------------------------------- 1 | {% extends 'sidebar_mobile_base.html' %} 2 | 3 | {% block sidebar_header %} 4 | 目录 5 | {% endblock sidebar_header %} 6 | 7 | {% block sidebar_body %} 8 |
9 | {% include 'blog/inclusions/_toc_content.html' %} 10 |
11 | {% endblock sidebar_body %} -------------------------------------------------------------------------------- /devops/ansible/roles/project/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: Restart program 2 | community.general.supervisorctl: 3 | config: /etc/supervisor/supervisord.conf 4 | name: "{{ item }}" 5 | state: restarted 6 | loop: 7 | - django-blog-project-celery-worker 8 | - django-blog-project-celery-beat 9 | - django-blog-project 10 | become: yes 11 | become_method: sudo -------------------------------------------------------------------------------- /frontend/src/style/_aside.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .blog-aside-desk-wrapper { 4 | position: fixed; 5 | width: 100%; 6 | height: 100%; 7 | top: 0; 8 | left: 0; 9 | padding-top: $size-base * 5; // 一定要使用 padding,否则底部部分内容无法显示 10 | } 11 | 12 | 13 | .blog-post-wrapper { 14 | height: 100%; 15 | } 16 | 17 | .blog-post-wrapper main { 18 | z-index: 1; 19 | } 20 | -------------------------------------------------------------------------------- /blogproject/alerts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.contenttypes.admin import GenericTabularInline 3 | 4 | from .models import Alert 5 | 6 | 7 | class AlertInline(GenericTabularInline): 8 | model = Alert 9 | 10 | 11 | @admin.register(Alert) 12 | class AlertAdmin(admin.ModelAdmin): 13 | list_display = ["id", "created_at", "modified_at"] 14 | -------------------------------------------------------------------------------- /compose/production/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.1 2 | 3 | RUN apt-get update && apt-get install -y --allow-unauthenticated certbot python-certbot-nginx 4 | 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | 7 | COPY ./compose/production/nginx/conf.d/*.conf /etc/nginx/conf.d/ 8 | # Proxy configurations 9 | COPY ./compose/production/nginx/includes/ /etc/nginx/includes/ 10 | 11 | 12 | -------------------------------------------------------------------------------- /blogproject/templates/notifications/comment.html: -------------------------------------------------------------------------------- 1 | {% extends 'notifications/base.html' %} 2 | {% block header %} 3 |
4 | {{ actor.name }} 5 | 评论了文章 6 | {{ target.content_object.title }} 7 |
8 | {% endblock header %} -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_toc.html: -------------------------------------------------------------------------------- 1 | {% if post.toc %} 2 |
3 |

目录

4 |
5 |
6 | {% include 'blog/inclusions/_toc_content.html' %} 7 |
8 |
9 |
10 | {% endif %} -------------------------------------------------------------------------------- /blogproject/templates/notifications/inclusions/_comment.html: -------------------------------------------------------------------------------- 1 | {% extends 'notifications/base.html' %} 2 | {% block header %} 3 |
4 | {{ actor.name }} 5 | 评论了文章 6 | {{ target.content_object.title }} 7 |
8 | {% endblock header %} -------------------------------------------------------------------------------- /blogproject/templates/notifications/reply.html: -------------------------------------------------------------------------------- 1 | {% extends 'notifications/base.html' %} 2 | {% block header %} 3 |
4 | {{ actor.name }} 5 | 在文章 6 | {{ target.content_object.title }} 8 | 中回复了你 9 |
10 | {% endblock header %} -------------------------------------------------------------------------------- /devops/ansible/roles/project/templates/supervisor/blogproject-celery-worker.conf.j2: -------------------------------------------------------------------------------- 1 | [program:django-blog-project-celery-worker] 2 | directory=/home/alice/apps/django-blog-project 3 | command=/home/alice/.local/bin/poetry run celery -A blogproject.taskapp worker -l INFO 4 | autostart=true 5 | startsecs=1 6 | startretries=3 7 | autorestart=unexpected 8 | stopasgroup=true 9 | killasgroup=true 10 | user=alice -------------------------------------------------------------------------------- /frontend/src/style/_literal.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .highlight pre { 4 | margin-top: 0; 5 | } 6 | 7 | .literal-block { 8 | .code-block-caption { 9 | background-color: $color-info-faded; 10 | padding: $width-gap-half $width-gap; 11 | font-size: $font-size-small; 12 | } 13 | 14 | .highlight pre { 15 | .lineno { 16 | margin-right: 0.3rem; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /blogproject/blog/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | 3 | from .models import Post 4 | 5 | 6 | class PostIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.CharField(document=True, use_template=True) 8 | 9 | def get_model(self): 10 | return Post 11 | 12 | def index_queryset(self, using=None): 13 | return self.get_model().objects.searchable() 14 | -------------------------------------------------------------------------------- /blogproject/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | from model_utils.fields import AutoCreatedField, AutoLastModifiedField 4 | 5 | 6 | class TimeStampedModel(models.Model): 7 | created_at = AutoCreatedField(_("created at")) 8 | modified_at = AutoLastModifiedField(_("modified at")) 9 | 10 | class Meta: 11 | abstract = True 12 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_courses.py: -------------------------------------------------------------------------------- 1 | from courses.models import Category 2 | from courses.tests.factories import CourseFactory 3 | from users.models import User 4 | 5 | 6 | def run(): 7 | categories = Category.objects.all() 8 | admin_user = User.objects.get(username="admin") 9 | for cate in categories: 10 | CourseFactory(category=cate, creator=admin_user) 11 | print("Courses created.") 12 | -------------------------------------------------------------------------------- /blogproject/templates/notifications/inclusions/_reply.html: -------------------------------------------------------------------------------- 1 | {% extends 'notifications/base.html' %} 2 | {% block header %} 3 |
4 | {{ actor.name }} 5 | 在文章 6 | {{ target.content_object.title }} 8 | 中回复了你 9 |
10 | {% endblock header %} -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/style/_alert.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .blog-alert { 4 | padding: $width-gap-half $width-gap; 5 | border-radius: $width-border-radius; 6 | } 7 | 8 | .blog-alert-warning { 9 | background-color: $color-warning-faded; 10 | } 11 | 12 | .blog-alert-danger { 13 | background-color: $color-danger-faded; 14 | } 15 | 16 | .blog-alert-success { 17 | background-color: $color-success-faded; 18 | } -------------------------------------------------------------------------------- /blogproject/courses/search_indexes.py: -------------------------------------------------------------------------------- 1 | from courses.models import Material 2 | from haystack import indexes 3 | 4 | 5 | class MaterialIndex(indexes.SearchIndex, indexes.Indexable): 6 | text = indexes.CharField(document=True, use_template=True) 7 | 8 | def get_model(self): 9 | return Material 10 | 11 | def index_queryset(self, using=None): 12 | return self.get_model().objects.searchable() 13 | -------------------------------------------------------------------------------- /blogproject/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load blog_extras comments %} 3 | {% block title %}登录{% endblock title %} 4 | {% block main %} 5 |
6 |
7 |

使用第三方账户账户登录

8 | {% include 'account/inclusions/_login.html' %} 9 |
10 |
11 | {% endblock main %} -------------------------------------------------------------------------------- /frontend/src/style/_header.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .blog-header { 4 | width: 100%; 5 | position: fixed; 6 | z-index: 10; 7 | border-bottom: $width-border solid $color-border; 8 | background-color: $color-background; 9 | height: $size-base * 5; 10 | 11 | .blog-header-container { 12 | padding: 0 $width-gap; 13 | } 14 | } 15 | 16 | .blog-header-placeholder { 17 | height: $size-base * 5; 18 | } -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_description.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ course.title }}

3 | {% if course.description %} 4 |
5 | {{ course.rich_content.content|safe }} 6 |
7 | {% else %} 8 |
9 | 立即学习 >> 10 |
11 | {% endif %} 12 |
-------------------------------------------------------------------------------- /blogproject/templates/inclusions/_simple_pagination.html: -------------------------------------------------------------------------------- 1 |
2 | {% if page_obj.has_previous %} 3 | 上一页 5 | {% endif %} 6 | {% if page_obj.has_next %} 7 | 下一页 9 | {% endif %} 10 |
11 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensupre project directory 3 | ansible.builtin.file: 4 | path: "{{ project_path }}" 5 | state: directory 6 | 7 | - name: Checkout repository 8 | ansible.builtin.git: 9 | repo: git@github.com:jukanntenn/django-blog-project.git 10 | dest: "{{ project_path }}" 11 | version: master 12 | accept_hostkey: yes 13 | notify: 14 | Restart program -------------------------------------------------------------------------------- /blogproject/newsletters/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from . import views 4 | 5 | app_name = "newsletters" 6 | urlpatterns = [ 7 | path("subscription/", views.SubscriptionCreateView.as_view(), name="subscription"), 8 | re_path( 9 | r"^subscription/confirm/(?P[-:\w]+)/$", 10 | views.SubscriptionConfirmView.as_view(), 11 | name="subscription-confirm", 12 | ), 13 | ] 14 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /devops/ansible/roles/pipx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Which pip excuteable 3 | ansible.builtin.shell: 4 | cmd: ". ~/.profile && which pip3" 5 | register: which_pip_result 6 | 7 | - name: Install 8 | ansible.builtin.pip: 9 | executable: "{{ which_pip_result.stdout }}" 10 | name: pipx 11 | state: present 12 | 13 | - name: Ensurepath 14 | ansible.builtin.shell: 15 | cmd: ". ~/.profile && pipx ensurepath" -------------------------------------------------------------------------------- /blogproject/templates/sidebar_mobile_base.html: -------------------------------------------------------------------------------- 1 |
5 | 8 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /blogproject/notify/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from factory.django import DjangoModelFactory 3 | from notifications.models import Notification 4 | from users.tests.factories import UserFactory 5 | 6 | 7 | class NotificationFactory(DjangoModelFactory): 8 | class Meta: 9 | model = Notification 10 | 11 | recipient = factory.SubFactory(UserFactory) 12 | actor = factory.SubFactory(UserFactory) 13 | verb = "notify" 14 | -------------------------------------------------------------------------------- /blogproject/blog/migrations/0005_remove_post_tags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-20 08:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0004_auto_20200306_1047'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='post', 15 | name='tags', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /blogproject/templates/account/inclusions/_login.html: -------------------------------------------------------------------------------- 1 | {% load socialaccount %} 2 | 3 | -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_sidebar_mobile.html: -------------------------------------------------------------------------------- 1 | {% extends 'sidebar_mobile_base.html' %} 2 | {% load courses_extras %} 3 | 4 | {% block sidebar_header %} 5 | {{ course.title }} 6 | {% endblock sidebar_header %} 7 | 8 | {% block sidebar_body %} 9 |
10 | {% show_course_toc course current %} 11 |
12 | {% endblock sidebar_body %} -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend1 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_posts.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from blog.models import Category 4 | from blog.tests.factories import PostFactory 5 | from users.models import User 6 | 7 | 8 | def run(): 9 | admin_user = User.objects.get(username="admin") 10 | for cate in Category.objects.all(): 11 | size = random.randint(10, 20) 12 | PostFactory.create_batch(size, author=admin_user, category=cate) 13 | print("Posts created.") 14 | -------------------------------------------------------------------------------- /blogproject/courses/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "courses" 6 | urlpatterns = [ 7 | path("/", views.CourseDetailView.as_view(), name="course_detail"), 8 | path( 9 | "/materials//", 10 | views.MaterialDetailView.as_view(), 11 | name="material_detail", 12 | ), 13 | path("", views.CourseListView.as_view(), name="course_list"), 14 | ] 15 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | working_dir="$(dirname ${0})" 10 | source "${working_dir}/_sourced/constants.sh" 11 | source "${working_dir}/_sourced/messages.sh" 12 | 13 | 14 | message_welcome "These are the backups you have got:" 15 | 16 | ls -lht "${BACKUP_DIR_PATH}" 17 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/templates/supervisor/blogproject-celery-beat.conf.j2: -------------------------------------------------------------------------------- 1 | [program:django-blog-project-celery-beat] 2 | directory=/home/alice/apps/django-blog-project 3 | command=/home/alice/.local/bin/poetry run celery -A blogproject.taskapp beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler 4 | autostart=true 5 | startsecs=1 6 | startretries=3 7 | autorestart=unexpected 8 | stopasgroup=true 9 | killasgroup=true 10 | user=alice -------------------------------------------------------------------------------- /devops/ansible/roles/project/templates/supervisor/blogproject.conf.j2: -------------------------------------------------------------------------------- 1 | [program:django-blog-project] 2 | directory=/home/alice/apps/django-blog-project 3 | command=/home/alice/.local/bin/poetry run gunicorn config.asgi:application -w 2 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 --chdir=/home/alice/apps/django-blog-project 4 | autostart=true 5 | startsecs=1 6 | startretries=3 7 | autorestart=unexpected 8 | stopasgroup=true 9 | killasgroup=true 10 | user=alice 11 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_materials.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from courses.models import Course 4 | from courses.tests.factories import MaterialFactory 5 | from users.models import User 6 | 7 | 8 | def run(): 9 | admin_user = User.objects.get(username="admin") 10 | for course in Course.objects.all(): 11 | size = random.randint(10, 20) 12 | MaterialFactory.create_batch(size, author=admin_user, course=course) 13 | print("Materials created.") 14 | -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_prev_next.html: -------------------------------------------------------------------------------- 1 |
2 | {% if prev %} 3 | 4 | 5 | {{ prev.title }} 6 | 7 | {% endif %} 8 | {% if next %} 9 | 10 | {{ next.title }} 11 | 12 | 13 | {% endif %} 14 |
-------------------------------------------------------------------------------- /blogproject/users/management/commands/create_tokens.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from rest_framework.authtoken.models import Token 3 | from users.models import User 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Create DRF Token for all users" 8 | 9 | def handle(self, *args, **options): 10 | for user in User.objects.all(): 11 | Token.objects.get_or_create(user=user) 12 | self.stdout.write("Done!") 13 | -------------------------------------------------------------------------------- /compose/production/nginx/DockerfileMainland: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.1 2 | 3 | RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list 4 | RUN apt-get update && apt-get install -y --allow-unauthenticated certbot python-certbot-nginx 5 | 6 | RUN rm /etc/nginx/conf.d/default.conf 7 | 8 | COPY ./compose/production/nginx/conf.d/*.conf /etc/nginx/conf.d/ 9 | # Proxy configurations 10 | COPY ./compose/production/nginx/includes/ /etc/nginx/includes/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@vue/cli-plugin-babel/preset', 5 | { 6 | // false:不在代码中使用polyfills,表现形式和@babel/preset-latest一样,当使用ES6+语法及API时,在不支持的环境下会报错。 7 | // 'usage':在文件需要的位置单独按需引入,可以保证在每个bundler中只引入一份 8 | // 'entry': 在入口处引入,一般 entry 打包后体积会比 usage 大 9 | useBuiltIns: false, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /blogproject/taskapp/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from django.core.management import call_command 3 | 4 | 5 | # https://django-dbbackup.readthedocs.io/en/master/commands.html#dbbackup 6 | @shared_task 7 | def dbbackup(): 8 | return call_command("dbbackup", "--clean") 9 | 10 | 11 | # https://django-dbbackup.readthedocs.io/en/master/commands.html#mediabackup 12 | @shared_task 13 | def mediabackup(): 14 | return call_command("mediabackup", "--clean") 15 | -------------------------------------------------------------------------------- /blogproject/templates/comments/inclusions/_comments_app.html: -------------------------------------------------------------------------------- 1 | {% load socialaccount %} 2 | 3 | 13 | 14 |
-------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django31 project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blogproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /frontend/src/images/lightbulb-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/build/img/lightbulb-line.a44c3828.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/newsletters/tests/test_view.py: -------------------------------------------------------------------------------- 1 | from django.core import mail 2 | from test_plus.test import TestCase 3 | 4 | 5 | class SubscriptionCreateViewTestCase(TestCase): 6 | def setUp(self) -> None: 7 | pass 8 | 9 | def test_subscribe(self): 10 | response = self.post( 11 | "newsletters:subscription", data={"email": "test@example.com"}, follow=True 12 | ) 13 | self.response_200(response) 14 | self.assertEqual(len(mail.outbox), 1) 15 | -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_sidebar_desk.html: -------------------------------------------------------------------------------- 1 | {% load courses_extras %} 2 | 3 |
4 |
5 |
6 |
7 |
{{ course.title }}
8 | {% show_course_toc course material %} 9 |
10 |
11 |
12 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django blog project 2 | 3 | ![django blog screenshot](./screenshot.png) 4 | 5 | ## 相关资源 6 | 7 | [在线预览](https://www.zmrenwu.com/) 8 | 9 | ## 特性一览 10 | 11 | - 简约优雅的 UI,移动端优先的响应式设计。 12 | - Webpack 前端资源打包。 13 | - 基于 Vue 的多级评论系统。 14 | - 文章、评论内容支持 Markdown 与代码高亮。 15 | - 支持 GitHub、新浪微博社交账户登录。 16 | - 中文全文搜索,关键词高亮。 17 | - 完善的通知系统,评论、回复博客内通知,同时邮件提醒。 18 | - 独有的教程系统,方便地管理系列文章。 19 | - Docker 部署,无痛上线。 20 | 21 | ## 声明 22 | 本项目作为开发者的个人博客系统,正在快速地开发和迭代,会引入很多不兼容的改变。目前仅建议将此项目用于学习目的,如需用于生产环境,请等待稳定版 v1.0.0 的发布。 23 | -------------------------------------------------------------------------------- /blogproject/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}{{ headline }}_{{ config.LOGO }}{% endblock title %} 3 | {% block main %} 4 |
5 | {% for entry in entry_list %} 6 |
7 | {% include 'blog/inclusions/_entry_list_item.html' %} 8 |
9 | {% empty %} 10 |

暂无文章

11 | {% endfor %} 12 | {% if is_paginated %} 13 | {{ page_obj.render }} 14 | {% endif %} 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_course_list_item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 | {{ course.title }} 10 |
11 |
12 | {{ course.level }} 13 | {{ course.views }} 14 |
15 |
16 | {{ course.brief }} 17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /frontend/src/style/_backtop.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .back-top { 4 | opacity: 0; 5 | position: fixed; 6 | bottom: $width-gap-double; 7 | right: $width-gap; 8 | font-size: $font-size-h3; 9 | color: $color-text; 10 | z-index: 100; 11 | cursor: pointer; 12 | transition: opacity 0.5s ease-in; 13 | } 14 | 15 | .back-top.fade-in { 16 | opacity: 1; 17 | } 18 | 19 | @media (max-width: 768px) { 20 | .back-top { 21 | right: $width-gap; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /blogproject/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class BlogConfig(AppConfig): 6 | name = "blog" 7 | verbose_name = _("blog") 8 | 9 | def ready(self): 10 | from comments.moderation import BlogCommentModerator, moderator 11 | from courses.models import Material 12 | 13 | moderator.register(self.get_model("Post"), BlogCommentModerator) 14 | moderator.register(Material, BlogCommentModerator) 15 | -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_material_meta.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_friend_link.html: -------------------------------------------------------------------------------- 1 | {% if friend_link_list %} 2 | 14 | {% endif %} -------------------------------------------------------------------------------- /blogproject/comments/migrations/0003_auto_20200920_1835.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-20 10:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('comments', '0002_blogcomment_user'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='blogcomment', 15 | name='object_pk', 16 | field=models.IntegerField(verbose_name='object ID'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /blogproject/templates/blog/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main %} 3 |
4 |
5 | 11 |
12 |
13 | {% endblock main %} -------------------------------------------------------------------------------- /blogproject/users/migrations/0002_auto_20200919_1157.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-19 03:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='first_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /docs/category.md: -------------------------------------------------------------------------------- 1 | 类似于发布博客文章,点击 **博客** 板块下的 **分类** 可进入到已有分类列表。 2 | 3 | 在分类列表下,点击右上角的 **增加分类** 进入到新增分类页面: 4 | 5 | ![](img/add_category.png) 6 | 7 | 重要字段含义说明如下: 8 | 9 | **名称**:分类名 10 | 11 | **标题**:同名称 12 | 13 | slug:URL 中的 slug,可看作该分类的一个标记,且这个标记会显示在 URL 中以增强可读性,其格式一般是英文单词,使用 - 符号分隔。 14 | 15 | 例如有一个分类叫做 Django ORM 高级操作,那可以将它的 slug 设置为 django-advanced-orm,则这个分类对应的 URL 就是 /category/django-advanced-orm/,访问这个 URL 将看到该分类下的文章列表。 16 | 17 | 通过博客导航条中的分类导航可进入分类列表页面: 18 | 19 | ![](img/blog_category_nav.png) 20 | 21 | ![](img/blog_category_list.png) 22 | 23 | -------------------------------------------------------------------------------- /blogproject/favorites/migrations/0002_auto_20200920_1601.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-20 08:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('favorites', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='favorite', 15 | name='tags', 16 | ), 17 | migrations.RemoveField( 18 | model_name='issue', 19 | name='tags', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /blogproject/templates/comments/email/reply.html: -------------------------------------------------------------------------------- 1 | {% extends 'comments/email/base.html' %} 2 | {% block content %} 3 |

4 | {{ comment.user.name }} 在 {{ content_object.title }} 中回复了你: 5 |

6 |

{{ comment.comment_html | safe }}

7 |

点击查看

8 |

如果点击无效,请复制以下链接到浏览器打开:
{{ link }}

9 |
10 |

11 | 发自:{{ site.name }} 12 |

13 | {% endblock content %} -------------------------------------------------------------------------------- /blogproject/templates/blog/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load blog_extras %} 3 | 4 | {% block header %} 5 | {% with show_trigger=True %} 6 | {{ block.super }} 7 | {% endwith %} 8 | {% endblock header %} 9 | 10 | {% block main %} 11 | {% include 'blog/inclusions/_detail.html' %} 12 | {% endblock main %} 13 | 14 | {% block side %} 15 | {% include 'blog/inclusions/_toc.html' %} 16 | {{ block.super }} 17 | {% endblock side %} 18 | 19 | {% block sidebar_mobile %} 20 | {% include 'blog/inclusions/_sidebar_mobile.html' %} 21 | {% endblock sidebar_mobile %} -------------------------------------------------------------------------------- /blogproject/templates/comments/email/comment.html: -------------------------------------------------------------------------------- 1 | {% extends 'comments/email/base.html' %} 2 | {% block content %} 3 |

4 | {{ comment.user.name }} 在 {{ content_object.title }} 中发布了评论: 5 |

6 |

{{ comment.comment_html | safe }}

7 |

点击查看

8 |

如果点击无效,请复制以下链接到浏览器打开:
{{ link }}

9 |
10 |

11 | 发自:{{ site.name }} 12 |

13 | {% endblock content %} -------------------------------------------------------------------------------- /blogproject/webtools/views.py: -------------------------------------------------------------------------------- 1 | from braces.views import SetHeadlineMixin 2 | from django.views.generic import FormView 3 | 4 | from .forms import DjangoSecretKeyCreateForm 5 | 6 | 7 | class DjangoSecretKeyCreateView(SetHeadlineMixin, FormView): 8 | headline = "Django Secret Key 在线生成器" 9 | form_class = DjangoSecretKeyCreateForm 10 | template_name = "webtools/django_secret_key.html" 11 | 12 | def form_valid(self, form): 13 | return self.render_to_response( 14 | context={"form": form, "django_secret_key": form.create_secret_key()} 15 | ) 16 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_related.html: -------------------------------------------------------------------------------- 1 | {% load more_like_this %} 2 | 3 |
4 |
相关文章
5 |
6 | {% more_like_this obj as related_objects limit 5 %} 7 |
    8 | {% for related_obj in related_objects %} 9 |
  • 10 | {{ related_obj.object.title }} 11 |
  • 12 | {% empty %} 13 | 没有相关文章 14 | {% endfor %} 15 |
16 |
17 |
-------------------------------------------------------------------------------- /devops/ansible/roles/project/templates/nginx/blogproject.conf.j2: -------------------------------------------------------------------------------- 1 | upstream blogproject { 2 | server 127.0.0.1:8000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name www.zmrenwu.com zmrenwu.com; 8 | charset utf-8; 9 | client_max_body_size 10M; 10 | location /static { 11 | alias /var/www/django-blog-project/static; 12 | } 13 | location /media { 14 | alias /var/www/django-blog-project/blogproject/media; 15 | } 16 | location / { 17 | include /etc/nginx/includes/proxy.conf; 18 | proxy_pass http://blogproject; 19 | } 20 | } -------------------------------------------------------------------------------- /blogproject/alerts/migrations/0002_alert_scopes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 06:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ("alerts", "0001_initial"), 12 | ("courses", "0001_initial"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="alert", 18 | name="scopes", 19 | field=models.ManyToManyField(to="courses.Course", verbose_name="scopes"), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/management.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure static and media directory 3 | ansible.builtin.file: 4 | path: /var/www/django-blog-project/blogproject 5 | state: directory 6 | become: yes 7 | become_method: sudo 8 | 9 | - name: Run django management commands 10 | ansible.builtin.shell: 11 | cmd: ". ~/.profile && poetry run python manage.py {{ item }} --settings=config.settings.production" 12 | chdir: ~/apps/django-blog-project/ 13 | loop: 14 | - collectstatic --noinput 15 | - compilemessages 16 | - migrate 17 | - setup_periodic_tasks 18 | -------------------------------------------------------------------------------- /frontend/build/index.html: -------------------------------------------------------------------------------- 1 | django-blog-project-frontend
-------------------------------------------------------------------------------- /blogproject/blog/migrations/0003_post_meta_ordering.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-09 16:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("blog", "0002_auto_20190918_1409"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="post", 15 | options={ 16 | "ordering": ["-pub_date", "-created"], 17 | "verbose_name": "Posts", 18 | "verbose_name_plural": "Posts", 19 | }, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/supervisor.yml: -------------------------------------------------------------------------------- 1 | - name: Copy program conf 2 | template: 3 | src: "supervisor/{{ item }}.j2" 4 | dest: "/etc/supervisor/conf.d/{{ item }}" 5 | loop: 6 | - blogproject.conf 7 | - blogproject-celery-beat.conf 8 | - blogproject-celery-worker.conf 9 | notify: Restart program 10 | 11 | - name: Add program 12 | community.general.supervisorctl: 13 | config: /etc/supervisor/supervisord.conf 14 | name: "{{ item }}" 15 | state: present 16 | loop: 17 | - django-blog-project-celery-worker 18 | - django-blog-project-celery-beat 19 | - django-blog-project -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | 可以通过后台对博客系统进行个性化定制。 2 | 3 | 点击 **配置**,进入到博客个性化定制页面: 4 | 5 | ![博客个性化配置](img/configuration.png) 6 | 7 | 目前支持的个性化配置有: 8 | 9 | **LOGO** 10 | 11 | 默认值:追梦人物的博客 12 | 13 | 这个配置项用于配置博客导航条显示的 LOGO 文字,下图中红色方框标出的部分。 14 | 15 | ![logo配置](img/logo.png) 16 | 17 | **COMMENT_EMAIL_SUBJECT** 18 | 19 | 当用户在博客发表评论后,管理员会收到新评论的邮件提醒。 20 | 21 | 这个配置项用于配置新评论提醒的邮件标题。 22 | 23 | **REPLY_EMAIL_SUBJECT** 24 | 25 | 博客支持多级评论。当用户在博客中**回复他人评论后**,除了管理员会收到新评论的邮件提醒外,被回复者也会收到邮件提醒。 26 | 27 | 这个配置项用于配置被回复提醒的邮件标题。 28 | 29 | **BAIDU_SCRIPT** 30 | 31 | 百度统计和百度站长工具的 JavaScript 脚本,主要用于搜索引擎优化。不需要的话可以不用配置。 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /blogproject/friendlinks/models.py: -------------------------------------------------------------------------------- 1 | from core.models import TimeStampedModel 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class FriendLink(TimeStampedModel): 7 | site_name = models.CharField(_("site name"), max_length=100) 8 | site_link = models.URLField(_("site link")) 9 | rank = models.IntegerField(_("rank"), default=0) 10 | 11 | class Meta: 12 | verbose_name = _("friend link") 13 | verbose_name_plural = _("friend links") 14 | ordering = ["rank", "created_at"] 15 | 16 | def __str__(self): 17 | return self.site_name 18 | -------------------------------------------------------------------------------- /frontend/src/style/_hilite.scss: -------------------------------------------------------------------------------- 1 | .codehilite { 2 | padding: 0; 3 | } 4 | 5 | /* for block of numbers */ 6 | .hljs-ln-numbers { 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | 14 | text-align: right; 15 | color: #ccc; 16 | border-right: 1px solid #CCC; 17 | vertical-align: top; 18 | padding-right: 5px; 19 | } 20 | 21 | .hljs-ln-n { 22 | width: 30px; 23 | } 24 | 25 | /* for block of code */ 26 | .hljs-ln .hljs-ln-code { 27 | padding-left: 10px; 28 | white-space: pre; 29 | } -------------------------------------------------------------------------------- /frontend/src/style/_widget.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .widget { 4 | a { 5 | text-decoration: none; 6 | } 7 | } 8 | 9 | .widget-title { 10 | margin-top: 0; 11 | font-weight: normal; 12 | } 13 | 14 | .widget-body { 15 | padding: 1rem 0; 16 | } 17 | 18 | .widget-friend-link { 19 | ul { 20 | list-style: none; 21 | padding: 0; 22 | margin: 0; 23 | } 24 | 25 | li { 26 | margin-top: 0.5rem; 27 | } 28 | } 29 | 30 | .widget-medium { 31 | dl { 32 | margin: 0; 33 | } 34 | 35 | dt:not(:nth-of-type(1)) { 36 | margin-top: 0.5rem; 37 | } 38 | 39 | dd { 40 | padding: 0.1rem 0; 41 | } 42 | } -------------------------------------------------------------------------------- /blogproject/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "blog" 6 | urlpatterns = [ 7 | path("", views.IndexView.as_view(), name="index"), 8 | path("post//", views.PostDetailView.as_view(), name="detail"), 9 | path("category//", views.CategoryView.as_view(), name="category"), 10 | path("categories/", views.CategoryListView.as_view(), name="categories"), 11 | path("archives/", views.PostArchivesView.as_view(), name="archives"), 12 | path("donate/", views.DonateView.as_view(), name="donate"), 13 | path("search/", views.BlogSearchView(), name="search"), 14 | ] 15 | -------------------------------------------------------------------------------- /blogproject/core/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from core.tests.models import Entry, RankableEntry 3 | from factory.django import DjangoModelFactory 4 | from users.tests.factories import UserFactory 5 | 6 | 7 | class EntryFactory(DjangoModelFactory): 8 | title = factory.Faker("sentence") 9 | body = factory.Faker("paragraph") 10 | brief = factory.Faker("sentence") 11 | author = factory.SubFactory(UserFactory) 12 | 13 | class Meta: 14 | model = Entry 15 | 16 | 17 | class RankableEntryFactory(EntryFactory): 18 | rank = factory.Sequence(lambda n: n) 19 | 20 | class Meta: 21 | model = RankableEntry 22 | -------------------------------------------------------------------------------- /blogproject/templates/inclusions/_footer.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 |
4 |
5 |
© 2018-2021 {{ config.LOGO }}
6 |
7 | 10 |
11 |
本网站由又拍云 logo提供 CDN 加速/云存储服务 13 |
14 |
15 |
-------------------------------------------------------------------------------- /blogproject/blog/migrations/0006_post_tags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-20 08:04 2 | 3 | from django.db import migrations 4 | import taggit.managers 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tags', '0001_initial'), 11 | ('blog', '0005_remove_post_tags'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='post', 17 | name='tags', 18 | field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /blogproject/favorites/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Favorite, Issue 4 | 5 | 6 | @admin.register(Issue) 7 | class IssueAdmin(admin.ModelAdmin): 8 | list_display = ["id", "number", "pub_date"] 9 | fields = [ 10 | "description", 11 | "pub_date", 12 | "number", 13 | ] 14 | filter_horizontal = [] 15 | 16 | def save_model(self, request, obj, form, change): 17 | obj.creator = request.user 18 | super().save_model(request, obj, form, change) 19 | 20 | 21 | @admin.register(Favorite) 22 | class FavoriteAdmin(admin.ModelAdmin): 23 | list_display = ["id", "rank", "title", "issue", "url"] 24 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/all.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | 3 | from . import ( 4 | _clean_db, 5 | _comments, 6 | _course_categories, 7 | _courses, 8 | _favorites, 9 | _issues, 10 | _materials, 11 | _post_categories, 12 | _posts, 13 | _superuser, 14 | ) 15 | 16 | 17 | def run(): 18 | with transaction.atomic(): 19 | _clean_db.run() 20 | _superuser.run() 21 | _post_categories.run() 22 | _posts.run() 23 | _course_categories.run() 24 | _courses.run() 25 | _materials.run() 26 | _comments.run() 27 | _issues.run() 28 | _favorites.run() 29 | print("Done!") 30 | -------------------------------------------------------------------------------- /blogproject/blog/migrations/0007_auto_20210411_1737.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-04-11 09:37 2 | 3 | from django.db import migrations 4 | import taggit_selectize.managers 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tags', '0001_initial'), 11 | ('blog', '0006_post_tags'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='post', 17 | name='tags', 18 | field=taggit_selectize.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django 博客用户使用手册 2 | site_description: Django 博客用户使用手册 3 | 4 | theme: 5 | name: 'readthedocs' 6 | language: 'zh' 7 | 8 | repo_name: zmrenwu/django-blog-project 9 | repo_url: https://github.com/zmrenwu/django-blog-project 10 | edit_uri: "" 11 | 12 | nav: 13 | - 博客后台概览: 'overview.md' 14 | - 个性化配置: 'configuration.md' 15 | - 发表博客文章: 'post.md' 16 | - 添加文章分类: 'category.md' 17 | - 标签: 'tag.md' 18 | - 友情链接: 'friendlink.md' 19 | - 教程系统: 'course.md' 20 | 21 | 22 | markdown_extensions: 23 | - markdown.extensions.codehilite: 24 | guess_lang: false 25 | - toc: 26 | baselevel: 2 27 | permalink: "#" 28 | - admonition 29 | - codehilite 30 | - extra 31 | -------------------------------------------------------------------------------- /blogproject/blog/feeds.py: -------------------------------------------------------------------------------- 1 | from constance import config 2 | from django.contrib.syndication.views import Feed 3 | from django.utils.feedgenerator import Atom1Feed 4 | 5 | from .models import Post 6 | 7 | 8 | class AllPostsRssFeed(Feed): 9 | title = config.LOGO 10 | link = "/" 11 | description = "{}最新文章".format(config.LOGO) 12 | 13 | def items(self): 14 | return Post.objects.all() 15 | 16 | def item_title(self, item): 17 | return "[%s] %s" % (item.category, item.title) 18 | 19 | def item_description(self, item): 20 | return item.body 21 | 22 | 23 | class AllPostsAtomFeed(AllPostsRssFeed): 24 | feed_type = Atom1Feed 25 | subtitle = AllPostsRssFeed.description 26 | -------------------------------------------------------------------------------- /blogproject/blog/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from blog.utils import Highlighter 2 | 3 | 4 | def test_highlight(): 5 | document = "这是一个比较长的标题,用于测试关键词高亮但不被截断。" 6 | highlighter = Highlighter("长标题") 7 | 8 | expected = ( 9 | '这是一个比较' 10 | '的标题,' 11 | "用于测试关键词高亮但不被截断。" 12 | ) 13 | assert highlighter.highlight(document) == expected 14 | highlighter = Highlighter("关键词高亮") 15 | # Todo: 更合理的情况应该是长词(“关键词”)高亮 16 | expected = ( 17 | '这是一个比较长的标题,用于测试关键词' 18 | '高亮但不被截断。' 19 | ) 20 | assert highlighter.highlight(document) == expected 21 | -------------------------------------------------------------------------------- /devops/ansible/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Site deployment 3 | hosts: all 4 | remote_user: alice 5 | roles: 6 | # - role: pyenv 7 | # tags: "pyenv" 8 | # - role: pipx 9 | # tags: "pipx" 10 | # - role: poetry 11 | # tags: "poetry" 12 | - role: postgresql 13 | become: true 14 | become_method: sudo 15 | tags: "postgresql" 16 | - role: redis 17 | become: true 18 | become_method: sudo 19 | tags: "redis" 20 | - role: nginx 21 | become: true 22 | become_method: sudo 23 | tags: "nginx" 24 | - role: supervisor 25 | become: true 26 | become_method: sudo 27 | tags: "supervisor" 28 | - role: project 29 | tags: "project" -------------------------------------------------------------------------------- /frontend/src/style/_post.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .blog-post-list { 4 | a { 5 | text-decoration: none; 6 | } 7 | 8 | .blog-entry-title a { 9 | color: $color-text; 10 | } 11 | 12 | .meta-item { 13 | margin-right: $width-gap-half; 14 | } 15 | 16 | // more gap 17 | .bfc { 18 | display: inline-block; 19 | } 20 | } 21 | 22 | .blog-entry-meta { 23 | .meta-item { 24 | margin-right: $width-gap-half; 25 | text-decoration: none; 26 | } 27 | } 28 | 29 | 30 | .blog-badge { 31 | border: $width-border solid $color-border; 32 | border-radius: $width-border-radius; 33 | padding: .15rem .25rem; 34 | font-size: $size-base; 35 | color: $color-danger; 36 | line-height: 1; 37 | } -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure includes directory 3 | ansible.builtin.file: 4 | path: /etc/nginx/includes 5 | state: directory 6 | become: yes 7 | become_method: sudo 8 | 9 | - name: Copy proxy conf 10 | template: 11 | src: nginx/proxy.conf.j2 12 | dest: /etc/nginx/includes/proxy.conf 13 | notify: Restart nginx 14 | 15 | - name: Copy site conf 16 | template: 17 | src: nginx/blogproject.conf.j2 18 | dest: /etc/nginx/sites-available/blogproject.conf 19 | notify: Restart nginx 20 | 21 | - name: Enable new site 22 | file: 23 | src: /etc/nginx/sites-available/blogproject.conf 24 | dest: /etc/nginx/sites-enabled/blogproject.conf 25 | state: link 26 | notify: Restart nginx -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | // Must import the processed file from dist instead of src 2 | import 'mobi.css/dist/mobi.css'; 3 | import './style/colorful.css'; 4 | import './styles.scss'; 5 | 6 | import BackTop from './scripts/backtop'; 7 | import Offcanvas from '@/scripts/offcanvas'; 8 | import './scripts/search'; 9 | import './scripts/toc'; 10 | import Comment from './CommentApp.vue'; 11 | import { createApp } from 'vue'; 12 | 13 | // @ts-ignore 14 | const {contentType, objectPk, token,numComments, numCommentParticipants} = jscontext 15 | 16 | createApp(Comment, { 17 | contentType, 18 | objectPk, 19 | token, 20 | numComments, 21 | numCommentParticipants, 22 | }).mount('#comment_app'); 23 | 24 | 25 | export default { BackTop, Offcanvas }; 26 | -------------------------------------------------------------------------------- /blogproject/comments/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from comments.models import BlogComment 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.utils import timezone 5 | from factory.django import DjangoModelFactory 6 | from users.tests.factories import UserFactory 7 | 8 | 9 | class BlogCommentFactory(DjangoModelFactory): 10 | 11 | object_pk = factory.SelfAttribute("content_object.id") 12 | content_type = factory.LazyAttribute( 13 | lambda o: ContentType.objects.get_for_model(o.content_object) 14 | ) 15 | user = factory.SubFactory(UserFactory) 16 | comment = factory.Faker("sentence") 17 | submit_date = factory.LazyFunction(timezone.now) 18 | 19 | class Meta: 20 | model = BlogComment 21 | -------------------------------------------------------------------------------- /blogproject/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | import factory 4 | from django.utils.crypto import get_random_string 5 | from factory import Faker, post_generation 6 | from factory.django import DjangoModelFactory 7 | from users.models import User 8 | 9 | 10 | class UserFactory(DjangoModelFactory): 11 | username = Faker("user_name") 12 | name = factory.lazy_attribute(lambda o: o.username.lower()) 13 | email = Faker("email") 14 | 15 | @post_generation 16 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 17 | password = get_random_string() 18 | self.set_password(password) 19 | 20 | class Meta: 21 | model = User 22 | django_get_or_create = ("username",) 23 | -------------------------------------------------------------------------------- /frontend/src/style/_tabbed.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .tabbed-set { 4 | margin-top: $width-gap; 5 | display: flex; 6 | position: relative; 7 | flex-wrap: wrap; 8 | 9 | .tabbed-content { 10 | display: none; 11 | order: 99; 12 | width: 100%; 13 | text-align: left; 14 | } 15 | 16 | > input { 17 | display: none; 18 | 19 | &:checked + label { 20 | color: $color-text; 21 | } 22 | } 23 | 24 | > label { 25 | display: inline-block; 26 | padding: 4px 8px; 27 | font-weight: normal; 28 | font-size: $font-size-small; 29 | text-align: center; 30 | color: $color-text-muted; 31 | } 32 | } 33 | 34 | .tabbed-set input:nth-child(n+1):checked + label + .tabbed-content { 35 | display: block; 36 | } -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /compose/production/nginx/conf.d/blogproject.conf-tmpl: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server django:8000; 3 | } 4 | 5 | server { 6 | if ($host = xxx.com) { 7 | return 301 https://www.$host$request_uri; 8 | } 9 | server_name www.xxx.com xxx.com; 10 | 11 | location /static { 12 | alias /apps/blogproject/static; 13 | } 14 | 15 | location /media { 16 | alias /apps/blogproject/media; 17 | } 18 | 19 | location / { 20 | include /etc/nginx/includes/proxy.conf; 21 | proxy_pass http://django; 22 | } 23 | 24 | listen 80; 25 | } 26 | 27 | server { 28 | if ($host = xxx.com) { 29 | return 301 https://www.$host$request_uri; 30 | } 31 | server_name www.xxx.com xxx.com; 32 | listen 443; 33 | } -------------------------------------------------------------------------------- /frontend/src/api.js: -------------------------------------------------------------------------------- 1 | import axios from './axiosService'; 2 | 3 | export function getCommentSecurityData(contentType, objectPk) { 4 | return axios.get('/comments/security-data/', { 5 | params: { 6 | content_type: contentType, 7 | object_pk: objectPk, 8 | }, 9 | }); 10 | } 11 | 12 | export function getCommentList(contentType, objectPk) { 13 | return axios.get('/comments/', { 14 | params: { 15 | content_type: contentType, 16 | object_pk: objectPk, 17 | }, 18 | }); 19 | } 20 | 21 | export function postComment(token, data) { 22 | return axios.post('/comments/', data, { 23 | headers: { 24 | Authorization: 'Token ' + token, 25 | }, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /blogproject/notify/templatetags/notify_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.template.loader import render_to_string 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def display(obj, request=None): 10 | tpl = getattr(settings, "NOTIFICATION_TEMPLATES").get(obj.verb) 11 | 12 | if not tpl: 13 | return "" 14 | 15 | context = { 16 | "notification": obj, 17 | "actor": obj.actor, 18 | "target": obj.target, 19 | "request": request, 20 | } 21 | return render_to_string(tpl, context=context) 22 | 23 | 24 | @register.filter 25 | def frag(notification): 26 | verb = notification.verb 27 | return "notifications/inclusions/_{verb}.html".format(verb=verb) 28 | -------------------------------------------------------------------------------- /blogproject/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | avatar_url = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = User 11 | fields = [ 12 | "id", 13 | "name", 14 | "avatar_url", 15 | ] 16 | 17 | def get_avatar_url(self, obj): 18 | try: 19 | socialaccount = obj.socialaccounts[0] 20 | except AttributeError: 21 | socialaccount = obj.socialaccount_set.first() 22 | except IndexError: 23 | return "" 24 | 25 | if socialaccount is None: 26 | return "" 27 | return socialaccount.get_avatar_url() 28 | -------------------------------------------------------------------------------- /blogproject/webtools/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.crypto import get_random_string 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class DjangoSecretKeyCreateForm(forms.Form): 7 | CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)" 8 | 9 | prefix = forms.CharField(max_length=10, required=False, label=_("prefix")) 10 | suffix = forms.CharField(max_length=10, required=False, label=_("suffix")) 11 | 12 | def create_secret_key(self): 13 | prefix = self.cleaned_data.get("prefix", "") 14 | suffix = self.cleaned_data.get("suffix", "") 15 | 16 | body_len = 50 - len(prefix) - len(suffix) 17 | body = get_random_string(body_len, self.CHARS) 18 | return "".join([prefix, body, suffix]) 19 | -------------------------------------------------------------------------------- /blogproject/tags/models.py: -------------------------------------------------------------------------------- 1 | from core.models import TimeStampedModel 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | from taggit.models import GenericTaggedItemBase, TagBase 5 | 6 | 7 | class Tag(TimeStampedModel, TagBase): 8 | class Meta: 9 | verbose_name = _("tag") 10 | verbose_name_plural = _("tags") 11 | 12 | 13 | class TaggedItem(TimeStampedModel, GenericTaggedItemBase): 14 | tag = models.ForeignKey( 15 | Tag, 16 | on_delete=models.CASCADE, 17 | related_name="%(app_label)s_%(class)s_items", 18 | ) 19 | 20 | class Meta: 21 | verbose_name = _("tagged item") 22 | verbose_name_plural = _("tagged items") 23 | unique_together = [["content_type", "object_id", "tag"]] 24 | -------------------------------------------------------------------------------- /blogproject/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from blog.tests.factories import PostFactory 3 | from courses.tests.factories import CourseFactory, MaterialFactory 4 | from django.contrib.sites.models import Site 5 | from users.models import User 6 | 7 | 8 | @pytest.fixture 9 | def user(): 10 | return User.objects.create_user( 11 | username="user", password="password", email="user@zmrenwu.com" 12 | ) 13 | 14 | 15 | @pytest.fixture 16 | def post(user): 17 | return PostFactory(author=user, body="正文") 18 | 19 | 20 | @pytest.fixture 21 | def course(): 22 | return CourseFactory(description="**教程**") 23 | 24 | 25 | @pytest.fixture 26 | def material(): 27 | return MaterialFactory() 28 | 29 | 30 | @pytest.fixture 31 | def site(): 32 | return Site.objects.get(name="example.com") 33 | -------------------------------------------------------------------------------- /blogproject/blog/sitemaps.py: -------------------------------------------------------------------------------- 1 | from courses.models import Course, Material 2 | from django.contrib.sitemaps import GenericSitemap 3 | 4 | from .models import Category, Post 5 | 6 | post_info_dict = { 7 | "queryset": Post.objects.all(), 8 | "date_field": "modified", 9 | } 10 | 11 | category_info_dict = { 12 | "queryset": Category.objects.all(), 13 | } 14 | 15 | course_info_dict = { 16 | "queryset": Course.objects.all(), 17 | } 18 | 19 | material_info_dict = { 20 | "queryset": Material.objects.all(), 21 | } 22 | 23 | sitemaps = { 24 | "post": GenericSitemap(post_info_dict, priority=0.6), 25 | "category": GenericSitemap(category_info_dict, priority=1), 26 | "course": GenericSitemap(course_info_dict, priority=1), 27 | "material": GenericSitemap(material_info_dict, priority=0.6), 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./style/_util.scss"; 2 | @import "./style/_header.scss"; 3 | @import "./style/_notification.scss"; 4 | @import "./style/_post.scss"; 5 | @import "./style/_widget.scss"; 6 | @import "./style/_aside.scss"; 7 | @import "./style/_sidebar.scss"; 8 | @import "./style/_menu.scss"; 9 | @import "./style/_backtop.scss"; 10 | @import "./style/_toc.scss"; 11 | @import "./style/_donate.scss"; 12 | @import "./style/_navbar.scss"; 13 | @import "./style/_pagination.scss"; 14 | @import "./style/_login.scss"; 15 | @import "./style/_course.scss"; 16 | @import "./style/_hilite.scss"; 17 | @import "./style/_alert.scss"; 18 | @import "./style/_tasklist.scss"; 19 | @import "./style/_admonition.scss"; 20 | @import "./style/_literal.scss"; 21 | @import "./style/_tabbed.scss"; 22 | @import "./style/_offcanvas.scss"; 23 | -------------------------------------------------------------------------------- /frontend/src/scripts/search.ts: -------------------------------------------------------------------------------- 1 | (function () { 2 | const searchBtnMobile = document.querySelector('.search-button-mobile'); 3 | const searchBtnMobileCancel = document.querySelector('.search-button-mobile-cancel'); 4 | 5 | 6 | searchBtnMobile?.addEventListener('click', function (e) { 7 | console.log('searchBtnMobile'); 8 | 9 | e.preventDefault(); 10 | (document.querySelector('.search-form-mobile-wrapper') as HTMLElement).style.display = 'block'; 11 | (document.querySelector('.search-form-mobile input[type=search]') as HTMLElement).focus(); 12 | }); 13 | 14 | searchBtnMobileCancel?.addEventListener('click', function (e) { 15 | e && e.preventDefault(); 16 | ((document.querySelector('.search-form-mobile-wrapper'))).style.display = 'none' 17 | }); 18 | })() -------------------------------------------------------------------------------- /frontend/src/style/_menu.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .menu { 4 | margin: 0; 5 | padding: 0; 6 | border-radius: $width-border-radius; 7 | border: $width-border solid $color-border; 8 | position: fixed; 9 | top: $size-base * 5; 10 | transform: scale(0); 11 | transition: transform 0.2s; 12 | transform-origin: top left; 13 | z-index: 10; 14 | background-color: $color-background; 15 | 16 | .divider { 17 | border-bottom: $width-border solid $color-border; 18 | } 19 | 20 | i { 21 | width: $size-base * 2.5; 22 | } 23 | 24 | a { 25 | display: block; 26 | padding: $width-gap-half/2 $width-gap; 27 | 28 | &:hover { 29 | color: $color-primary; 30 | } 31 | } 32 | } 33 | 34 | .menu-toggle-checkbox:checked ~ .menu { 35 | transform: scale(1); 36 | transform-origin: top left; 37 | } -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_recommendation.html: -------------------------------------------------------------------------------- 1 | {% if recommendation_list %} 2 |
3 |

最新福利

4 |
5 | {% for rec in recommendation_list %} 6 |
7 |
8 | 9 |
10 | 16 |
17 | {% endfor %} 18 |
19 |
20 | {% endif %} -------------------------------------------------------------------------------- /frontend/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "entrypoints": { 3 | "main": { 4 | "assets": { 5 | "css": [ 6 | "/static/css/main.0620af7d.css" 7 | ], 8 | "js": [ 9 | "/static/js/main.61a9c678.js" 10 | ] 11 | } 12 | } 13 | }, 14 | "favicon.ico": "/static/favicon.ico", 15 | "img/alert-fill.svg": "/static/img/alert-fill.bbcee1b1.svg", 16 | "img/error-warning-fill.svg": "/static/img/error-warning-fill.027f8c93.svg", 17 | "img/information-fill.svg": "/static/img/information-fill.dfcc3b8f.svg", 18 | "img/lightbulb-line.svg": "/static/img/lightbulb-line.a44c3828.svg", 19 | "index.html": "/static/index.html", 20 | "js/main.61a9c678.js.map": "/static/js/main.61a9c678.js.map", 21 | "main.css": "/static/css/main.0620af7d.css", 22 | "main.js": "/static/js/main.61a9c678.js" 23 | } -------------------------------------------------------------------------------- /blogproject/users/adapter.py: -------------------------------------------------------------------------------- 1 | from allauth.account.utils import user_field 2 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 3 | 4 | from .models import User 5 | 6 | 7 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 8 | def populate_user(self, request, sociallogin, data): 9 | user = super().populate_user(request, sociallogin, data) 10 | name = data.get("name") 11 | 12 | if name: 13 | try: 14 | User.objects.get(name=name) 15 | name = "%s_%s" % (name, sociallogin.account.provider) 16 | user_field(user, "name", name) 17 | except User.DoesNotExist: 18 | user_field(user, "name", name) 19 | else: 20 | name = user.username 21 | user_field(user, "name", name) 22 | return user 23 | -------------------------------------------------------------------------------- /frontend/src/components/CommentList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /devops/ansible/roles/project/tasks/db.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update 3 | ansible.builtin.command: 4 | cmd: apt-get update -y 5 | become: true 6 | become_method: sudo 7 | 8 | - name: Install dependencies 9 | ansible.builtin.package: 10 | name: 11 | - libpq-dev 12 | - python3-psycopg2 13 | state: present 14 | become: true 15 | become_method: sudo 16 | 17 | - name: Create database 18 | # must use root, there is an error from ubuntu become to postgres 19 | # remote_user: root 20 | become: yes 21 | become_user: postgres 22 | community.postgresql.postgresql_db: 23 | name: "{{ db_name }}" 24 | 25 | - name: Create user 26 | # remote_user: root 27 | become: yes 28 | become_user: postgres 29 | community.postgresql.postgresql_user: 30 | db: "{{ db_name }}" 31 | name: "{{ db_user }}" 32 | password: "{{ db_pwd }}" 33 | priv: "ALL" -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_medium.html: -------------------------------------------------------------------------------- 1 | {% if medium_list %} 2 |
3 |

交流学习

4 |
5 | {% regroup medium_list by get_flag_display as grouped_medium_list %} 6 |
7 | {% for medium in grouped_medium_list %} 8 |
{{ medium.grouper }}
9 | {% for m in medium.list %} 10 | {# TODO: 消除魔法字符串 #} 11 | {% if m.flag == 0 %} 12 |
{{ m.identifier }}
13 | {% elif m.flag == 1 %} 14 |
{{ m.name }}
15 | {% endif %} 16 | {% endfor %} 17 | {% endfor %} 18 |
19 |
20 |
21 | {% endif %} -------------------------------------------------------------------------------- /blogproject/templates/notifications/base.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {% block header %}{% endblock %} 7 |
8 |
9 | {% block type %}{% endblock %} 10 | 11 | {% if notification.unread %} 12 | 14 | 标为已读 15 | {% endif %} 16 |
17 |
18 |
19 |
20 |
21 | {{ target.comment_html|safe }} 22 |
-------------------------------------------------------------------------------- /blogproject/blog/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils.html import strip_tags 2 | from haystack.utils import Highlighter as HaystackHighlighter 3 | from jieba.analyse.analyzer import ChineseTokenizer 4 | 5 | 6 | class Highlighter(HaystackHighlighter): 7 | """ 8 | 自定义关键词高亮器,不截断过短的文本(例如文章标题) 9 | """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.query_words = set([token.text for token in ChineseTokenizer()(self.query)]) 14 | 15 | def highlight(self, text_block): 16 | self.text_block = strip_tags(text_block) 17 | highlight_locations = self.find_highlightable_words() 18 | start_offset, end_offset = self.find_window(highlight_locations) 19 | if len(text_block) < self.max_length: 20 | start_offset = 0 21 | return self.render_html(highlight_locations, start_offset, end_offset) 22 | -------------------------------------------------------------------------------- /blogproject/blog/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Manager, QuerySet 2 | from django.utils import timezone 3 | 4 | 5 | class PostQuerySet(QuerySet): 6 | def published(self): 7 | return self.filter(status=self.model.STATUS_CHOICES.published) 8 | 9 | def draft(self): 10 | return self.filter(status=self.model.STATUS_CHOICES.draft) 11 | 12 | def hidden(self): 13 | return self.filter(status=self.model.STATUS_CHOICES.hidden) 14 | 15 | def searchable(self): 16 | return self.published().filter(pub_date__lte=timezone.now()) 17 | 18 | 19 | class PostManager(Manager.from_queryset(PostQuerySet)): 20 | pass 21 | 22 | 23 | class IndexPostManager(Manager.from_queryset(PostQuerySet)): 24 | """ 25 | 专门用于管理首页文章的模型管理器 26 | """ 27 | 28 | def get_queryset(self): 29 | return super().get_queryset().searchable().filter(show_on_index=True) 30 | -------------------------------------------------------------------------------- /blogproject/templates/courses/inclusions/_toc.html: -------------------------------------------------------------------------------- 1 | {% load cache %} 2 | {% cache 300 course_toc course.pk %} 3 |
    4 | {% for material in material_list %} 5 |
  • 6 | {% if material.toc %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | {{ material.title }} 12 | {% if material.toc %} 13 |
      14 | {{ material.toc|safe }} 15 |
    16 | {% endif %} 17 |
  • 18 | {% endfor %} 19 |
20 | {% endcache %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django # NOQA 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /blogproject/blog/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from blog.models import Category, Post, Tag 3 | from factory.django import DjangoModelFactory 4 | from users.tests.factories import UserFactory 5 | 6 | 7 | class PostFactory(DjangoModelFactory): 8 | title = factory.Faker("sentence") 9 | body = factory.Faker("paragraph") 10 | brief = factory.Faker("sentence") 11 | status = Post.STATUS_CHOICES.published 12 | author = factory.SubFactory(UserFactory) 13 | 14 | class Meta: 15 | model = Post 16 | 17 | 18 | class CategoryFactory(DjangoModelFactory): 19 | name = factory.Faker("word") 20 | slug = factory.LazyAttribute(lambda c: c.name) 21 | creator = factory.SubFactory(UserFactory) 22 | 23 | class Meta: 24 | model = Category 25 | 26 | 27 | class TagFactory(DjangoModelFactory): 28 | name = factory.Faker("word") 29 | 30 | class Meta: 31 | model = Tag 32 | -------------------------------------------------------------------------------- /blogproject/courses/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Manager, QuerySet 2 | from django.utils import timezone 3 | 4 | 5 | class MaterialQuerySet(QuerySet): 6 | def published(self): 7 | return self.filter(status=self.model.STATUS.published) 8 | 9 | def writing(self): 10 | return self.filter(status=self.model.STATUS.writing) 11 | 12 | def draft(self): 13 | return self.filter(status=self.model.STATUS.draft) 14 | 15 | def hidden(self): 16 | return self.filter(status=self.model.STATUS.hidden) 17 | 18 | def searchable(self): 19 | return self.published().filter(pub_date__lte=timezone.now()) 20 | 21 | 22 | class MaterialManager(Manager.from_queryset(MaterialQuerySet)): 23 | pass 24 | 25 | 26 | class IndexMaterialManager(MaterialManager): 27 | def get_queryset(self): 28 | return super().get_queryset().searchable().filter(show_on_index=True) 29 | -------------------------------------------------------------------------------- /blogproject/users/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import AbstractUser 3 | from django.db import models 4 | from django.db.models.signals import post_save 5 | from django.dispatch import receiver 6 | from django.utils.translation import gettext_lazy as _ 7 | from rest_framework.authtoken.models import Token 8 | 9 | 10 | class User(AbstractUser): 11 | name = models.CharField(_("name"), blank=True, max_length=255) 12 | email_bound = models.BooleanField(_("email bound"), default=False) 13 | 14 | def social_avatar(self): 15 | if self.socialaccount_set.exists(): 16 | return self.socialaccount_set.first().get_avatar_url() 17 | return "" 18 | 19 | 20 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 21 | def create_auth_token(sender, instance=None, created=False, **kwargs): 22 | if created: 23 | Token.objects.create(user=instance) 24 | -------------------------------------------------------------------------------- /frontend/src/scripts/backtop.ts: -------------------------------------------------------------------------------- 1 | class BackTop { 2 | private _element: HTMLElement; 3 | constructor(element: HTMLElement) { 4 | this._element = element; 5 | this._init(); 6 | } 7 | 8 | _init(): void { 9 | window.addEventListener('scroll', this.toggle.bind(this)); 10 | this._element?.addEventListener('click', this.to.bind(this)); 11 | } 12 | 13 | to(): void { 14 | if (document.documentElement.scrollTop > 0) { 15 | document.documentElement.scrollTo({ top: 0, behavior: 'smooth' }); 16 | } 17 | } 18 | 19 | toggle(): void { 20 | const pos = window.innerHeight / 3; 21 | 22 | if (document.documentElement.scrollTop > pos) { 23 | this._element?.classList.add('fade-in'); 24 | } else { 25 | this._element?.classList.remove('fade-in'); 26 | } 27 | } 28 | } 29 | 30 | export default BackTop; 31 | -------------------------------------------------------------------------------- /blogproject/comments/migrations/0002_blogcomment_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 06:09 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("comments", "0001_initial"), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="blogcomment", 20 | name="user", 21 | field=models.ForeignKey( 22 | blank=True, 23 | null=True, 24 | on_delete=django.db.models.deletion.SET_NULL, 25 | related_name="blogcomment_comments", 26 | to=settings.AUTH_USER_MODEL, 27 | verbose_name="user", 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /blogproject/alerts/models.py: -------------------------------------------------------------------------------- 1 | from core.models import TimeStampedModel 2 | from django.contrib.contenttypes.fields import GenericForeignKey 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.db import models 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | class Alert(TimeStampedModel): 9 | text = models.TextField(_("text")) 10 | show = models.BooleanField(_("show"), default=True) 11 | 12 | content_type = models.ForeignKey( 13 | ContentType, 14 | on_delete=models.CASCADE, 15 | ) 16 | object_id = models.PositiveIntegerField() 17 | content_object = GenericForeignKey("content_type", "object_id") 18 | 19 | rank = models.IntegerField(_("rank"), default=0) 20 | 21 | class Meta: 22 | verbose_name = _("alert") 23 | verbose_name_plural = _("alerts") 24 | ordering = ["rank", "-created_at"] 25 | 26 | def __str__(self): 27 | return self.text[:30] 28 | -------------------------------------------------------------------------------- /blogproject/favorites/migrations/0003_auto_20200920_1604.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-20 08:04 2 | 3 | from django.db import migrations 4 | import taggit.managers 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tags', '0001_initial'), 11 | ('favorites', '0002_auto_20200920_1601'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='favorite', 17 | name='tags', 18 | field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 19 | ), 20 | migrations.AddField( 21 | model_name='issue', 22 | name='tags', 23 | field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /docs/post.md: -------------------------------------------------------------------------------- 1 | 博客最核心的功能就是发表博客文章。 2 | 3 | 点击 **博客** 板块下的 **文章**,将进入到已发布的博客文章列表页面: 4 | 5 | ![后台博客文章列表](img/blog_post_list.png) 6 | 7 | 点击右上角红色方框标出的 **增加文章** 按钮,进入到文章发布页面: 8 | 9 | ![发布文章](img/add_post.png) 10 | 11 | 各输入项说明如下: 12 | 13 | **标题**:博客文章的标题 14 | 15 | **正文**:博客文章的内容 16 | 17 | **简介**:博客文章简介,类似于摘要,可填可不填,如果填写的话,博客文章列表会显示填写的摘要内容,图中红色方框部分就是简介内容。 18 | 19 | ![博客文章简介](img/post_brief.png) 20 | 21 | **分类**:文章分类,如何添加分类详见 [添加文章分类](category.md) 22 | 23 | **标签**:文章标签,如何添加标签详见 [标签](tag.md) 24 | 25 | **状态**:文章状态,用于控制文章的显示,有以下三种状态 26 | 27 | - 草稿:草稿状态的文章不会在博客首页的文章列表显示,且无法被搜索到 28 | - 隐藏:隐藏状态的文章不会在博客首页的文章列表显示,但可以被搜索到 29 | - 发布:正常发布的文章,会在博客首页的文章列表显示。 30 | 31 | **发布时间**:文章发布的时间,可以选择一个比当前时间更晚的时间,达到定时发布的效果。 32 | 33 | **在首页显示**:是否显示在博客首页的文章列表中。但无论是否显示,都可以被搜索到。 34 | 35 | **启用评论**:允许评论。 36 | 37 | **置顶**:置顶的文章会显示在博客首页的文章列表的最前面。 38 | 39 | **摘要**:类似于简介,主要用于搜索引擎优化,对内容的展示没有影响。 40 | 41 | 内容填写完成后点击 **保存** 就可以发布文章了。 42 | 43 | 此外,对于已发布的文章,在后台的文章列表页点击文章标题可进入文章编辑页面,你可以对文章内容进行修改,修改完后别忘了点击 **保存**。 -------------------------------------------------------------------------------- /docs/course.md: -------------------------------------------------------------------------------- 1 | 博客包含一套教程系统,其设计初衷是方便组织成体系的博客文章,例如 xxx 入门教程 x 篇,类似于 GitBook。 2 | 3 | 博客中的全部教程会在教程列表页展示: 4 | 5 | ![](img/course_list.png) 6 | 7 | 教程中的文章会以文档集合的形式展示,左边是目录,右边是内容,方便成体系地阅读。 8 | 9 | ![](img/course_detail.png) 10 | 11 | ## 添加教程分类 12 | 13 | 在博客后台 **教程** 板块的 **分类** 下可以添加教程的分类: 14 | 15 | ![](img/course_category.png) 16 | 17 | 分类主要用于组织不同主题的教程。例如第一张图展示的教程列表页面,可以看到 2 个分类:django 和 Vue,这是因为在后台录入了 django 和 Vue 的分类。 18 | 19 | ![](img/admin_course_category_list.png) 20 | 21 | ## 添加教程 22 | 23 | 成体系的博客文章由一个教程来组织。 24 | 25 | 在博客后台 **教程** 板块的 **教程** 下可以添加教程。 26 | 27 | ![](img/add_course.png) 28 | 29 | 各关键字段的含义说明如下: 30 | 31 | slug:和博客文章分类中 slug 的含义一样 32 | 33 | 描述:对教程的总体描述,用户点击某个教程后首先展示的就是描述的内容,描述支持 Markdown 语法。 34 | 35 | 简介:主要用于 SEO。 36 | 37 | 封面:教程展示的封面 38 | 39 | 状态:writing 和 finished,分别表示正在书写中和已完成 40 | 41 | 级别:教程的难度级别。 42 | 43 | 序号:教程列表展示的排序,数字越小越靠前。 44 | 45 | 分类:教程的分类。 46 | 47 | ## 添加教程类文章 48 | 49 | 在博客后台 **教程** 板块的 **资料** 下可以添加教程类文章。 50 | 51 | ![](img/add_material.png) -------------------------------------------------------------------------------- /blogproject/newsletters/models.py: -------------------------------------------------------------------------------- 1 | from core.models import TimeStampedModel 2 | from django.conf import settings 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class Subscription(TimeStampedModel): 8 | email = models.EmailField(_("email")) 9 | user = models.ForeignKey( 10 | settings.AUTH_USER_MODEL, 11 | verbose_name=_("user"), 12 | on_delete=models.SET_NULL, 13 | null=True, 14 | ) 15 | confirmed = models.BooleanField(_("confirmed"), default=False) 16 | active = models.BooleanField(_("active"), default=False) 17 | 18 | class Meta: 19 | verbose_name = _("subscription") 20 | verbose_name_plural = _("subscriptions") 21 | ordering = ["-created_at"] 22 | 23 | def __str__(self): 24 | return self.email 25 | 26 | def confirm(self): 27 | self.confirmed = True 28 | self.active = True 29 | self.save(update_fields=["confirmed", "active"]) 30 | -------------------------------------------------------------------------------- /blogproject/favorites/migrations/0004_auto_20210411_1737.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-04-11 09:37 2 | 3 | from django.db import migrations 4 | import taggit_selectize.managers 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tags', '0001_initial'), 11 | ('favorites', '0003_auto_20200920_1604'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='favorite', 17 | name='tags', 18 | field=taggit_selectize.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 19 | ), 20 | migrations.AlterField( 21 | model_name='issue', 22 | name='tags', 23 | field=taggit_selectize.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tags.TaggedItem', to='tags.Tag', verbose_name='tags'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_donate.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 |
4 | 9 | 24 |
-------------------------------------------------------------------------------- /blogproject/templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load highlight %} 3 | {% load comments %} 4 | 5 | {% block extra_styles %} 6 | {{ block.super }} 7 | 12 | {% endblock extra_styles %} 13 | {% block title %}搜索结果_{{ config.LOGO }}{% endblock title %} 14 | 15 | {% block main %} 16 |
17 | {% if query %} 18 | {% for result in page.object_list %} 19 |
20 | {% include 'search/_search_entry_list_item.html' with entry=result.object %} 21 | {% empty %} 22 |
23 |
没有搜索到你想要的结果!
24 | {% endfor %} 25 | {% if page.has_other_pages %} 26 | {% include 'pure_pagination/pagination.html' with page_obj=page query=query %} 27 | {% endif %} 28 | {% else %} 29 |
请输入搜索关键词,例如 django
30 | {% endif %} 31 |
32 | {% endblock main %} -------------------------------------------------------------------------------- /blogproject/blog/templatetags/blog_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from friendlinks.models import FriendLink 3 | 4 | from ..models import Medium, Recommendation 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag("blog/inclusions/_friend_link.html", takes_context=True) 10 | def show_friend_links(context, num=10): 11 | friend_link_list = FriendLink.objects.all()[:num] 12 | return {"friend_link_list": friend_link_list} 13 | 14 | 15 | @register.inclusion_tag("blog/inclusions/_medium.html", takes_context=True) 16 | def show_mediums(context): 17 | medium_list = Medium.objects.all() 18 | return {"medium_list": medium_list} 19 | 20 | 21 | @register.inclusion_tag("blog/inclusions/_recommendation.html", takes_context=True) 22 | def show_recommendations(context): 23 | recommendation_list = Recommendation.objects.all() 24 | return {"recommendation_list": recommendation_list} 25 | 26 | 27 | @register.inclusion_tag("blog/inclusions/_ad.html", takes_context=True) 28 | def show_ads(context): 29 | return {} 30 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | '@vue/prettier', 11 | '@vue/prettier/@typescript-eslint', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | parser: '@typescript-eslint/parser', 16 | }, 17 | rules: { 18 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | 'prettier/prettier': [ 21 | 'warn', 22 | { 23 | tabWidth: 4, 24 | useTabs: false, 25 | singleQuote: true, 26 | trailingComma: 'all', 27 | printWidth: 120, 28 | }, 29 | ], 30 | '@typescript-eslint/ban-ts-ignore': 'off', 31 | '@typescript-eslint/ban-ts-comment': 'off', 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /compose/production/django/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # N.B. If only .env files supported variable expansion... 4 | export CELERY_BROKER_URL="${REDIS_URL}" 5 | 6 | 7 | if [ -z "${POSTGRES_USER}" ]; then 8 | base_postgres_image_default_user='postgres' 9 | export POSTGRES_USER="${base_postgres_image_default_user}" 10 | fi 11 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 12 | 13 | postgres_ready() { 14 | python << END 15 | import sys 16 | 17 | import psycopg2 18 | 19 | try: 20 | psycopg2.connect( 21 | dbname="${POSTGRES_DB}", 22 | user="${POSTGRES_USER}", 23 | password="${POSTGRES_PASSWORD}", 24 | host="${POSTGRES_HOST}", 25 | port="${POSTGRES_PORT}", 26 | ) 27 | except psycopg2.OperationalError: 28 | sys.exit(-1) 29 | sys.exit(0) 30 | 31 | END 32 | } 33 | until postgres_ready; do 34 | >&2 echo 'Waiting for PostgreSQL to become available...' 35 | sleep 1 36 | done 37 | >&2 echo 'PostgreSQL is available' 38 | 39 | exec "$@" 40 | -------------------------------------------------------------------------------- /blogproject/templates/blog/archives.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main %} 3 |
4 |
5 | {% regroup post_list by pub_date.year as year_post_group %} 6 |
    7 | {% for year in year_post_group %} 8 |
  • {{ year.grouper }} 年 9 | {% regroup year.list by pub_date.month as month_post_group %} 10 |
      11 | {% for month in month_post_group %} 12 |
    • {{ month.grouper }} 月 ({{ month.list|length }} 篇) 13 |
        14 | {% for post in month.list %} 15 |
      • {{ post.title }} 16 |
      • 17 | {% endfor %} 18 |
      19 |
    • 20 | {% endfor %} 21 |
    22 |
  • 23 | {% endfor %} 24 |
25 |
26 |
27 | {% endblock main %} -------------------------------------------------------------------------------- /blogproject/comments/templatetags/comments_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.contenttypes.models import ContentType 3 | from rest_framework.authtoken.models import Token 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.inclusion_tag("comments/inclusions/_comments_app.html", takes_context=True) 9 | def show_comment_app(context, target): 10 | num_comments = context["num_comments"] 11 | num_comment_participants = context["num_comment_participants"] 12 | user = context["user"] 13 | app_label, model = ContentType.objects.get_for_model(target).natural_key() 14 | content_type = "{}.{}".format(app_label, model) 15 | object_pk = target.pk 16 | token = "" 17 | if user.is_authenticated: 18 | try: 19 | token = user.auth_token.key 20 | except Token.DoesNotExist: 21 | pass 22 | return { 23 | "content_type": content_type, 24 | "object_pk": object_pk, 25 | "token": token, 26 | "num_comments": num_comments, 27 | "num_comment_participants": num_comment_participants, 28 | } 29 | -------------------------------------------------------------------------------- /blogproject/blog/migrations/0004_auto_20200306_1047.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-03-06 01:47 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("blog", "0003_post_meta_ordering"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="friendlink", 15 | options={ 16 | "verbose_name": "friend link", 17 | "verbose_name_plural": "friend links", 18 | }, 19 | ), 20 | migrations.AlterModelOptions( 21 | name="medium", 22 | options={ 23 | "ordering": ["flag", "name"], 24 | "verbose_name": "medium", 25 | "verbose_name_plural": "mediums", 26 | }, 27 | ), 28 | migrations.AlterModelOptions( 29 | name="recommendation", 30 | options={ 31 | "verbose_name": "recommendation", 32 | "verbose_name_plural": "recommendations", 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /blogproject/templates/courses/material_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'courses/base.html' %} 2 | {% load static %} 3 | {% load courses_extras %} 4 | {% load comments_extras %} 5 | 6 | {% block sidebar_mobile %} 7 | {% include 'courses/inclusions/_sidebar_mobile.html' %} 8 | {% endblock sidebar_mobile %} 9 | 10 | {% block article %} 11 |
12 |

{{ material.title }}

13 | {% include 'courses/inclusions/_material_meta.html' %} 14 |
15 | {{ material.body_html|safe }} 16 |

17 | -- EOF -- 18 |

19 | 20 |
21 | 最后更新:{{ material.modified }} 22 |
23 |
24 |
25 | {% endblock article %} 26 |
27 | {% block comment %} 28 |
29 | {% if user.is_anonymous %} 30 | {% include 'account/inclusions/_login.html' %} 31 | {% endif %} 32 | {% show_comment_app material %} 33 |
34 | {% endblock comment %} 35 | -------------------------------------------------------------------------------- /frontend/src/style/_notification.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .unread { 4 | color: var(--color-danger); 5 | } 6 | 7 | .unread-bg { 8 | background-color: lighten(yellow, 45%); 9 | } 10 | 11 | .notification-list { 12 | a { 13 | text-decoration: none; 14 | } 15 | 16 | .notification-list-header { 17 | padding-bottom: $width-gap-half; 18 | //border-bottom: 1px solid $color-border; 19 | 20 | .notification-func-btn:not(:last-child) { 21 | margin-right: $width-gap; 22 | } 23 | } 24 | 25 | .notification { 26 | margin: $width-gap-double 0; 27 | padding: $width-gap-half $width-gap-half / 2; 28 | 29 | &:nth-of-type(n) { 30 | margin-bottom: 0; 31 | } 32 | 33 | &:nth-of-type(2) { 34 | margin-top: $width-gap-half; 35 | } 36 | } 37 | 38 | .notification-avatar { 39 | width: 48px; 40 | border-radius: 50%; 41 | margin-right: $width-gap; 42 | } 43 | } 44 | 45 | .notification-meta { 46 | margin-top: 5px; 47 | 48 | div { 49 | > i, 50 | > time, 51 | > a { 52 | margin-right: 7px; 53 | } 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /blogproject/templates/blog/donate.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block main %} 4 |
5 |
6 |
如果觉得本博客的教程对你有帮助,不妨小额赞助博主一下,鼓励博主继续写出更多高质量的教程。
7 |
8 |
9 |
10 |
11 | 微信支付收款二维码 13 |
微信
14 |
15 |
16 |
17 |
18 | 支付宝收款二维码 19 |
支付宝
20 |
21 |
22 |
23 |
24 |
25 |
26 | {% endblock main %} -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | working_dir="$(dirname ${0})" 11 | source "${working_dir}/_sourced/constants.sh" 12 | source "${working_dir}/_sourced/messages.sh" 13 | 14 | 15 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 16 | 17 | 18 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 19 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 20 | exit 1 21 | fi 22 | 23 | export PGHOST="${POSTGRES_HOST}" 24 | export PGPORT="${POSTGRES_PORT}" 25 | export PGUSER="${POSTGRES_USER}" 26 | export PGPASSWORD="${POSTGRES_PASSWORD}" 27 | export PGDATABASE="${POSTGRES_DB}" 28 | 29 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 30 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 31 | 32 | 33 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 34 | -------------------------------------------------------------------------------- /blogproject/core/decrators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import typing 3 | 4 | from django.utils.translation import gettext_lazy as _ 5 | from rest_framework.exceptions import ValidationError 6 | 7 | 8 | def field_whitelist(fields: typing.Iterable, raise_exception: bool = True): 9 | def decorator(func): 10 | @functools.wraps(func) 11 | def wrapper(*args, **kwargs): 12 | try: 13 | # self.request 14 | request = args[0].request 15 | except AttributeError: 16 | # first arg is request 17 | request = args[0] 18 | 19 | extras = request.data.keys() - set(list(fields) + ["id"]) 20 | if len(extras) != 0 and raise_exception: 21 | raise ValidationError( 22 | {"detail": _("Only accept {fields} fields.".format(fields=fields))} 23 | ) 24 | 25 | for black_field in extras: 26 | del request.data[black_field] 27 | return func(*args, **kwargs) 28 | 29 | # wrapper.__wrapped__ = func 30 | return wrapper 31 | 32 | return decorator 33 | -------------------------------------------------------------------------------- /blogproject/templates/notifications/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load notify_tags %} 4 | {% block main %} 5 |
6 | 13 | {% for notification in notifications %} 14 |
15 | {% include notification|frag with target=notification.target actor=notification.actor %} 16 |
17 | {% empty %} 18 |
19 | 暂无通知 20 |
21 | {% endfor %} 22 | {% if is_paginated %} 23 | {{ page_obj.render }} 24 | {% endif %} 25 |
26 | {% endblock main %} -------------------------------------------------------------------------------- /blogproject/comments/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | from django_comments.forms import COMMENT_MAX_LENGTH, CommentForm 4 | 5 | from . import get_model 6 | 7 | 8 | class BlogCommentForm(CommentForm): 9 | parent = forms.IntegerField(required=False, widget=forms.HiddenInput) 10 | comment = forms.CharField(label=_("Comment"), max_length=COMMENT_MAX_LENGTH) 11 | 12 | def __init__(self, target_object, parent=None, data=None, initial=None, **kwargs): 13 | self.parent = parent 14 | if initial is None: 15 | initial = {} 16 | 17 | if parent: 18 | initial.update({"parent": self.parent.pk}) 19 | super().__init__(target_object, data=data, initial=initial, **kwargs) 20 | self.fields["email"].required = False 21 | self.fields["name"].required = False 22 | 23 | def get_comment_model(self): 24 | return get_model() 25 | 26 | def get_comment_create_data(self, **kwargs): 27 | d = super().get_comment_create_data() 28 | # todo: validate parent 29 | d["parent_id"] = self.cleaned_data["parent"] 30 | return d 31 | -------------------------------------------------------------------------------- /compose/external/django/DockerfileMainland: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list 5 | RUN apt-get update && apt-get install -y gettext python3-dev libpq-dev wget 6 | 7 | WORKDIR /app 8 | 9 | # githubusercontent 无法访问,使用 jsdelivr 加速 10 | # 从 Gitub 下载 realease 非常慢,使用 fastgit 镜像加速 11 | RUN curl -sSL https://cdn.jsdelivr.net/gh/python-poetry/poetry/get-poetry.py | sed 's+https://github.com/python-poetry/poetry+https://download.fastgit.org/python-poetry/poetry+g' | sed 's+https://github.com/sdispater/poetry/releases/download/+https://download.fastgit.org/python-poetry/poetry/releases/download/+g' | python - 12 | ENV PATH "/root/.poetry/bin:${PATH}" 13 | RUN poetry config virtualenvs.create false 14 | COPY poetry.lock pyproject.toml /app/ 15 | RUN poetry install --no-root --no-interaction 16 | 17 | COPY ./compose/local/django/celery/worker/start.sh /start-celeryworker.sh 18 | RUN sed -i 's/\r$//g' /start-celeryworker.sh 19 | RUN chmod +x /start-celeryworker.sh 20 | 21 | COPY ./compose/local/django/celery/beat/start.sh /start-celerybeat.sh 22 | RUN sed -i 's/\r$//g' /start-celerybeat.sh 23 | RUN chmod +x /start-celerybeat.sh -------------------------------------------------------------------------------- /frontend/src/style/_offcanvas.scss: -------------------------------------------------------------------------------- 1 | // Most of codes are borrowed from Bootstrap v5 2 | 3 | @import 'variables'; 4 | 5 | .offcanvas { 6 | position: fixed; 7 | bottom: 0; 8 | z-index: 900; 9 | display: flex; 10 | flex-direction: column; 11 | max-width: 100%; 12 | visibility: hidden; 13 | background-color: $color-background; 14 | background-clip: padding-box; 15 | outline: 0; 16 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 17 | transition: transform 500ms ease-in-out; 18 | } 19 | 20 | .offcanvas-header { 21 | display: flex; 22 | align-items: center; 23 | justify-content: space-between; 24 | padding: $width-gap-half; 25 | } 26 | 27 | .offcanvas-title { 28 | margin-bottom: 0; 29 | } 30 | 31 | .offcanvas-body { 32 | flex-grow: 1; 33 | padding: $width-gap-half; 34 | overflow-y: auto; 35 | } 36 | 37 | .offcanvas-start { 38 | top: 0; 39 | left: 0; 40 | width: 70%; 41 | transform: translateX(-100%); 42 | } 43 | 44 | .offcanvas.show { 45 | transform: none; 46 | } 47 | 48 | .offcanvas-backdrop::before { 49 | position: fixed; 50 | top: 0; 51 | left: 0; 52 | z-index: 899; 53 | width: 100vw; 54 | height: 100vh; 55 | content: ""; 56 | opacity: 0; 57 | } -------------------------------------------------------------------------------- /blogproject/favorites/views.py: -------------------------------------------------------------------------------- 1 | from braces.views import SetHeadlineMixin 2 | from django.views.generic import DetailView, ListView 3 | 4 | from .models import Issue 5 | 6 | 7 | class IssueListView(SetHeadlineMixin, ListView): 8 | model = Issue 9 | context_object_name = "issue_list" 10 | template_name = "favorites/issue_list.html" 11 | paginate_by = 15 12 | headline = "每周精选收藏" 13 | 14 | def get_queryset(self): 15 | return super().get_queryset().prefetch_related("tags").order_by("-number") 16 | 17 | 18 | class IssueDetailView(SetHeadlineMixin, DetailView): 19 | model = Issue 20 | context_object_name = "issue" 21 | template_name = "favorites/issue_detail.html" 22 | slug_field = "number" 23 | slug_url_kwarg = "number" 24 | 25 | def get_context_data(self, *, object_list=None, **kwargs): 26 | issue = self.object 27 | favorite_qs = issue.favorites.order_by("rank", "-created_at") 28 | context = super().get_context_data(object_list=object_list, **kwargs) 29 | context["favorite_list"] = favorite_qs 30 | return context 31 | 32 | def get_headline(self): 33 | return "第{}周精选收藏".format(self.get_object().number) 34 | -------------------------------------------------------------------------------- /blogproject/newsletters/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .models import Subscription 5 | 6 | 7 | class SubscriptionForm(forms.ModelForm): 8 | def __init__(self, *args, **kwargs): 9 | self.user = kwargs.pop("user") 10 | super(SubscriptionForm, self).__init__(*args, **kwargs) 11 | 12 | class Meta: 13 | model = Subscription 14 | fields = ["email"] 15 | 16 | def clean_email(self): 17 | email = self.cleaned_data["email"] 18 | try: 19 | subscription = Subscription.objects.get(email=email) 20 | if subscription.confirmed: 21 | raise forms.ValidationError(_("Already subscribed!")) 22 | else: 23 | subscription.delete() 24 | except Subscription.DoesNotExist: 25 | pass 26 | 27 | return email 28 | 29 | def save(self, commit=True): 30 | subscription = super().save(commit=False) 31 | if self.user and self.user.is_authenticated: 32 | subscription.user = self.user 33 | 34 | if commit: 35 | subscription.save() 36 | return subscription 37 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-blog-project-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "main": "src/main.ts", 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "core-js": "^3.6.5", 14 | "mobi.css": "^3.1.1", 15 | "vue": "^3.0.0" 16 | }, 17 | "devDependencies": { 18 | "@typescript-eslint/eslint-plugin": "^4.18.0", 19 | "@typescript-eslint/parser": "^4.18.0", 20 | "@vue/cli-plugin-babel": "^5.0.0-alpha.8", 21 | "@vue/cli-plugin-eslint": "^5.0.0-alpha.8", 22 | "@vue/cli-plugin-typescript": "^5.0.0-alpha.8", 23 | "@vue/cli-service": "^5.0.0-alpha.8", 24 | "@vue/compiler-sfc": "^3.0.0", 25 | "@vue/eslint-config-prettier": "^6.0.0", 26 | "@vue/eslint-config-typescript": "^7.0.0", 27 | "eslint": "^7.20.0", 28 | "eslint-plugin-prettier": "^3.3.1", 29 | "eslint-plugin-vue": "^7.6.0", 30 | "node-sass": "^4.12.0", 31 | "prettier": "^2.2.1", 32 | "sass-loader": "^8.0.2", 33 | "typescript": "~4.1.5", 34 | "webpack-assets-manifest": "^5.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blogproject/notify/views.py: -------------------------------------------------------------------------------- 1 | from braces.views import SetHeadlineMixin 2 | from notifications.views import AllNotificationsList, UnreadNotificationsList 3 | from pure_pagination.mixins import PaginationMixin 4 | 5 | 6 | class AllNotificationsListView(PaginationMixin, SetHeadlineMixin, AllNotificationsList): 7 | headline = "全部通知" 8 | paginate_by = 10 9 | prefetch_related = ("actor", "target") 10 | 11 | def get_context_data(self, **kwargs): 12 | context = super().get_context_data(**kwargs) 13 | context["num_all"] = self.request.user.notifications.active().count() 14 | context["num_unread"] = self.request.user.notifications.unread().count() 15 | return context 16 | 17 | 18 | class UnreadNotificationsListView( 19 | PaginationMixin, SetHeadlineMixin, UnreadNotificationsList 20 | ): 21 | headline = "未读通知" 22 | paginate_by = 10 23 | prefetch_related = ("actor", "target") 24 | 25 | def get_context_data(self, **kwargs): 26 | context = super().get_context_data(**kwargs) 27 | context["num_all"] = self.request.user.notifications.active().count() 28 | context["num_unread"] = self.request.user.notifications.unread().count() 29 | return context 30 | -------------------------------------------------------------------------------- /blogproject/courses/templatetags/courses_extras.py: -------------------------------------------------------------------------------- 1 | from core.utils import generate_rich_content 2 | from django import template 3 | from django.core.cache import cache 4 | from django.core.cache.utils import make_template_fragment_key 5 | from django.urls import reverse 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.inclusion_tag("courses/inclusions/_toc.html") 11 | def show_course_toc(course, current=None): 12 | material_list = course.material_set.all().values("id", "title", "body") 13 | 14 | key = make_template_fragment_key("course_toc", [course.pk]) 15 | result = cache.get(key) 16 | # if not cached, regenerate the course's toc 17 | if result is None: 18 | # attach toc to material 19 | for material in material_list: 20 | toc_url = reverse( 21 | "courses:material_detail", 22 | kwargs={"slug": course.slug, "pk": material["id"]}, 23 | ) 24 | material["toc"] = generate_rich_content(material["body"], toc_url=toc_url)[ 25 | "toc" 26 | ] 27 | 28 | context = { 29 | "material_list": material_list, 30 | "course": course, 31 | "current": current, 32 | } 33 | return context 34 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_detail.html: -------------------------------------------------------------------------------- 1 | {% load comments_extras %} 2 | 3 |
4 |
5 |

{{ post.title }}

6 | 13 |
14 | {{ post.body_html|safe }} 15 |

16 | -- EOF -- 17 |

18 | {% include 'inclusions/_donate.html' %} 19 | {% include 'blog/inclusions/_tags.html' %} 20 | 21 | {% block related_posts %}{% endblock related_posts %} 22 |
23 |
24 |
25 |
26 |
27 | {% if user.is_anonymous %} 28 | {% include 'account/inclusions/_login.html' %} 29 | {% endif %} 30 | {% show_comment_app post %} 31 |
-------------------------------------------------------------------------------- /blogproject/comments/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from blog.tests.factories import PostFactory 3 | from comments.forms import BlogCommentForm 4 | from comments.models import BlogComment 5 | from django.contrib.sites.models import Site 6 | 7 | from .factories import BlogCommentFactory 8 | 9 | 10 | @pytest.mark.django_db 11 | class TestBlogCommentForm: 12 | def setup_method(self): 13 | site = Site.objects.get(name="example.com") 14 | post = PostFactory() 15 | self.comment = BlogCommentFactory( 16 | is_public=True, is_removed=False, site=site, content_object=post 17 | ) 18 | form = BlogCommentForm(target_object=post, parent=self.comment) 19 | self.bound_form = BlogCommentForm( 20 | target_object=post, 21 | parent=self.comment, 22 | data=dict(**form.initial, comment="test comment"), 23 | ) 24 | 25 | def test_get_comment_model(self): 26 | assert self.bound_form.get_comment_model() == BlogComment 27 | 28 | def test_get_comment_create_data(self): 29 | assert self.bound_form.is_valid(), self.bound_form.errors 30 | comment_create_data = self.bound_form.get_comment_create_data() 31 | assert comment_create_data["parent_id"] == self.comment.pk 32 | -------------------------------------------------------------------------------- /frontend/src/style/_donate.scss: -------------------------------------------------------------------------------- 1 | .donate-wrapper { 2 | position: relative; 3 | } 4 | 5 | .donate-panel { 6 | position: absolute; 7 | left: 50%; 8 | right: 50%; 9 | transform: translateY(-100%); 10 | z-index: 10; 11 | } 12 | 13 | .donate { 14 | display: none; 15 | background-color: #fff; 16 | padding: 1.5rem; 17 | border-radius: 4%; 18 | border: 1px #eee solid; 19 | -moz-box-shadow: 10px 10px 5px #eee; /* 老的 Firefox */ 20 | box-shadow: 10px 10px 5px #eee; 21 | } 22 | 23 | .donate__provider-tabs, .donate__amount-tabs, .donate__qrcode { 24 | list-style: none; 25 | margin: 0; 26 | padding: 0; 27 | 28 | li { 29 | white-space: nowrap; 30 | } 31 | } 32 | 33 | .provider-tabs_item_active { 34 | color: blue; 35 | border-bottom: 2px blue solid; 36 | } 37 | 38 | .donate__provider-tabs li, .donate__amount-tabs li, .donate__qrcode li { 39 | display: inline-block; 40 | cursor: pointer; 41 | } 42 | 43 | 44 | .qrcode__img { 45 | width: 20rem; 46 | display: none; 47 | } 48 | 49 | .qrcode__img_show { 50 | display: block; 51 | } 52 | 53 | .donate__provider-tabs li { 54 | margin: 0 0.5rem; 55 | padding: 0.1rem 0.2rem; 56 | } 57 | 58 | .donate__amount-tabs li { 59 | margin: 0 0.5rem; 60 | } 61 | 62 | .amount-tabs_item_active { 63 | color: red; 64 | } -------------------------------------------------------------------------------- /blogproject/taskapp/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery, signals 4 | from django.apps import AppConfig, apps 5 | from django.conf import settings 6 | 7 | if not settings.configured: 8 | # set the default Django settings module for the 'celery' program. 9 | os.environ.setdefault( 10 | "DJANGO_SETTINGS_MODULE", "config.settings.production" 11 | ) # pragma: no cover 12 | 13 | app = Celery("blogproject") 14 | 15 | 16 | @signals.setup_logging.connect 17 | def setup_celery_logging(**kwargs): 18 | pass 19 | 20 | 21 | app.log.setup() 22 | 23 | # Using a string here means the worker will not have to 24 | # pickle the object when using Windows. 25 | # - namespace='CELERY' means all celery-related configuration keys 26 | # should have a `CELERY_` prefix. 27 | app.config_from_object("django.conf:settings", namespace="CELERY") 28 | 29 | 30 | class CeleryAppConfig(AppConfig): 31 | name = "blogproject.taskapp" 32 | verbose_name = "Celery Config" 33 | 34 | def ready(self): 35 | installed_apps = [app_config.name for app_config in apps.get_app_configs()] 36 | app.autodiscover_tasks(lambda: installed_apps, force=True) 37 | 38 | 39 | @app.task(bind=True) 40 | def debug_task(self): 41 | print(f"Request: {self.request!r}") # pragma: no cover 42 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const WebpackAssetsManifest = require('webpack-assets-manifest'); 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | 5 | module.exports = { 6 | outputDir: path.resolve(__dirname, './build'), 7 | publicPath: process.env.NODE_ENV == 'production' ? '/static/' : 'http://localhost:8080/', 8 | 9 | configureWebpack: (config) => { 10 | if (process.env.NODE_ENV == 'production') { 11 | config.externals = { 12 | vue: 'Vue', 13 | axios: 'axios', 14 | }; 15 | } 16 | 17 | config.entry = './src/main.ts'; 18 | // 为了暴露自定义的class组件 19 | config.output.library = { name: 'blogComponents', type: 'umd' }; 20 | 21 | config.plugins.push( 22 | new WebpackAssetsManifest({ 23 | entrypoints: true, 24 | output: 'manifest.json', 25 | writeToDisk: true, 26 | publicPath: true, 27 | }), 28 | new BundleAnalyzerPlugin({ 29 | analyzerPort: process.env.VUE_CLI_MODERN_BUILD ? 8888 : 9999, // Prevents build errors when running --modern 30 | analyzerMode: 'disabled', 31 | }), 32 | ); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /blogproject/courses/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from courses.models import Category, Course, Material 3 | from django.utils import timezone 4 | from factory.django import DjangoModelFactory 5 | from users.tests.factories import UserFactory 6 | 7 | 8 | class CategoryFactory(DjangoModelFactory): 9 | 10 | name = factory.Faker("name") 11 | rank = factory.Sequence(lambda n: n) 12 | 13 | class Meta: 14 | model = Category 15 | 16 | 17 | class CourseFactory(DjangoModelFactory): 18 | 19 | title = factory.Faker("sentence") 20 | slug = factory.Sequence(lambda n: f"course-slug-{n}") 21 | description = factory.Faker("paragraph") 22 | status = Course.STATUS.finished 23 | creator = factory.SubFactory(UserFactory) 24 | category = factory.SubFactory(CategoryFactory) 25 | rank = factory.Sequence(lambda n: n) 26 | 27 | class Meta: 28 | model = Course 29 | 30 | 31 | class MaterialFactory(DjangoModelFactory): 32 | 33 | title = factory.Faker("sentence") 34 | body = factory.Faker("paragraph") 35 | status = Material.STATUS.published 36 | pub_date = factory.LazyFunction(timezone.now) 37 | author = factory.SubFactory(UserFactory) 38 | rank = factory.Sequence(lambda n: n) 39 | course = factory.SubFactory(CourseFactory) 40 | 41 | class Meta: 42 | model = Material 43 | -------------------------------------------------------------------------------- /blogproject/favorites/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | from factory.django import DjangoModelFactory 4 | from favorites.models import Favorite, Issue 5 | from users.tests.factories import UserFactory 6 | 7 | 8 | class IssueFactory(DjangoModelFactory): 9 | number = factory.Sequence(lambda n: n) 10 | pub_date = factory.LazyFunction(timezone.now) 11 | description = factory.Faker("paragraph") 12 | creator = factory.SubFactory(UserFactory) 13 | 14 | class Meta: 15 | model = Issue 16 | 17 | @factory.post_generation 18 | def tags(self, create, extracted, **kwargs): 19 | if not create: 20 | return 21 | 22 | if extracted: 23 | for tag in extracted: 24 | self.tags.add(tag) 25 | 26 | 27 | class FavoriteFactory(DjangoModelFactory): 28 | issue = factory.SubFactory(IssueFactory) 29 | title = factory.Faker("sentence") 30 | description = factory.Faker("paragraph") 31 | url = factory.Faker("uri") 32 | rank = factory.Sequence(lambda n: n) 33 | 34 | class Meta: 35 | model = Favorite 36 | 37 | @factory.post_generation 38 | def tags(self, create, extracted, **kwargs): 39 | if not create: 40 | return 41 | 42 | if extracted: 43 | for tag in extracted: 44 | self.tags.add(tag) 45 | -------------------------------------------------------------------------------- /blogproject/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as AuthUserAdmin 3 | from django.utils.translation import gettext_lazy as _ 4 | from rest_framework.authtoken.models import Token 5 | 6 | from .models import User 7 | 8 | 9 | class UserAdmin(AuthUserAdmin): 10 | list_display = ["pk"] + list(AuthUserAdmin.list_display) 11 | fieldsets = ( 12 | (None, {"fields": ("username", "password")}), 13 | ( 14 | _("Personal info"), 15 | {"fields": ("first_name", "last_name", "email", "email_bound")}, 16 | ), 17 | ( 18 | _("Permissions"), 19 | { 20 | "fields": ( 21 | "is_active", 22 | "is_staff", 23 | "is_superuser", 24 | "groups", 25 | "user_permissions", 26 | ) 27 | }, 28 | ), 29 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 30 | ) 31 | 32 | 33 | @admin.register(Token) 34 | class MyTokenAdmin(admin.ModelAdmin): 35 | list_display = ("key", "user", "created") 36 | fields = ("user",) 37 | ordering = ("-created",) 38 | search_fields = ( 39 | "user__username", 40 | "user__email", 41 | ) 42 | 43 | 44 | admin.site.register(User, UserAdmin) 45 | -------------------------------------------------------------------------------- /blogproject/friendlinks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-09-19 03:57 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import model_utils.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='FriendLink', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created at')), 21 | ('modified_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified at')), 22 | ('site_name', models.CharField(max_length=100, verbose_name='site name')), 23 | ('site_link', models.URLField(verbose_name='site link')), 24 | ('rank', models.IntegerField(default=0, verbose_name='rank')), 25 | ], 26 | options={ 27 | 'verbose_name': 'friend link', 28 | 'verbose_name_plural': 'friend links', 29 | 'ordering': ['rank', 'created_at'], 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN apt-get update && apt-get install -y gettext python3-dev libpq-dev wget 6 | RUN wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | apt-key add - 7 | RUN bash -c "echo deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main >> /etc/apt/sources.list.d/pgdg.list" 8 | RUN apt-get update && apt-get -y install postgresql-client-12 9 | 10 | WORKDIR /app 11 | 12 | RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | POETRY_PREVIEW=1 python 13 | ENV PATH "/root/.poetry/bin:${PATH}" 14 | RUN poetry config virtualenvs.create false 15 | COPY poetry.lock pyproject.toml /app/ 16 | RUN poetry install --no-root --no-interaction 17 | 18 | COPY ./compose/production/django/entrypoint.sh /entrypoint.sh 19 | RUN sed -i 's/\r$//g' /entrypoint.sh 20 | RUN chmod +x /entrypoint.sh 21 | 22 | COPY ./compose/local/django/start.sh /start.sh 23 | RUN sed -i 's/\r//' /start.sh 24 | RUN chmod +x /start.sh 25 | 26 | COPY ./compose/local/django/celery/worker/start.sh /start-celeryworker.sh 27 | RUN sed -i 's/\r$//g' /start-celeryworker.sh 28 | RUN chmod +x /start-celeryworker.sh 29 | 30 | COPY ./compose/local/django/celery/beat/start.sh /start-celerybeat.sh 31 | RUN sed -i 's/\r$//g' /start-celerybeat.sh 32 | RUN chmod +x /start-celerybeat.sh 33 | 34 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /blogproject/core/views.py: -------------------------------------------------------------------------------- 1 | from braces.views import CsrfExemptMixin, JSONResponseMixin 2 | from django.core import mail 3 | from django.views.generic import View 4 | 5 | 6 | class SingleEmailDebugView(CsrfExemptMixin, JSONResponseMixin, View): 7 | """A view that sending a single email for debug purpose""" 8 | 9 | def post(self, request, *args, **kwargs): 10 | mail.send_mail( 11 | subject="Hello World", 12 | message="Let's say hello to the world.", 13 | from_email="helloworld@example.com", 14 | recipient_list=["test@example.com"], 15 | ) 16 | return self.render_json_response({"code": 1, "msg": "ok"}) 17 | 18 | 19 | class MassEmailDebugView(CsrfExemptMixin, JSONResponseMixin, View): 20 | """A view that sending mass emails for debug purpose""" 21 | 22 | def post(self, request, *args, **kwargs): 23 | emails = ( 24 | ( 25 | "Hey Man", 26 | "I'm The Dude! So that's what you call me.", 27 | "dude@aol.com", 28 | ["mr@lebowski.com"], 29 | ), 30 | ( 31 | "Dammit Walter", 32 | "Let's go bowlin'.", 33 | "dude@aol.com", 34 | ["wsobchak@vfw.org"], 35 | ), 36 | ) 37 | mail.send_mass_mail(emails) 38 | return self.render_json_response({"code": 1, "msg": "ok"}) 39 | -------------------------------------------------------------------------------- /frontend/src/style/_toc.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .toc-wrapper::-webkit-scrollbar { 4 | display: none; 5 | } 6 | 7 | .toc-wrapper:hover::-webkit-scrollbar { 8 | display: initial; 9 | width: 4px; 10 | transition: background .2s linear; 11 | } 12 | 13 | .toc-wrapper:hover::-webkit-scrollbar-thumb { 14 | background: rgba(0, 0, 0, .16); 15 | border-radius: 2px; 16 | } 17 | 18 | .toc { 19 | padding-right: $width-gap; 20 | 21 | > ul { 22 | list-style: none; 23 | padding: 0; 24 | margin: 0; 25 | 26 | > li:not(:last-child) { 27 | margin-bottom: $width-gap-half / 2; 28 | } 29 | } 30 | } 31 | 32 | .toc, .toc-mobile { 33 | //font-size: $font-size-small; 34 | 35 | > ul { 36 | list-style: none; 37 | padding: 0; 38 | margin: 0; 39 | 40 | > li:not(:last-child) { 41 | margin-bottom: $width-gap-half; 42 | } 43 | } 44 | 45 | a { 46 | color: $color-text-muted; 47 | 48 | &:hover { 49 | color: darken($color-text-muted, 100%); 50 | } 51 | } 52 | 53 | .toc-title { 54 | margin: $width-gap-half 0; 55 | } 56 | 57 | .toc-item-icon { 58 | display: inline-block; 59 | color: $color-text-muted; 60 | min-width: $width-gap; 61 | width: $width-gap; 62 | } 63 | 64 | .material-toc { 65 | list-style: none; 66 | font-size: $font-size-small; 67 | padding-left: 3rem; 68 | margin-top: $width-gap-half / 2; 69 | display: none; 70 | } 71 | } -------------------------------------------------------------------------------- /blogproject/templates/search/_search_entry_list_item.html: -------------------------------------------------------------------------------- 1 | {% load highlight %} 2 | {% load comments %} 3 | 4 | -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_pagination.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 |
3 | 14 | 15 | {# {% if page_obj.has_previous %}#} 16 | {# #} 18 | {# {% endif %}#} 19 | {# #} 20 | {# {% if page_obj.has_next %}#} 21 | {# #} 23 | {# {% endif %}#} 24 |
25 | {% endif %} -------------------------------------------------------------------------------- /blogproject/templates/courses/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block header %} 5 | {% with show_trigger=True %} 6 | {{ block.super }} 7 | {% endwith %} 8 | {% endblock header %} 9 | 10 | {% block content %} 11 | {% include 'courses/inclusions/_sidebar_desk.html' %} 12 |
13 |
14 |
15 |
16 |
17 |
18 | {% block alerts %} 19 |
20 | {% for alert in course.alerts.all %} 21 |
{{ alert.text|safe }}
22 | {% endfor %} 23 |
24 | {% endblock alerts %} 25 | {% block main %} 26 | {% block article %}{% endblock article %} 27 | {% include 'inclusions/_donate.html' %} 28 | {% include 'courses/inclusions/_prev_next.html' %} 29 |
30 | {% block comment %}{% endblock comment %} 31 | {% endblock main %} 32 |
33 |
34 |
35 |
36 |
37 | {% endblock content %} 38 | 39 | {% block sidebar_mobile %} 40 | {% include 'courses/inclusions/_sidebar_mobile.html' %} 41 | {% endblock sidebar_mobile %} -------------------------------------------------------------------------------- /frontend/src/style/_sidebar.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .blog-aside-trigger { 4 | cursor: pointer; 5 | margin-right: $width-gap-double; 6 | } 7 | 8 | .blog-aside-mobile-wrapper { 9 | display: block; 10 | background-color: $color-background; 11 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | height: 100%; 16 | width: 70%; 17 | max-width: 70%; 18 | margin-left: -100%; 19 | transition: margin-left 500ms; 20 | z-index: 900; 21 | &.show { 22 | margin-left: 0; 23 | } 24 | 25 | .sidebar-header { 26 | height: $size-base * 5; 27 | border-bottom: $width-border solid $color-border; 28 | padding: 0 $width-gap; 29 | 30 | .sidebar-title { 31 | color: $color-text; 32 | font-size: $font-size-h5; 33 | } 34 | } 35 | 36 | .sidebar-body { 37 | padding: $width-gap; 38 | } 39 | } 40 | 41 | // .overlay { 42 | // display: none; 43 | // position: fixed; 44 | // top: 0; 45 | // bottom: 0; 46 | // left: 0; 47 | // right: 0; 48 | // background-color: $color-background-faded; 49 | // opacity: 0; 50 | // transition: opacity 500ms; 51 | // z-index: 800; 52 | // } 53 | body.dropback::before { 54 | position: fixed; 55 | top: 0; 56 | left: 0; 57 | z-index: 800; 58 | width: 100vw; 59 | height: 100vh; 60 | content: ""; 61 | background-color: rgba(0,0,0,.5); 62 | } 63 | -------------------------------------------------------------------------------- /devops/ansible/roles/pyenv/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #- name: Install prerequisites 3 | # become: yes 4 | # become_method: sudo 5 | # ansible.builtin.package: 6 | # name: libedit-dev 7 | # state: present 8 | # 9 | #- name: Install pyenv 10 | # ansible.builtin.shell: 11 | # cmd: "curl -s -S -L https://raw.fastgit.org/pyenv/pyenv-installer/master/bin/pyenv-installer | sed 's+https://github.com+https://hub.fastgit.org+g' | bash" 12 | # 13 | #- name: Modify .profile 14 | # lineinfile: 15 | # dest: "~/.profile" 16 | # state: present 17 | # line: "{{ item }}" 18 | # loop: 19 | # - 'export PYENV_ROOT="$HOME/.pyenv"' 20 | # - 'export PATH="$PYENV_ROOT/bin:$PATH"' 21 | # - 'eval "$(pyenv init --path)"' 22 | # 23 | #- name: Install build dependencies 24 | # become: yes 25 | # become_method: sudo 26 | # ansible.builtin.package: 27 | # state: present 28 | # name: 29 | # - make 30 | # - build-essential 31 | # - libssl-dev 32 | # - zlib1g-dev 33 | # - libbz2-dev 34 | # - libreadline-dev 35 | # - libsqlite3-dev 36 | # - wget 37 | # - curl 38 | # - llvm 39 | # - libncursesw5-dev 40 | # - xz-utils 41 | # - tk-dev 42 | # - libxml2-dev 43 | # - libxmlsec1-dev 44 | # - libffi-dev 45 | # - liblzma-dev 46 | 47 | #- name: Install python 48 | # ansible.builtin.shell: 49 | # cmd: ". ~/.profile && pyenv install 3.8.9" 50 | 51 | - name: Set global python 52 | ansible.builtin.shell: 53 | cmd: ". ~/.profile && pyenv global 3.8.9" -------------------------------------------------------------------------------- /devops/ansible/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | project_path: "~/apps/django-blog-project" 2 | 3 | # Database 4 | db_name: !vault | 5 | $ANSIBLE_VAULT;1.1;AES256 6 | 61353939316536623932633037353163343362366633316630623333313536353161376639366639 7 | 6634383037396136383635303661376238376130336336640a323863373261653230333835363437 8 | 66323232386138313063323263346534363235646262363565653932386238346465633536663838 9 | 6534363333376437370a303233383162306338303866353336616164663362656564356635396564 10 | 3437 11 | db_user: !vault | 12 | $ANSIBLE_VAULT;1.1;AES256 13 | 35353530323066336362333130313838333833666333663835666636303333633261613536656566 14 | 3338626561306133663839666266623330306135383031630a633437333862306133356633396530 15 | 34643532363633346631643338393930646236396465386561353863343161616163613337383839 16 | 6630313734346265660a333664663837386331323733363439353736346665363061626431623534 17 | 3231 18 | db_pwd: !vault | 19 | $ANSIBLE_VAULT;1.1;AES256 20 | 64393031646133613035656562346137646332303231383231303362313633623333383561326432 21 | 3530323831323831353464333037386238633866396431320a376630613833376462373466326233 22 | 63373834633339306339626663326333643638346638356563363538656134653739313538643835 23 | 3030393566633865660a363038326334633861643533633665383135323939366131663332373263 24 | 65666366353039656132643662326139613037303361643730393964626435626236 25 | -------------------------------------------------------------------------------- /blogproject/comments/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from users.serializers import UserSerializer 3 | 4 | from .models import BlogComment 5 | 6 | 7 | class CommentSerializer(serializers.ModelSerializer): 8 | user = UserSerializer(read_only=True) 9 | parent_user = serializers.SerializerMethodField() 10 | comment_html = serializers.CharField() 11 | 12 | class Meta: 13 | model = BlogComment 14 | fields = [ 15 | "id", 16 | "user", 17 | "parent", 18 | "parent_user", 19 | "comment", 20 | "comment_html", 21 | "submit_date", 22 | ] 23 | 24 | extra_kwargs = { 25 | "submit_date": {"format": "%Y-%m-%d %H:%M:%S"}, 26 | "comment": {"write_only": True}, 27 | } 28 | 29 | def get_parent_user(self, obj): 30 | if obj.parent: 31 | return UserSerializer(obj.parent.user).data 32 | 33 | 34 | class TreeCommentSerializer(serializers.ModelSerializer): 35 | descendants = CommentSerializer(many=True, read_only=True) 36 | user = UserSerializer(read_only=True) 37 | comment_html = serializers.CharField() 38 | 39 | class Meta: 40 | model = BlogComment 41 | fields = [ 42 | "id", 43 | "comment_html", 44 | "submit_date", 45 | "user", 46 | "parent", 47 | "descendants", 48 | ] 49 | extra_kwargs = {"submit_date": {"format": "%Y-%m-%d %H:%M:%S"}} 50 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | from .common import * # noqa 2 | 3 | DEBUG = True 4 | SECRET_KEY = "fake-secret-key-for-test" 5 | ALLOWED_HOSTS = ["*"] 6 | 7 | INSTALLED_APPS += [ # noqa 8 | "core.tests", 9 | ] 10 | 11 | 12 | # CACHES 13 | # ------------------------------------------------------------------------------ 14 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 15 | CACHES = { 16 | "default": { 17 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 18 | "LOCATION": "", 19 | } 20 | } 21 | 22 | 23 | # PASSWORDS 24 | # ------------------------------------------------------------------------------ 25 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 26 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 27 | 28 | 29 | # EMAIL 30 | # ------------------------------------------------------------------------------ 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 32 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 33 | 34 | 35 | # DATABASE 36 | DATABASES = { 37 | "default": { 38 | "ENGINE": "django.db.backends.sqlite3", 39 | "NAME": ":memory:", 40 | "ATOMIC_REQUESTS": True, 41 | } 42 | } 43 | 44 | ADMINS = [("admin", "admin@example.com")] 45 | MANAGERS = ADMINS 46 | LANGUAGE_CODE = "en-us" 47 | 48 | # todo: more elegant way to generate manifest file for test 49 | WEBPACK_LOADER["MANIFEST_FILE"] = str( # noqa F405 50 | ROOT_DIR / "frontend" / "manifest-test.json" # noqa F405 51 | ) 52 | -------------------------------------------------------------------------------- /frontend/src/style/_navbar.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .navbar { 4 | width: 100%; 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | .navbar-logo { 11 | font-size: $font-size-h4; 12 | color: $color-text; 13 | line-height: 1; 14 | } 15 | 16 | .navbar-item { 17 | display: inline-flex; 18 | color: $color-text-muted; 19 | margin-left: $width-gap-double; 20 | 21 | &:hover { 22 | color: $color-primary; 23 | } 24 | } 25 | } 26 | 27 | .search-form, .search-form-mobile { 28 | input[type=search] { 29 | background-color: transparent; 30 | border: none; 31 | padding-left: 0.5rem; 32 | outline: 0; 33 | } 34 | } 35 | 36 | .search-form { 37 | input[type=search] { 38 | border-left: $width-border * 2 solid $color-border; 39 | font-size: $font-size-small; 40 | 41 | &:focus { 42 | border-left: $width-border* 2 solid $color-primary; 43 | } 44 | } 45 | 46 | button[type=submit] { 47 | cursor: pointer; 48 | border: 0; 49 | background-color: transparent; 50 | 51 | &:focus { 52 | border: 0; 53 | outline: 0; 54 | } 55 | } 56 | } 57 | 58 | .search-form-mobile-wrapper { 59 | display: none; 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | height: 100%; 64 | width: 100%; 65 | background-color: $color-background; 66 | z-index: 100; 67 | 68 | .search-form-mobile { 69 | width: 100%; 70 | padding: $width-gap $width-gap-half; 71 | 72 | input[type=search] { 73 | width: 100%; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /blogproject/courses/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import pytest 4 | from courses.templatetags.courses_extras import show_course_toc 5 | from django.core.cache import cache 6 | from django.core.cache.utils import make_template_fragment_key 7 | from django.utils import timezone 8 | from freezegun import freeze_time 9 | 10 | from .factories import MaterialFactory 11 | 12 | 13 | @pytest.mark.django_db 14 | def test_generate_toc_if_no_cache(course): 15 | MaterialFactory(course=course) 16 | MaterialFactory(course=course) 17 | context = show_course_toc(course) 18 | material_list = context["material_list"] 19 | assert all("toc" in m for m in material_list) 20 | 21 | 22 | @pytest.mark.django_db 23 | def test_do_not_generate_toc_if_cache(course): 24 | key = make_template_fragment_key("course_toc", [course.pk]) 25 | cache.set(key, "cached toc", timeout=300) 26 | assert cache.get(key) is not None 27 | context = show_course_toc(course) 28 | material_list = context["material_list"] 29 | assert all("toc" not in m for m in material_list) 30 | 31 | 32 | @pytest.mark.django_db 33 | def test_generate_toc_if_cache_expired(course): 34 | key = make_template_fragment_key("course_toc", [course.pk]) 35 | cache.set(key, "cached toc", timeout=300) 36 | assert cache.get(key) is not None 37 | 38 | with freeze_time(timezone.now() + timedelta(seconds=301)): 39 | context = show_course_toc(course) 40 | material_list = context["material_list"] 41 | assert all("toc" in m for m in material_list) 42 | -------------------------------------------------------------------------------- /blogproject/scripts/fake/_comments.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from blog.models import Post 4 | from comments.tests.factories import BlogCommentFactory 5 | from courses.models import Material 6 | from django.contrib.sites.models import Site 7 | 8 | 9 | def run(): 10 | first_post = Post.index.all().order_by("-pinned", "-pub_date").first() 11 | site = Site.objects.get(name="example.com") 12 | for _ in range(30): 13 | root_comment = BlogCommentFactory( 14 | is_public=True, is_removed=False, site=site, content_object=first_post 15 | ) 16 | children_size = random.randint(3, 10) 17 | for _ in range(children_size): 18 | root_comment = BlogCommentFactory( 19 | is_public=True, 20 | is_removed=False, 21 | site=site, 22 | content_object=first_post, 23 | parent=root_comment, 24 | ) 25 | 26 | first_material = Material.index.all().order_by("-pub_date").first() 27 | for _ in range(30): 28 | root_comment = BlogCommentFactory( 29 | is_public=True, is_removed=False, site=site, content_object=first_material 30 | ) 31 | children_size = random.randint(3, 10) 32 | for _ in range(children_size): 33 | root_comment = BlogCommentFactory( 34 | is_public=True, 35 | is_removed=False, 36 | site=site, 37 | content_object=first_material, 38 | parent=root_comment, 39 | ) 40 | 41 | print("Comments created.") 42 | -------------------------------------------------------------------------------- /compose/production/statusok/config/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": { 3 | "slack": { 4 | "channel": "#test", 5 | "username": "statusok", 6 | "channelWebhookURL": "https://hooks.slack.com/services/channel/webhook/url" 7 | } 8 | }, 9 | "requests": [ 10 | { 11 | "url": "http://example.com/watchman/", 12 | "urlParams": { 13 | "watchman-token": "watchman-token", 14 | "check": "watchman.checks.email" 15 | }, 16 | "requestType": "GET", 17 | "checkEvery": 28800, 18 | "responseCode": 200, 19 | "responseTime": 5000 20 | }, 21 | { 22 | "url": "http://example.com/watchman/", 23 | "urlParams": { 24 | "watchman-token": "watchman-token", 25 | "check": "watchman.checks.databases" 26 | }, 27 | "requestType": "GET", 28 | "checkEvery": 60, 29 | "responseCode": 200, 30 | "responseTime": 1000 31 | }, 32 | { 33 | "url": "http://example.com/watchman/", 34 | "urlParams": { 35 | "watchman-token": "watchman-token", 36 | "check": "watchman.checks.caches" 37 | }, 38 | "requestType": "GET", 39 | "checkEvery": 60, 40 | "responseCode": 200, 41 | "responseTime": 1000 42 | }, 43 | { 44 | "url": "http://example.com/watchman/", 45 | "urlParams": { 46 | "watchman-token": "watchman-token", 47 | "check": "watchman.checks.storage" 48 | }, 49 | "requestType": "GET", 50 | "checkEvery": 60, 51 | "responseCode": 200, 52 | "responseTime": 1000 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /blogproject/templates/blog/inclusions/_entry_list_item.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% if entry.type == 'p' %} 4 |

{% if entry.pinned %} 5 | [置顶]{% endif %} {{ entry.title }}

6 | {% elif entry.type == 'm' %} 7 |

{{ entry.title }} 9 |

10 | {% endif %} 11 | 28 | {% if entry.brief %}

{{ entry.brief }}

{% endif %} 29 |
30 |
-------------------------------------------------------------------------------- /blogproject/alerts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 06:09 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import model_utils.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Alert", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ( 28 | "created", 29 | model_utils.fields.AutoCreatedField( 30 | default=django.utils.timezone.now, 31 | editable=False, 32 | verbose_name="created", 33 | ), 34 | ), 35 | ( 36 | "modified", 37 | model_utils.fields.AutoLastModifiedField( 38 | default=django.utils.timezone.now, 39 | editable=False, 40 | verbose_name="modified", 41 | ), 42 | ), 43 | ("text", models.TextField(verbose_name="text")), 44 | ], 45 | options={"verbose_name": "alert", "verbose_name_plural": "alerts",}, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /frontend/src/style/_course.scss: -------------------------------------------------------------------------------- 1 | .category { 2 | font-size: 1.8rem; 3 | font-weight: 600; 4 | } 5 | 6 | .category__logo { 7 | width: 1.8rem; 8 | margin-right: 0.5rem; 9 | } 10 | 11 | .category:before, .category:after { 12 | content: ""; 13 | width: 8rem; 14 | border-top: 0.1rem #b2bbbe solid; 15 | display: inline-block; 16 | vertical-align: middle; 17 | } 18 | 19 | .category:before { 20 | margin-right: 10px; 21 | } 22 | 23 | .category:after { 24 | margin-left: 10px; 25 | } 26 | 27 | .course { 28 | margin-bottom: 1.5rem; 29 | } 30 | 31 | .course__img { 32 | border-radius: 3px; 33 | } 34 | 35 | .course__title { 36 | margin-top: 0.5rem; 37 | } 38 | 39 | .course__progress { 40 | } 41 | 42 | .course__progress_writing { 43 | color: #41ae3c; 44 | } 45 | 46 | .course__progress_finished { 47 | color: #ee3f4d; 48 | } 49 | 50 | .course__progress:before { 51 | content: '·'; 52 | margin: 0 0.3rem; 53 | font-weight: 600; 54 | color: #b2bbbe; 55 | } 56 | 57 | .course__meta { 58 | margin-top: 0.5rem; 59 | } 60 | 61 | .course__level { 62 | } 63 | 64 | .course__level_elementary { 65 | color: #4c1f24; 66 | } 67 | 68 | .course__level_intermediate { 69 | color: #1661ab; 70 | } 71 | 72 | .course__level_advanced { 73 | color: #f97d1c; 74 | } 75 | 76 | .course__views { 77 | margin-left: 0.8rem; 78 | } 79 | 80 | .course__brief { 81 | margin-top: 0.5rem; 82 | color: #93999f; 83 | word-break: break-all; 84 | display: -webkit-box; 85 | -webkit-line-clamp: 2; 86 | -webkit-box-orient: vertical; 87 | overflow: hidden; 88 | } 89 | 90 | .course__meta, .course__brief { 91 | font-size: 1.2rem; 92 | } -------------------------------------------------------------------------------- /blogproject/scripts/fake/_clean_db.py: -------------------------------------------------------------------------------- 1 | from alerts.models import Alert 2 | from blog.models import Category as PostCategory 3 | from blog.models import Post 4 | from comments.models import BlogComment 5 | from courses.models import Category as CourseCategory 6 | from courses.models import Course, Material 7 | from django.conf import settings 8 | from django.contrib.auth import get_user_model 9 | from favorites.models import Favorite, Issue 10 | from friendlinks.models import FriendLink 11 | from newsletters.models import Subscription 12 | 13 | User = get_user_model() 14 | 15 | 16 | def run(): 17 | if not settings.DEBUG: 18 | warning_msg = ( 19 | "You are not in development environment. " 20 | "This script will DELETE ALL DATA in your database. " 21 | "If you really want to continue this script, please input 'yEs'. " 22 | "Make sure you know what you are doing!" 23 | ) 24 | print(warning_msg) 25 | prompt = input("Please input 'yEs' to continue") 26 | if prompt != "yEs": 27 | print("Unexpected input, return!") 28 | return 29 | 30 | User.objects.all().delete() 31 | Alert.objects.all().delete() 32 | BlogComment.objects.all().delete() 33 | PostCategory.objects.all().delete() 34 | Post.objects.all().delete() 35 | CourseCategory.objects.all().delete() 36 | Course.objects.all().delete() 37 | Material.objects.all().delete() 38 | Issue.objects.all().delete() 39 | Favorite.objects.all().delete() 40 | FriendLink.objects.all().delete() 41 | Subscription.objects.all().delete() 42 | print("Database cleaned.") 43 | -------------------------------------------------------------------------------- /frontend/src/scripts/toc.ts: -------------------------------------------------------------------------------- 1 | // (function () { 2 | // document ready 3 | 4 | // $('.toc-item-icon i').on('click', function (e) { 5 | // let $this = $(this); 6 | // let $tocItem = $this.closest('li.material-title'); 7 | // let $subToc = $tocItem.find('ul.material-toc'); 8 | // 9 | // if ($subToc.length <= 0) { 10 | // return; 11 | // } 12 | // 13 | // if ($subToc.is(':hidden')) { 14 | // $this.removeClass('remixicon-arrow-right-s-line'); 15 | // $this.addClass('remixicon-arrow-down-s-line'); 16 | // $subToc.show(); 17 | // } else { 18 | // $this.removeClass('remixicon-arrow-down-s-line'); 19 | // $this.addClass('remixicon-arrow-right-s-line'); 20 | // $subToc.hide(); 21 | // } 22 | // }); 23 | 24 | // function closest(el, selector) { 25 | // const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 26 | // 27 | // while (el) { 28 | // if (matchesSelector.call(el, selector)) { 29 | // break; 30 | // } 31 | // el = el.parentElement; 32 | // } 33 | // return el; 34 | // } 35 | // 36 | // const tocIcon = document.querySelector('.toc-item-icon i'); 37 | // 38 | // console.log(tocIcon); 39 | // 40 | // tocIcon?.addEventListener('click', function (events) { 41 | // console.log('toc icon click'); 42 | // const tocItem = closest(tocIcon, 'li.material-title'); 43 | // console.log('tocItem', tocItem); 44 | // }); 45 | // 46 | // })(); -------------------------------------------------------------------------------- /frontend/src/scripts/offcanvas.ts: -------------------------------------------------------------------------------- 1 | class Offcanvas { 2 | private readonly _trigger: HTMLElement; 3 | private readonly _element: HTMLElement; 4 | private _isShown: boolean; 5 | 6 | constructor(element: HTMLElement, trigger: HTMLElement) { 7 | this._trigger = trigger; 8 | this._isShown = false; 9 | this._element = element; 10 | this._init(); 11 | } 12 | 13 | _init(): void { 14 | this._trigger && this._trigger.addEventListener('click', this.show.bind(this), false); 15 | // 这里必须使用捕获模式,否则 _trigger 元素触发 click 事件后会立即触发 document.body 绑定的事件 16 | document.addEventListener('click', this.hide.bind(this),true); 17 | this.toggle = this.toggle.bind(this); 18 | } 19 | 20 | toggle(): void { 21 | // @ts-ignore 22 | this._isShown ? this.hide() : this.show(); 23 | } 24 | 25 | show(): void { 26 | if (this._isShown) { 27 | return; 28 | } 29 | this._isShown = true; 30 | document.body.classList.add('offcanvas-backdrop'); 31 | this._element.classList.add('show'); 32 | } 33 | 34 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 35 | hide(event: MouseEvent): void { 36 | if (!this._isShown) { 37 | return; 38 | } 39 | 40 | // @ts-ignore 41 | if (!this._element.contains(event.target) && event.target !== this._element) { 42 | this._isShown = false; 43 | this._element.classList.remove('show'); 44 | document.body.classList.remove('offcanvas-backdrop'); 45 | } 46 | } 47 | } 48 | 49 | export default Offcanvas; 50 | -------------------------------------------------------------------------------- /blogproject/newsletters/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-06-06 10:29 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import model_utils.fields 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Subscription', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created at')), 24 | ('modified_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified at')), 25 | ('email', models.EmailField(max_length=254, verbose_name='email')), 26 | ('confirmed', models.BooleanField(default=False, verbose_name='confirmed')), 27 | ('active', models.BooleanField(default=False, verbose_name='active')), 28 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='user')), 29 | ], 30 | options={ 31 | 'verbose_name': 'subscription', 32 | 'verbose_name_plural': 'subscriptions', 33 | 'ordering': ['-created_at'], 34 | }, 35 | ), 36 | ] 37 | --------------------------------------------------------------------------------