├── .DS_Store
├── .env.example
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── pylint.yml
├── .gitignore
├── .pylintrc
├── .vscode
├── launch.json
└── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── bloggy
├── .DS_Store
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── admin.py
│ ├── category_admin.py
│ ├── comment_admin.py
│ ├── course_admin.py
│ ├── misc_admin.py
│ ├── page_admin.py
│ ├── post_admin.py
│ ├── quiz_admin.py
│ ├── subscriber_admin.py
│ └── user_admin.py
├── apps.py
├── context_processors.py
├── forms
│ ├── __init__.py
│ ├── comment_form.py
│ ├── edit_profile_form.py
│ ├── signup_form.py
│ └── update_password_form.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── .DS_Store
│ │ ├── __init__.py
│ │ ├── runseed.py
│ │ ├── seed_categories.py
│ │ ├── seed_pages.py
│ │ ├── seed_posts.py
│ │ ├── seed_redirectrules.py
│ │ ├── seed_users.py
│ │ └── update_category_count.py
├── middleware
│ ├── __init__.py
│ ├── redirect.py
│ └── slash_middleware.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_rename_description_category_excerpt.py
│ ├── 0003_rename_logo_category_thumbnail_page_thumbnail.py
│ ├── 0004_remove_redirectrule_is_regx.py
│ └── __init__.py
├── models.py
├── models
│ ├── .DS_Store
│ ├── __init__.py
│ ├── categories.py
│ ├── comment.py
│ ├── course.py
│ ├── media.py
│ ├── mixin
│ │ ├── Content.py
│ │ ├── ResizeImageMixin.py
│ │ ├── SeoAware.py
│ │ ├── __init__.py
│ │ └── updatable.py
│ ├── option.py
│ ├── page.py
│ ├── post.py
│ ├── post_actions.py
│ ├── quizzes.py
│ ├── redirect_rule.py
│ ├── subscriber.py
│ ├── user.py
│ └── verification_token.py
├── services
│ ├── __init__.py
│ ├── account_manager.py
│ ├── email_service.py
│ ├── gravtar.py
│ ├── post_service.py
│ ├── quiz_service.py
│ ├── seo_service.py
│ ├── sitemaps.py
│ ├── token_generator.py
│ ├── token_service.py
│ └── url_shortener.py
├── settings.py
├── shortcodes
│ ├── __init__.py
│ └── parser.py
├── signals.py
├── storage_backends.py
├── templates
│ ├── admin
│ │ └── base_site.html
│ ├── auth
│ │ ├── login.html
│ │ └── register.html
│ ├── base-with-header-footer.html
│ ├── base-with-header.html
│ ├── base.html
│ ├── email
│ │ ├── acc_active_email.html
│ │ ├── contact_form.tpl
│ │ ├── login_code_email.html
│ │ ├── newsletter_verification_token.html
│ │ ├── weekly_updates_email.html
│ │ └── welcome_email.html
│ ├── errors
│ │ ├── 404.html
│ │ ├── 500.html
│ │ ├── error_page_links.html
│ │ └── search_widget.html
│ ├── forms
│ │ └── widgets
│ │ │ └── non_clearable_imagefield.html
│ ├── pages
│ │ ├── about.html
│ │ ├── archive
│ │ │ ├── categories.html
│ │ │ ├── courses.html
│ │ │ ├── posts.html
│ │ │ └── quizzes.html
│ │ ├── authors.html
│ │ ├── contact.html
│ │ ├── home.html
│ │ ├── page.html
│ │ ├── search_result.html
│ │ ├── single
│ │ │ ├── course.html
│ │ │ ├── lesson.html
│ │ │ ├── post-naked.html
│ │ │ ├── post.html
│ │ │ └── quiz.html
│ │ └── user.html
│ ├── partials
│ │ ├── article_bookmark_row_list.html
│ │ ├── article_meta_container.html
│ │ ├── article_row_list.html
│ │ ├── article_row_mini_grid.html
│ │ ├── author_widget.html
│ │ ├── category_archive_banner.html
│ │ ├── course_grid_column.html
│ │ ├── course_row_grid.html
│ │ ├── dashboard_menu.html
│ │ ├── footer.html
│ │ ├── github_widget.html
│ │ ├── header.html
│ │ ├── home_article_breadcrumb.html
│ │ ├── home_lesson_breadcrumb.html
│ │ ├── home_widget_contribute_cta.html
│ │ ├── home_widget_course_toc.html
│ │ ├── home_widget_join_cta.html
│ │ ├── lesson_single_toc.html
│ │ ├── newsletter.html
│ │ ├── pages_menu.html
│ │ ├── paging.html
│ │ ├── post_list_item.html
│ │ ├── social_share.html
│ │ ├── theme_switch.html
│ │ ├── toc_widget.html
│ │ ├── user_profile_social_media_links.html
│ │ └── video_widget.html
│ ├── profile
│ │ ├── edit_profile.html
│ │ ├── user_bookmarks.html
│ │ └── user_dashboard.html
│ ├── seo
│ │ ├── article_jsonld.html
│ │ ├── cookie-consent.html
│ │ ├── course_jsonld.html
│ │ ├── footer_scripts.html
│ │ ├── header_scripts.html
│ │ ├── lesson_jsonld.html
│ │ └── seotags.html
│ ├── sitemap_template.html
│ ├── social_share
│ │ ├── copy_script.html
│ │ ├── copy_to_clipboard.html
│ │ ├── post_to_facebook.html
│ │ ├── post_to_linkedin.html
│ │ ├── post_to_twitter.html
│ │ └── send_email.html
│ └── widgets
│ │ ├── categories.html
│ │ ├── related_posts.html
│ │ └── related_quiz_widget.html
├── templatetags
│ ├── __init__.py
│ ├── custom_widgets.py
│ ├── define_action.py
│ ├── shortcodes_filters.py
│ └── social_share.py
├── urls.py
├── utils
│ ├── __init__.py
│ └── string_utils.py
├── views
│ ├── __init__.py
│ ├── account.py
│ ├── category_view.py
│ ├── courses_view.py
│ ├── edit_profile_view.py
│ ├── error_views.py
│ ├── login.py
│ ├── pages.py
│ ├── posts.py
│ ├── quizzes_view.py
│ ├── register.py
│ ├── rss.py
│ ├── search.py
│ ├── user.py
│ └── user_collections.py
└── wsgi.py
├── bloggy_api
├── .DS_Store
├── __init__.py
├── apps.py
├── exception
│ ├── __init__.py
│ ├── not_found_exception.py
│ ├── rest_exception_handler.py
│ └── unauthorized_access.py
├── migrations
│ └── __init__.py
├── pagination.py
├── serializers.py
├── service
│ └── recaptcha.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── articles_api.py
│ ├── bookmark_api.py
│ ├── category_api.py
│ ├── comments_api_view.py
│ ├── course_api.py
│ ├── newsletter_api.py
│ ├── quizzes_api.py
│ ├── user_api.py
│ └── vote_api.py
├── bloggy_frontend
├── assets
│ ├── caret-down.svg
│ ├── caret-filled-down.svg
│ ├── caret-filled-up.svg
│ ├── caret-up.svg
│ ├── default-banner.png
│ ├── default_avatar.png
│ ├── eye.svg
│ ├── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── apple-touch-icon.png
│ │ └── favicon.ico
│ ├── full-logo-light.png
│ ├── full-logo.png
│ ├── github-bg.png
│ ├── hero-img.png
│ ├── home-bg-image.png
│ ├── icon-blue.png
│ ├── icon-dark.png
│ ├── icon-gray.png
│ ├── index__hero.png
│ ├── like.svg
│ ├── liked.svg
│ ├── logo-dark.png
│ ├── logo-icon.png
│ ├── logo.png
│ ├── love-selected.svg
│ ├── love.svg
│ ├── playbutton.png
│ ├── reply.svg
│ └── resources
│ │ ├── base-64.gif
│ │ ├── file-yaml-o.svg
│ │ ├── http-status.png
│ │ ├── icons8-links-66.png
│ │ ├── internet.gif
│ │ ├── ip-scanner.gif
│ │ ├── json.gif
│ │ ├── links.gif
│ │ ├── mime.gif
│ │ ├── url-encode.png
│ │ └── xml.gif
├── js
│ ├── app
│ │ ├── bookmark.js
│ │ ├── cookie.js
│ │ ├── copyCode.js
│ │ ├── heading.js
│ │ ├── newsletter.js
│ │ ├── toast.js
│ │ └── toc.js
│ ├── components.js
│ ├── html-table-of-contents.js
│ ├── index.js
│ └── vue
│ │ ├── Api.js
│ │ ├── Bookmark.vue
│ │ ├── CookieConsent.vue
│ │ ├── LikeButton.vue
│ │ ├── Newsletter.vue
│ │ ├── Testimonials.vue
│ │ ├── disqus
│ │ ├── CommentForm.vue
│ │ ├── CommentItem.vue
│ │ ├── Comments.vue
│ │ └── ContactForm.vue
│ │ └── quiz
│ │ ├── QuestionKindBinary.vue
│ │ ├── QuestionKindMultiple.vue
│ │ ├── QuestionState.js
│ │ ├── QuestionType.js
│ │ ├── QuizLandingComponent.vue
│ │ ├── QuizState.js
│ │ ├── QuizSummaryPage.vue
│ │ └── Quizlet.vue
├── package-lock.json
├── package.json
├── postcss.config.js
├── sass
│ ├── _customVariables.scss
│ ├── _utils.scss
│ ├── bootstrap.scss
│ ├── content
│ │ ├── _category_widget.scss
│ │ ├── _code.scss
│ │ ├── _comments.scss
│ │ ├── _cookie-consent.scss
│ │ ├── _courses.scss
│ │ ├── _darkMode.scss
│ │ ├── _hero.scss
│ │ ├── _highlightjs.scss
│ │ ├── _highlightjsDark.scss
│ │ ├── _home.scss
│ │ ├── _listGroup.scss
│ │ ├── _login.scss
│ │ ├── _newsletter.scss
│ │ ├── _pageContent.scss
│ │ ├── _pagination.scss
│ │ ├── _print.scss
│ │ ├── _questions.scss
│ │ ├── _quiz.scss
│ │ ├── _search.scss
│ │ ├── _social.scss
│ │ ├── _table.scss
│ │ ├── _toast.scss
│ │ ├── _toc.scss
│ │ ├── _update-profile.scss
│ │ ├── _voting.scss
│ │ ├── _widget.scss
│ │ ├── model.scss
│ │ ├── post.scss
│ │ └── quizlet.scss
│ ├── style.scss
│ └── vendor
│ │ ├── _alert.scss
│ │ ├── _author.scss
│ │ ├── _bookmark_button.scss
│ │ ├── _button.scss
│ │ ├── _card.scss
│ │ ├── _dark_mode_toggle.scss
│ │ ├── _footer.scss
│ │ ├── _header.scss
│ │ ├── _navbar.scss
│ │ ├── _timeline.scss
│ │ └── _toast.scss
└── webpack.config.js
├── demo_content
├── categories.csv
├── pages.csv
├── posts.csv
├── redirect_rules.csv
└── users.csv
├── manage.py
├── media
└── .DS_Store
├── requirements.txt
├── runtime.txt
├── seo_settings.json
└── vetur.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/.DS_Store
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Secret Key hash
2 | SECRET_KEY=
3 | DEBUG=True
4 | ALLOWED_HOSTS=127.0.0.1,localhost
5 |
6 | # Website details
7 | SITE_URL=SITE URL
8 | SITE_TITLE=SITE TITLE
9 | SITE_TAGLINE=SITE TAGLINE
10 | SITE_DESCRIPTION=YOUR SITE DESCRIPTION
11 | ASSETS_DOMAIN=YOUR ASSETS DOMAIN
12 | SITE_LOGO=YOUR SITE LOGO URL
13 |
14 | # Your database configruation details
15 | DB_NAME=bloggy
16 | DB_USER=root
17 | DB_PASSWORD=
18 | DB_HOST=127.0.0.1
19 | DB_PORT=3306
20 |
21 | # Media configurations
22 | USE_SPACES=False
23 | AWS_ACCESS_KEY_ID=
24 | AWS_SECRET_ACCESS_KEY=
25 | AWS_STORAGE_BUCKET_NAME=
26 | AWS_S3_ENDPOINT_URL=
27 |
28 | # Other site configruations
29 | GOOGLE_RECAPTHCA_SECRET_KEY=
30 |
31 | # SEO Related
32 | PING_INDEX_NOW_POST_UPDATE=False
33 | PING_GOOGLE_POST_UPDATE=False
34 | INDEX_NOW_API_KEY=
35 |
36 | #Sends emails using an SMTP server
37 | EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
38 | EMAIL_HOST=
39 | EMAIL_PORT=587
40 | EMAIL_HOST_USER=
41 | EMAIL_HOST_PASSWORD=
42 | EMAIL_USE_TLS=True
43 | DEFAULT_FROM_EMAIL=
44 |
45 | POST_TYPE_CHOICES=article:Article,quiz:Quiz,lesson:Lesson
46 | SHOW_EMTPY_CATEGORIES=False
47 |
48 |
49 | #ads.txt file content
50 | LOAD_GOOGLE_TAG_MANAGER=True
51 | LOAD_GOOGLE_ADS=True
52 | MY_ADS_TXT_CONTENT=
53 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [nilandev]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: waiting for triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[New Feature] "
5 | labels: waiting for triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.10"]
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v3
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install -r requirements.txt
21 | pip install pylint
22 | - name: Analysing the code with pylint
23 | run: |
24 | pylint -d C0114,C0115,C0116,C0301,E1101,R0903,R0901,W0613 $(git ls-files '*.py') --fail-under=9
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.production
3 | /venv/*
4 | /venv
5 | /.idea/
6 | /venv/
7 | __pycache__/*
8 | /bloggy_api/__pycache__/*
9 | /bloggy/__pycache__/*
10 | # Byte-compiled / optimized / DLL files
11 | __pycache__/
12 | *.py[cod]
13 | __pycache__
14 | /bloggy/media/uploads/*
15 | /bloggy/media/uploads/*.*
16 |
17 | # C extensions
18 | *.so
19 |
20 | # Distribution / packaging
21 | bin/
22 | build/
23 | develop-eggs/
24 | eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | .tox/
40 | .coverage
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 |
45 | # Translations
46 | *.mo
47 |
48 | # Mr Developer
49 | .mr.developer.cfg
50 | .project
51 | .pydevproject
52 |
53 | # Rope
54 | .ropeproject
55 |
56 | # Django stuff:
57 | *.log
58 | *.pot
59 | *.__pycache__
60 | **.pyc
61 |
62 | # Sphinx documentation
63 | docs/_build/
64 | /bloggy_frontend/node_modules/
65 | /bloggy/static/
66 | /bloggy/static
67 | media/uploads/*
68 | media/uploads/
69 |
70 | # Virtual Environments Ignore
71 | .venv
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | ignore=migrations
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Djanog:Run",
9 | "type": "python",
10 | "request": "launch",
11 | "stopOnEntry": false,
12 | "python": "${workspaceRoot}/venv/bin/python3",
13 | "program": "${workspaceFolder}/manage.py",
14 | "args": [
15 | "runserver"
16 | ],
17 | "django": false,
18 | "justMyCode": true,
19 | "autoReload": {
20 | "enable": true
21 | }
22 | },
23 | {
24 | "name": "Extension",
25 | "type": "extensionHost",
26 | "request": "launch",
27 | "runtimeExecutable": "${execPath}",
28 | "args": [
29 | "--extensionDevelopmentPath=${workspaceFolder}"
30 | ]
31 | },
32 | {
33 | "name": "Djanog:Debug",
34 | "type": "python",
35 | "request": "launch",
36 | "stopOnEntry": false,
37 | "python": "${workspaceRoot}/venv/bin/python3",
38 | "program": "${workspaceFolder}/manage.py",
39 | "args": [
40 | "runserver"
41 | ],
42 | "django": true,
43 | "justMyCode": true
44 | },
45 | ],
46 | "compounds": []
47 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.wordWrapColumn": 120,
3 | "python.analysis.typeCheckingMode": "basic",
4 | "python.formatting.provider": "autopep8",
5 | "editor.formatOnSave": false,
6 | "python.linting.enabled": true,
7 | "python.linting.lintOnSave": true,
8 | // "editor.fontFamily": "Dank Mono, JetBrains Mono NL, Fira Code, Menlo, Monaco, 'Courier New', monospace",
9 | // "editor.fontSize": 14,
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 - 2023 Nilanchala Panigrahy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting Security Issues
2 |
3 | The StackTips community take security bugs in seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
4 |
5 | To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/StackTipsLab/bloggy/security/advisories/new) tab.
6 |
7 | We will review the issue and take the necessary steps in handling your report. During the issue investigation we may ask for additional information or guidance.
8 |
9 | ## Third party security issues
10 | Report security bugs in third-party modules to the person or team maintaining the module.
11 |
12 |
13 | If you find security issues in node module, you can report them directly through the [npm contact form](https://www.npmjs.com/support)
14 |
15 | To report a vulnerability in Django, you [visit this link here](https://docs.djangoproject.com/en/dev/internals/security/#reporting-security-issues)
--------------------------------------------------------------------------------
/bloggy/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/.DS_Store
--------------------------------------------------------------------------------
/bloggy/__init__.py:
--------------------------------------------------------------------------------
1 | import pymysql
2 |
3 | pymysql.install_as_MySQLdb()
4 |
5 | default_app_config = 'bloggy.apps.MyAppConfig'
6 |
--------------------------------------------------------------------------------
/bloggy/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from .admin import *
2 | from .post_admin import *
3 | from .category_admin import *
4 | from .course_admin import *
5 | from .misc_admin import *
6 | from .subscriber_admin import *
7 | from .user_admin import *
8 | from .page_admin import *
9 | from .quiz_admin import *
10 |
--------------------------------------------------------------------------------
/bloggy/admin/admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django_summernote.admin import SummernoteModelAdmin
3 |
4 | seo_fieldsets = ('SEO Settings', {
5 | 'fields': ('meta_title', 'meta_description', 'meta_keywords'),
6 | })
7 |
8 | publication_fieldsets = ('Publication options', {
9 | 'fields': ('publish_status', 'published_date',),
10 | })
11 |
12 |
13 | def publish(model_admin, request, queryset):
14 | queryset.update(publish_status='LIVE')
15 |
16 |
17 | publish.short_description = "Publish"
18 |
19 |
20 | def unpublish(model_admin, request, queryset):
21 | queryset.update(publish_status='DRAFT')
22 |
23 |
24 | unpublish.short_description = "Unpublish"
25 |
26 |
27 | class BloggyAdminForm(forms.ModelForm):
28 | excerpt = forms.CharField(widget=forms.Textarea(attrs={'rows': 3, 'cols': 105}))
29 | title = forms.CharField(widget=forms.TextInput(attrs={'size': 105}))
30 | meta_title = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 1, 'cols': 100}))
31 | meta_description = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
32 | meta_keywords = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 2, 'cols': 100}))
33 |
34 | class Meta:
35 | abstract = True
36 |
37 |
38 | class BloggyAdmin(SummernoteModelAdmin):
39 | actions = [publish, unpublish]
40 | list_per_page = 50
41 |
42 | def published_date_display(self, obj):
43 | if obj.published_date:
44 | return obj.published_date.strftime("%b %d, %Y")
45 | return "-"
46 |
47 | published_date_display.short_description = "Date Published"
48 |
49 | def author_display(self, obj):
50 | return '' + obj.author.name
51 |
52 | author_display.short_description = "Author"
53 |
54 | def is_published(self, queryset):
55 | if queryset.publish_status == 'LIVE':
56 | return True
57 | return False
58 |
59 | is_published.boolean = True
60 |
61 | class Meta:
62 | abstract = True
63 |
--------------------------------------------------------------------------------
/bloggy/admin/comment_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django_summernote.admin import SummernoteModelAdmin
3 |
4 | from bloggy.models.comment import Comment
5 |
6 |
7 | def approve_comments(request, queryset):
8 | queryset.update(active=True)
9 |
10 |
11 | @admin.register(Comment)
12 | class CommentAdmin(SummernoteModelAdmin):
13 | list_display = (
14 | 'comment_content',
15 | 'comment_author_name',
16 | 'comment_author_email',
17 | 'comment_author_url',
18 | 'comment_author_ip',
19 | 'post',
20 | 'comment_date',
21 | 'active')
22 | list_filter = ('active', 'comment_date')
23 | search_fields = ('user', 'user', 'comment_content')
24 | actions = ['approve_comments']
25 |
--------------------------------------------------------------------------------
/bloggy/admin/course_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from bloggy.admin import BloggyAdmin, BloggyAdminForm, seo_fieldsets, publication_fieldsets
3 | from bloggy.models.course import Course
4 |
5 |
6 | class CourseForm(BloggyAdminForm):
7 | model = Course
8 |
9 |
10 | @admin.register(Course)
11 | class CourseAdmin(BloggyAdmin):
12 | prepopulated_fields = {
13 | "slug": ("title",)
14 | }
15 | list_display = (
16 | 'id',
17 | 'title',
18 | 'is_published',
19 | 'thumbnail_tag',
20 | 'display_order',
21 | 'author_display',
22 | 'published_date_display',
23 | 'display_order')
24 |
25 | list_filter = (
26 | 'difficulty',
27 | ("category", admin.RelatedOnlyFieldListFilter),
28 | )
29 |
30 | fieldsets = ((None, {'fields': (
31 | 'title',
32 | 'excerpt',
33 | 'slug',
34 | 'description',
35 | 'display_order',
36 | 'thumbnail',
37 | 'category',
38 | 'difficulty',
39 | 'is_featured')
40 | }), publication_fieldsets, seo_fieldsets)
41 |
42 | summernote_fields = ('description',)
43 | readonly_fields = ['thumbnail_tag']
44 | ordering = ('-display_order',)
45 | list_display_links = ['title']
46 | form = CourseForm
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/bloggy/admin/misc_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | import bloggy.models.option
4 | from bloggy import settings
5 | from bloggy.models import RedirectRule
6 |
7 | admin.site.site_header = settings.SITE_TITLE.upper()
8 | admin.site.site_title = settings.SITE_TITLE
9 | admin.site.index_title = "Dashboard"
10 | admin.site.register(bloggy.models.option.Option)
11 |
12 |
13 | @admin.register(RedirectRule)
14 | class RedirectRuleAdmin(admin.ModelAdmin):
15 | list_display = (
16 | 'source',
17 | 'destination',
18 | 'status_code',
19 | 'note',
20 | )
21 |
--------------------------------------------------------------------------------
/bloggy/admin/page_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from bloggy.admin import BloggyAdminForm, BloggyAdmin, publication_fieldsets, seo_fieldsets
4 | from bloggy.models.page import Page
5 |
6 |
7 | class PageForm(BloggyAdminForm):
8 | model = Page
9 |
10 |
11 | @admin.register(Page)
12 | class PageAdmin(BloggyAdmin):
13 | prepopulated_fields = {"url": ("title",)}
14 | list_display = (
15 | 'id',
16 | 'title',
17 | 'url',
18 | 'excerpt',
19 | 'publish_status',
20 | )
21 |
22 | fieldsets = (
23 | (None, {
24 | 'fields': ('title', 'excerpt', 'url', 'content',)
25 | }), publication_fieldsets, seo_fieldsets)
26 |
27 | search_fields = ['title']
28 | summernote_fields = ('content',)
29 | readonly_fields = ['updated_date', 'created_date']
30 | date_hierarchy = 'published_date'
31 | form = PageForm
32 | ordering = ('-created_date',)
33 | list_display_links = ['title']
34 |
35 | def get_form(self, request, obj=None, change=False, **kwargs):
36 | return super().get_form(request, obj, change, **kwargs)
37 |
--------------------------------------------------------------------------------
/bloggy/admin/subscriber_admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import admin
3 |
4 | from bloggy.models.subscriber import Subscribers
5 |
6 |
7 | class SubscriberForm(forms.ModelForm):
8 | model = Subscribers
9 |
10 |
11 | @admin.register(Subscribers)
12 | class SubscribersAdmin(admin.ModelAdmin):
13 | list_display_links = ['email']
14 | list_display = ('id', 'email', 'name', 'confirmed', 'created_date')
15 |
--------------------------------------------------------------------------------
/bloggy/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class MyAppConfig(AppConfig):
5 | name = 'bloggy'
6 |
7 | def ready(self):
8 | pass
9 |
--------------------------------------------------------------------------------
/bloggy/context_processors.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.http import HttpRequest
4 |
5 | from bloggy import settings
6 |
7 |
8 | def seo_attrs(request: HttpRequest):
9 | """returns seo attributes to be merged into the context
10 | Arguments:
11 | request {HttpRequest} -- request object
12 | """
13 | with open('seo_settings.json', 'r', encoding='utf-8') as seo_file:
14 | seo_settings = json.load(seo_file)
15 | # Default SEO attributes
16 | seo = {
17 | 'site_name': settings.SITE_TITLE,
18 | 'meta_title': settings.SITE_TAGLINE,
19 | 'meta_description': settings.SITE_DESCRIPTION,
20 | 'meta_image': settings.SITE_LOGO
21 | }
22 |
23 | # Get SEO attributes based on the request path
24 | request_path = request.path
25 | if request_path in seo_settings:
26 | seo.update(seo_settings[request_path])
27 |
28 | return seo
29 |
30 |
31 | def app_settings(request: HttpRequest):
32 | """
33 | returns app settings
34 | """
35 | return {
36 | "LOAD_GOOGLE_TAG_MANAGER": settings.LOAD_GOOGLE_TAG_MANAGER,
37 | "LOAD_GOOGLE_ADS": settings.LOAD_GOOGLE_ADS,
38 | "ASSETS_DOMAIN": settings.ASSETS_DOMAIN
39 | }
40 |
--------------------------------------------------------------------------------
/bloggy/forms/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/forms/__init__.py
--------------------------------------------------------------------------------
/bloggy/forms/comment_form.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from bloggy.models.comment import Comment
4 |
5 |
6 | class CommentForm(forms.ModelForm):
7 |
8 | class Meta:
9 | model = Comment
10 | fields = ('use_name', 'user_email', 'comment_content')
11 |
--------------------------------------------------------------------------------
/bloggy/forms/signup_form.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from django.contrib.auth.forms import UserCreationForm
3 | from django.core.exceptions import ValidationError
4 | from bloggy.models import User
5 | from django import forms
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class SignUpForm(UserCreationForm):
11 | honeypot = forms.CharField(required=False, widget=forms.HiddenInput)
12 |
13 | class Meta:
14 | model = User
15 | fields = ('name', 'email', 'password1', 'password2')
16 |
17 | def save(self, commit=True):
18 | user = super().save(commit=False) # Call the parent class's save method
19 | # Generate the username based on the user's name (you can use your custom function here)
20 | user.username = self.generate_unique_username(self.cleaned_data['name'])
21 | user.is_active = False
22 | user.is_staff = False
23 |
24 | if commit:
25 | user.save()
26 | return user
27 |
28 | def clean_honeypot(self):
29 | honeypot_value = self.cleaned_data.get('honeypot')
30 | if honeypot_value:
31 | logger.error("ERROR: Honeypot validation error!")
32 | raise ValidationError("Oops! Looks like you're not a human!")
33 | return honeypot_value
34 |
35 | @staticmethod
36 | def generate_unique_username(name):
37 | # Convert the user's name to a lowercase username with underscores
38 | base_username = name.lower().replace(' ', '_')
39 |
40 | # Check if the base_username is unique, if not, append a number until it is
41 | username = base_username
42 | count = 1
43 | while User.objects.filter(username=username).exists():
44 | username = f"{base_username}{count}"
45 | count += 1
46 |
47 | return username
48 |
--------------------------------------------------------------------------------
/bloggy/forms/update_password_form.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.forms import CharField, PasswordInput
3 |
4 | from bloggy.models import User
5 |
6 |
7 | class UpdatePasswordForm(forms.BaseForm):
8 | model = User
9 |
10 | error_css_class = 'has-error'
11 | error_messages = {'password_incorrect': "The old password is not correct. Try again."}
12 |
13 | old_password = CharField(
14 | required=True, label='Password',
15 | widget=PasswordInput(attrs={'class': 'form-control'}),
16 | error_messages={'required': 'The password can not be empty'})
17 |
18 | new_password1 = CharField(
19 | required=True, label='Password',
20 | widget=PasswordInput(attrs={'class': 'form-control'}),
21 | error_messages={'required': 'The password can not be empty'})
22 |
23 | new_password2 = CharField(
24 | required=True, label='Password (Repeat)',
25 | widget=PasswordInput(attrs={'class': 'form-control'}),
26 | error_messages={'required': 'The password can not be empty'})
27 |
--------------------------------------------------------------------------------
/bloggy/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/management/__init__.py
--------------------------------------------------------------------------------
/bloggy/management/commands/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/management/commands/.DS_Store
--------------------------------------------------------------------------------
/bloggy/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/management/commands/__init__.py
--------------------------------------------------------------------------------
/bloggy/management/commands/runseed.py:
--------------------------------------------------------------------------------
1 | from django.core.management import call_command
2 | from django.core.management.base import BaseCommand
3 |
4 |
5 | class Command(BaseCommand):
6 | help = 'Importing demo contents'
7 |
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 |
11 | def add_arguments(self, parser):
12 | parser.add_argument('--dir', type=str, help="File path to import, e.g. ~/bloggy/demo_content")
13 |
14 | def handle(self, *args, **options):
15 | file_path = options['dir']
16 |
17 | commands = [
18 | ('seed_users', 'users.csv'),
19 | ('seed_categories', 'categories.csv'),
20 | ('seed_posts', 'posts.csv'),
21 | ('seed_pages', 'pages.csv'),
22 | ('seed_redirectrules', 'redirect_rules.csv'),
23 | ('update_category_count', None),
24 | ]
25 |
26 | for command, file in commands:
27 | if file:
28 | call_command(command, f'--file={file_path}/{file}')
29 | else:
30 | call_command(command)
31 |
32 | self.stdout.write(self.style.SUCCESS("Import Complete!"))
33 |
--------------------------------------------------------------------------------
/bloggy/management/commands/seed_categories.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | from django.core.management.base import BaseCommand
4 | from django.utils.text import slugify
5 |
6 | from bloggy.models import Category
7 |
8 |
9 | class Command(BaseCommand):
10 | help = 'Importing categories'
11 |
12 | def add_arguments(self, parser):
13 | parser.add_argument('-f', '--file', type=str,
14 | help="File path to import, e.g. ~/bloggy/demo_content/categories.csv")
15 |
16 | def handle(self, *args, **options):
17 | file_path = options['file']
18 |
19 | counter = 0
20 | with open(file_path, encoding="utf-8") as f:
21 | reader = csv.reader(f)
22 | print('Importing categories from file', file_path)
23 |
24 | for index, row in enumerate(reader):
25 | if index > 0:
26 | counter = counter + 1
27 | Category.objects.get_or_create(
28 | title=row[0],
29 | slug=slugify(row[1]),
30 | description=row[2],
31 | logo=row[3],
32 | color=row[4],
33 | publish_status=row[5]
34 | )
35 |
36 | self.stdout.write(self.style.SUCCESS(f"Imported %s categories" % counter))
37 |
--------------------------------------------------------------------------------
/bloggy/management/commands/seed_pages.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | from django.core.management.base import BaseCommand
4 | from bloggy.models.page import Page
5 |
6 |
7 | class Command(BaseCommand):
8 | help = 'Importing pages'
9 |
10 | def add_arguments(self, parser):
11 | parser.add_argument('-f', '--file', type=str,
12 | help="File path to import, e.g. ~/bloggy/demo_content/pages.csv")
13 |
14 | def handle(self, *args, **options):
15 | file_path = options['file']
16 |
17 | counter = 0
18 | with open(file_path, encoding="utf-8") as f:
19 | reader = csv.reader(f)
20 | print('Importing pages from file', file_path)
21 | for index, row in enumerate(reader):
22 | if index > 0:
23 | counter = counter + 1
24 | Page.objects.get_or_create(
25 | title=row[0],
26 | url=row[1],
27 | publish_status=row[2],
28 | meta_title=row[3],
29 | meta_description=row[4],
30 | meta_keywords=row[5],
31 | excerpt=row[6],
32 | content=row[7],
33 | )
34 |
35 | self.stdout.write(self.style.SUCCESS(f"Imported %s pages" % counter))
36 |
--------------------------------------------------------------------------------
/bloggy/management/commands/seed_posts.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | from django.core.management.base import BaseCommand
4 | from django.utils import timezone
5 | from django.utils.text import slugify
6 |
7 | from bloggy.models import Category, Post, User
8 |
9 |
10 | class Command(BaseCommand):
11 | help = 'Importing posts'
12 |
13 | def __init__(self, *args, **kwargs):
14 | super().__init__()
15 |
16 | def add_arguments(self, parser):
17 | parser.add_argument('-f', '--file', type=str,
18 | help="File path to import, e.g. ~/bloggy/demo_content/posts.csv")
19 |
20 | def handle(self, *args, **options):
21 | file_path = options['file']
22 |
23 | counter = 0
24 | with open(file_path, encoding="utf-8") as f:
25 | reader = csv.reader(f)
26 | print('Importing articles from file', file_path)
27 | for index, row in enumerate(reader):
28 | if index > 0:
29 | counter = counter + 1
30 | slug = slugify(row[0])
31 | article = Post.objects.get_or_create(
32 | title=row[0],
33 | slug=slug,
34 | publish_status=row[1],
35 | excerpt=row[2],
36 | difficulty=row[3],
37 | is_featured=row[4],
38 | content=row[5],
39 | video_id=row[8],
40 | post_type=row[9],
41 | template_type=row[10],
42 | published_date=timezone.now(),
43 | author=User.objects.get(id=row[7]),
44 | )
45 |
46 | categories = Category.objects.filter(slug__in=row[11].split(",")).all()
47 | saved_article = Post.objects.get(slug=slug)
48 | saved_article.category.set(categories)
49 | saved_article.save()
50 |
51 | self.stdout.write(self.style.SUCCESS(f"Imported %s articles" % counter))
52 |
--------------------------------------------------------------------------------
/bloggy/management/commands/seed_redirectrules.py:
--------------------------------------------------------------------------------
1 | import csv
2 |
3 | from django.core.management.base import BaseCommand
4 | from django.utils.text import slugify
5 |
6 | from bloggy.models import Category, RedirectRule
7 |
8 |
9 | class Command(BaseCommand):
10 | help = 'Importing redirect rules'
11 |
12 | def add_arguments(self, parser):
13 | parser.add_argument('-f', '--file', type=str,
14 | help="File path to import, e.g. ~/bloggy/demo_content/redirect_rules.csv")
15 |
16 | def handle(self, *args, **options):
17 | file_path = options['file']
18 |
19 | counter = 0
20 | with open(file_path, encoding="utf-8") as f:
21 | reader = csv.reader(f)
22 | print('Importing redirect rules', file_path)
23 |
24 | for index, row in enumerate(reader):
25 | if index > 0:
26 | counter = counter + 1
27 | RedirectRule.objects.get_or_create(
28 | from_url=row[0],
29 | to_url=row[1],
30 | status_code=row[2],
31 | note=row[3]
32 | )
33 |
34 | self.stdout.write(self.style.SUCCESS(f"%s redirect rules imported" % counter))
35 |
--------------------------------------------------------------------------------
/bloggy/management/commands/update_category_count.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django.db import transaction
3 |
4 | from bloggy.models import Category, Post
5 |
6 |
7 | class Command(BaseCommand):
8 | help = 'Update Category Count'
9 |
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 |
13 | def handle(self, *args, **options):
14 | categories = Category.objects.select_for_update().all()
15 | with transaction.atomic():
16 | for category in categories:
17 | article_count = Post.objects.all().filter(category=category).count()
18 | if article_count > 0:
19 | category.article_count = article_count
20 | category.save()
21 |
22 | self.stdout.write(self.style.SUCCESS('Updated category count.'))
23 |
--------------------------------------------------------------------------------
/bloggy/middleware/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/middleware/__init__.py
--------------------------------------------------------------------------------
/bloggy/middleware/redirect.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.http import HttpResponsePermanentRedirect
4 | from django.utils.deprecation import MiddlewareMixin
5 |
6 | from bloggy import settings
7 | from bloggy.models import RedirectRule
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | class RedirectMiddleware(MiddlewareMixin):
13 |
14 | def process_response(self, request, response):
15 | request_path = request.path
16 |
17 | # Don't do anything for /api endpoints
18 | if request_path.startswith("/api/"):
19 | return response
20 |
21 | if response.status_code == 404:
22 | logger.warning("ERROR 404:: %s", request_path)
23 | redirect_rule = RedirectRule.objects.filter(source__exact=request_path).first()
24 |
25 | if redirect_rule:
26 | logger.warning("Explicit redirect rule found %s ==> %s", redirect_rule.source,
27 | redirect_rule.destination)
28 | return HttpResponsePermanentRedirect(settings.SITE_URL + redirect_rule.destination)
29 |
30 | return response
31 |
--------------------------------------------------------------------------------
/bloggy/migrations/0002_rename_description_category_excerpt.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.6 on 2023-11-11 16:32
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('bloggy', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name='category',
15 | old_name='description',
16 | new_name='excerpt',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/bloggy/migrations/0003_rename_logo_category_thumbnail_page_thumbnail.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.6 on 2023-11-11 16:48
2 |
3 | import bloggy.models.page
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('bloggy', '0002_rename_description_category_excerpt'),
11 | ]
12 |
13 | operations = [
14 | migrations.RenameField(
15 | model_name='category',
16 | old_name='logo',
17 | new_name='thumbnail',
18 | ),
19 | migrations.AddField(
20 | model_name='page',
21 | name='thumbnail',
22 | field=models.ImageField(blank=True, null=True, upload_to=bloggy.models.page.image_upload_path),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/bloggy/migrations/0004_remove_redirectrule_is_regx.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.7 on 2023-11-17 16:54
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('bloggy', '0003_rename_logo_category_thumbnail_page_thumbnail'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='redirectrule',
15 | name='is_regx',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/bloggy/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/migrations/__init__.py
--------------------------------------------------------------------------------
/bloggy/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/models.py
--------------------------------------------------------------------------------
/bloggy/models/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/models/.DS_Store
--------------------------------------------------------------------------------
/bloggy/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .media import Media
2 | from .categories import Category
3 | from .user import User
4 | from .post_actions import Bookmark, Vote
5 | from .comment import Comment
6 | from .option import Option
7 | from .post import Post
8 | from .course import Course
9 | from .quizzes import Quiz, QuizQuestion, QuizAnswer, UserQuizScore
10 | from .redirect_rule import RedirectRule
11 | from .verification_token import VerificationToken
12 |
13 |
--------------------------------------------------------------------------------
/bloggy/models/categories.py:
--------------------------------------------------------------------------------
1 | from colorfield.fields import ColorField
2 | from django.db import models
3 | from django.urls import reverse
4 | from django.utils.html import format_html
5 | from django.utils.text import slugify
6 |
7 | from bloggy.models.mixin.SeoAware import SeoAware
8 | from bloggy.models.mixin.updatable import Updatable
9 | from bloggy.utils.string_utils import StringUtils
10 |
11 |
12 | def upload_logo_image(self, filename):
13 | return f'uploads/categories/{filename}'
14 |
15 |
16 | class Category(Updatable, SeoAware):
17 | title = models.CharField(max_length=150, help_text='Enter title')
18 | article_count = models.IntegerField(default=0)
19 | slug = models.SlugField(max_length=150, help_text='Enter slug', unique=True)
20 | excerpt = models.TextField(max_length=1000, help_text='Enter description', null=True, blank=True)
21 | thumbnail = models.ImageField(upload_to=upload_logo_image, null=True)
22 | color = ColorField(default='#1976D2')
23 |
24 | publish_status = models.CharField(
25 | max_length=20, choices=[
26 | ('DRAFT', 'DRAFT'),
27 | ('LIVE', 'LIVE')
28 | ],
29 | default='DRAFT', blank=True, null=True,
30 | help_text="Select publish status",
31 | verbose_name="Publish status")
32 |
33 | class Meta:
34 | ordering = ['title']
35 | verbose_name = "category"
36 | verbose_name_plural = "categories"
37 |
38 | def save(self, *args, **kwargs):
39 | if StringUtils.is_blank(self.slug):
40 | self.slug = slugify(self.title)
41 | super().save(*args, **kwargs)
42 |
43 | def get_absolute_url(self):
44 | return reverse('categories_single', args=[str(self.slug)])
45 |
46 | def thumbnail_tag(self):
47 | if self.thumbnail_tag:
48 | return format_html(f' ')
49 | return ""
50 |
51 | thumbnail_tag.short_description = 'Thumbnail'
52 | thumbnail_tag.allow_tags = True
53 |
54 | def __str__(self):
55 | return str(self.title)
56 |
--------------------------------------------------------------------------------
/bloggy/models/comment.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from bloggy import settings
4 |
5 |
6 | class Comment(models.Model):
7 | post = models.ForeignKey('bloggy.Post', on_delete=models.CASCADE, related_name='comments')
8 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments', blank=True,
9 | null=True)
10 | parent = models.ForeignKey('self', related_name='reply_set', null=True, on_delete=models.PROTECT)
11 | comment_content = models.TextField()
12 | comment_author_name = models.TextField(null=True, blank=True)
13 | comment_author_email = models.TextField(null=True, blank=True)
14 | comment_author_url = models.TextField(null=True, blank=True)
15 | comment_author_ip = models.GenericIPAddressField(default="0.0.0.0", null=True, blank=True)
16 | comment_date = models.DateTimeField(auto_now_add=True)
17 | active = models.BooleanField(default=False)
18 |
19 | class Meta:
20 | ordering = ['comment_date']
21 | verbose_name = "Comment"
22 | verbose_name_plural = "Comments"
23 |
24 | def __str__(self):
25 | return 'Comment {} by {}'.format(self.comment_content, self.user.get_full_name() if self.user else '-')
26 |
27 | def get_comments(self):
28 | return Comment.objects.filter(parent=self).filter(active=True)
29 |
--------------------------------------------------------------------------------
/bloggy/models/media.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django_summernote.models import AbstractAttachment
3 | from django_summernote.utils import get_config
4 |
5 |
6 | class Media(AbstractAttachment):
7 | post_id = models.TextField(max_length=300, help_text='Enter post ID', null=True, blank=True)
8 | post_type = models.CharField(
9 | max_length=20, choices=[
10 | ('article', 'article'),
11 | ('question', 'question'),
12 | ('category', 'category'),
13 | ], default='article', blank=True, null=True, help_text="Select post type", verbose_name="Post type")
14 | media_type = models.CharField(max_length=255, null=True, blank=True,
15 | help_text="Media type like attachment, thumbnail")
16 |
17 | def save(self, *args, **kwargs):
18 | get_config()['attachment_upload_to'] = f'uploads/{self.post_type}/{self.post_id}'
19 | super().save(*args, **kwargs)
20 |
21 | def get_attachment_upload_to(self):
22 | return f'uploads/{self.post_type}/{self.post_id}'
23 |
--------------------------------------------------------------------------------
/bloggy/models/mixin/Content.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.text import slugify
3 | from bloggy.models.mixin.SeoAware import SeoAware
4 | from bloggy.models.mixin.updatable import Updatable
5 | from bloggy.utils.string_utils import StringUtils
6 |
7 |
8 | class Content(Updatable, SeoAware):
9 | title = models.CharField(max_length=300, help_text='Enter title')
10 | slug = models.SlugField(max_length=150, help_text='Enter slug', unique=True)
11 | excerpt = models.CharField(
12 | max_length=500,
13 | help_text='Enter excerpt',
14 | null=True,
15 | blank=True
16 | )
17 |
18 | display_order = models.IntegerField(null=True, help_text='Display order', default=0)
19 | published_date = models.DateTimeField(null=True, blank=True)
20 | publish_status = models.CharField(
21 | max_length=20, choices=[
22 | ('DRAFT', 'DRAFT'),
23 | ('LIVE', 'LIVE'),
24 | ('DELETED', 'DELETED')
25 | ],
26 | default='DRAFT', blank=True, null=True,
27 | help_text="Select publish status",
28 | verbose_name="Publish status")
29 |
30 | def get_excerpt(self):
31 | return self.excerpt[:10]
32 |
33 | def save(self, *args, **kwargs):
34 | if StringUtils.is_blank(self.slug):
35 | self.slug = slugify(self.title)
36 | super().save(*args, **kwargs)
37 |
38 | def __str__(self):
39 | return str(self.title)
40 |
41 | class Meta:
42 | abstract = True
43 |
--------------------------------------------------------------------------------
/bloggy/models/mixin/ResizeImageMixin.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from io import BytesIO
3 |
4 | from PIL import Image
5 | from django.core.files import File
6 | from django.core.files.base import ContentFile
7 | from django.db import models
8 |
9 |
10 | # Not used atm
11 | class ResizeImageMixin:
12 | def resize(self, image_field: models.ImageField, size: tuple):
13 | im = Image.open(image_field) # Catch original
14 | source_image = im.convert('RGB')
15 | source_image.thumbnail(size) # Resize to size
16 | output = BytesIO()
17 | source_image.save(output, format='JPEG') # Save resize image to bytes
18 | output.seek(0)
19 |
20 | content_file = ContentFile(output.read()) # Read output and create ContentFile in memory
21 | file = File(content_file)
22 |
23 | random_name = f'{uuid.uuid4()}.jpeg'
24 | image_field.save(random_name, file, save=False)
25 |
--------------------------------------------------------------------------------
/bloggy/models/mixin/SeoAware.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class SeoAware(models.Model):
5 | meta_title = models.CharField(max_length=120, help_text='Meta Title', blank=True, null=True)
6 | meta_description = models.TextField(help_text='Meta Description', blank=True, null=True)
7 | meta_keywords = models.CharField(max_length=300, help_text='Meta Keywords', blank=True, null=True)
8 |
9 | class Meta:
10 | abstract = True
11 |
--------------------------------------------------------------------------------
/bloggy/models/mixin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/models/mixin/__init__.py
--------------------------------------------------------------------------------
/bloggy/models/mixin/updatable.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Updatable(models.Model):
5 | created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
6 | updated_date = models.DateTimeField(auto_now=True, null=True)
7 |
8 | def get_model_type(self):
9 | return self._meta.verbose_name
10 |
11 | class Meta:
12 | abstract = True
13 |
--------------------------------------------------------------------------------
/bloggy/models/option.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import TextField
3 |
4 | from bloggy.models.mixin.updatable import Updatable
5 |
6 |
7 | class Option(Updatable):
8 | id = models.AutoField(primary_key=True)
9 | key = models.SlugField(max_length=150, help_text='Enter key', unique=True)
10 | value = TextField(null=True, help_text='Enter value')
11 |
12 | def __str__(self):
13 | return str(self.key)
14 |
--------------------------------------------------------------------------------
/bloggy/models/page.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import TextField
3 |
4 | from bloggy.models.mixin.SeoAware import SeoAware
5 | from bloggy.models.mixin.updatable import Updatable
6 |
7 |
8 | def image_upload_path(self, page_id):
9 | return f'uploads/pages/{page_id}'
10 |
11 |
12 | class Page(Updatable, SeoAware):
13 | """
14 | Stores page data.
15 | """
16 |
17 | title = models.CharField(max_length=300, help_text='Enter title')
18 | excerpt = models.CharField(
19 | max_length=500,
20 | help_text='Enter excerpt',
21 | null=True,
22 | blank=True
23 | )
24 |
25 | url = models.CharField(max_length=150, help_text='Enter url', unique=True)
26 | thumbnail = models.ImageField(upload_to=image_upload_path, blank=True, null=True)
27 | content = TextField(null=True, help_text='Post content')
28 | published_date = models.DateTimeField(null=True, blank=True)
29 | publish_status = models.CharField(
30 | max_length=20, choices=[
31 | ('DRAFT', 'DRAFT'),
32 | ('LIVE', 'LIVE'),
33 | ('DELETED', 'DELETED')
34 | ],
35 | default='DRAFT', blank=True, null=True,
36 | help_text="Select publish status",
37 | verbose_name="Publish status")
38 |
39 | def __str__(self):
40 | return str(self.title)
41 |
42 | class Meta:
43 | verbose_name = 'Page'
44 | verbose_name_plural = 'Pages'
45 |
--------------------------------------------------------------------------------
/bloggy/models/post_actions.py:
--------------------------------------------------------------------------------
1 |
2 | from django.db import models
3 |
4 | from bloggy import settings
5 | from bloggy.models.mixin.updatable import Updatable
6 |
7 |
8 | class Vote(Updatable):
9 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
10 | post_id = models.IntegerField(null=False, help_text='Post id')
11 | post_type = models.CharField(
12 | null=False,
13 | max_length=20,
14 | choices=settings.get_post_types(),
15 | help_text="Select content type",
16 | verbose_name="Content type"
17 | )
18 |
19 | class Meta:
20 | verbose_name = "Vote"
21 | verbose_name_plural = "Votes"
22 |
23 | class Bookmark(Vote):
24 | class Meta:
25 | verbose_name = "Bookmark"
26 | verbose_name_plural = "Bookmarks"
27 |
--------------------------------------------------------------------------------
/bloggy/models/redirect_rule.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from bloggy.models.mixin.updatable import Updatable
4 |
5 |
6 | class RedirectRule(Updatable):
7 | source = models.CharField(max_length=300, help_text='Enter from url')
8 | destination = models.CharField(max_length=300, help_text='Enter to url')
9 | status_code = models.IntegerField(
10 | default='standard', blank=True, null=True,
11 | choices=[
12 | (301, '301 Moved Permanently'),
13 | (307, '307 Temporary Redirect'),
14 | ],
15 | help_text="Redirect type",
16 | verbose_name="Redirect type")
17 |
18 | note = models.CharField(
19 | max_length=500,
20 | help_text='Enter note',
21 | null=True,
22 | blank=True
23 | )
24 |
25 | def __str__(self):
26 | return f"{self.status_code}::{self.source}"
27 |
28 |
29 | class Meta:
30 | verbose_name = "Redirect"
31 | verbose_name_plural = "Redirects"
32 |
--------------------------------------------------------------------------------
/bloggy/models/subscriber.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from bloggy import settings
4 | from bloggy.services.token_generator import TOKEN_LENGTH
5 |
6 |
7 | class Subscribers(models.Model):
8 | email = models.CharField(unique=True, max_length=50)
9 | name = models.CharField(max_length=50)
10 | confirmed = models.BooleanField(null=True, default=False)
11 | confirmation_code = models.CharField(
12 | default=None,
13 | max_length=TOKEN_LENGTH,
14 | help_text="The random token identifying the verification request.",
15 | null=True,
16 | blank=True,
17 | verbose_name="token",
18 | )
19 | created_date = models.DateTimeField(auto_created=True, null=True, auto_now_add=True)
20 | user = models.ForeignKey(
21 | settings.AUTH_USER_MODEL,
22 | on_delete=models.PROTECT,
23 | related_name='user',
24 | blank=True,
25 | null=True
26 | )
27 |
28 | def __str__(self):
29 | return self.email + " (" + ("not " if not self.confirmed else "") + "confirmed)"
30 |
31 | class Meta:
32 | ordering = ['created_date']
33 | verbose_name = "Subscriber"
34 | verbose_name_plural = "Subscribers"
35 |
--------------------------------------------------------------------------------
/bloggy/models/verification_token.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.db import models
4 |
5 | from bloggy import settings
6 | from bloggy.services.token_generator import TOKEN_LENGTH
7 |
8 | TOKEN_TYPE = [
9 | ('signup', 'signup'),
10 | ('login', 'login'),
11 | ]
12 |
13 |
14 | def build_repr(instance, fields):
15 | values = [f"{f}={repr(getattr(instance, f))}" for f in fields]
16 | return f'{instance.__class__.__name__}({", ".join(values)})'
17 |
18 |
19 | class VerificationToken(models.Model):
20 | uuid = models.UUIDField(
21 | default=uuid.uuid4,
22 | primary_key=True,
23 | unique=True,
24 | db_index=True,
25 | help_text="A unique identifier for the instance.",
26 | verbose_name="uuid",
27 | )
28 |
29 | user = models.ForeignKey(
30 | settings.AUTH_USER_MODEL,
31 | help_text="The user who owns the email address.",
32 | on_delete=models.CASCADE,
33 | related_name="email_addresses",
34 | related_query_name="email_address",
35 | verbose_name="user",
36 | )
37 |
38 | token_type = models.CharField(
39 | max_length=20,
40 | choices=TOKEN_TYPE,
41 | help_text="Token type",
42 | verbose_name="Token type")
43 |
44 | time_created = models.DateTimeField(
45 | auto_now_add=True,
46 | help_text="The time that the token was created.",
47 | verbose_name="creation time",
48 | )
49 |
50 | token = models.CharField(
51 | help_text="The random token identifying the verification request.",
52 | max_length=TOKEN_LENGTH,
53 | unique=True,
54 | verbose_name="token",
55 | )
56 |
57 | class Meta:
58 | db_table = "bloggy_verification_token"
59 | ordering = ("time_created",)
60 | verbose_name = "verification token"
61 | verbose_name_plural = "verifications tokens"
62 |
63 | def __repr__(self):
64 | return build_repr(
65 | self,
66 | ["uuid", "time_created", "code"],
67 | )
68 |
--------------------------------------------------------------------------------
/bloggy/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/services/__init__.py
--------------------------------------------------------------------------------
/bloggy/services/account_manager.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import BaseUserManager
2 |
3 |
4 | class NewUserAccountManager(BaseUserManager):
5 |
6 | def create_superuser(self, name, email, password, **other_fields):
7 | other_fields.setdefault('is_staff', True)
8 | other_fields.setdefault('is_superuser', True)
9 | other_fields.setdefault('is_active', True)
10 |
11 | if other_fields.get('is_staff') is not True:
12 | raise ValueError('Superuser must be assigned to is_staff=True')
13 |
14 | if other_fields.get('is_superuser') is not True:
15 | raise ValueError('Superuser must be assigned to is_superuser=True')
16 |
17 | user = self.create_user(name=name, email=email, password=password, **other_fields)
18 | user.set_password(password)
19 | user.save()
20 | return user
21 |
22 | def create_user(self, name, email, password, **other_fields):
23 | if not email:
24 | raise ValueError('Email address is required!')
25 |
26 | if not name:
27 | raise ValueError('Please enter your name')
28 |
29 | email = self.normalize_email(email)
30 | if password is not None:
31 | user = self.model(email=email, name=name, password=password, **other_fields)
32 | user.save()
33 | else:
34 | user = self.model(email=email, name=name, password=password, **other_fields)
35 | user.set_unusable_password()
36 | user.save()
37 | return user
38 |
--------------------------------------------------------------------------------
/bloggy/services/email_service.py:
--------------------------------------------------------------------------------
1 | from django.core.mail import send_mail
2 | from django.template.loader import render_to_string
3 | from django.urls import reverse
4 |
5 | from bloggy import settings
6 |
7 |
8 | def send_custom_email(subject, recipients, template, args, from_email=settings.DEFAULT_FROM_EMAIL):
9 | email_body = render_to_string(template, args)
10 | send_mail(
11 | subject,
12 | email_body,
13 | from_email,
14 | recipients,
15 | fail_silently=False,
16 | html_message=email_body
17 | )
18 |
19 |
20 | def send_newsletter_verification_token(request, email, uuid, token):
21 | subject = f'Confirm to {settings.SITE_TITLE} newsletter'
22 |
23 | args = {
24 | "email_subject": subject,
25 | "app_name": settings.SITE_TITLE,
26 | "verification_link": request.build_absolute_uri(reverse("newsletter_verification", args=[uuid, token]))
27 | }
28 |
29 | send_custom_email(subject, [email], "email/newsletter_verification_token.html", args)
30 |
31 |
32 | def email_verification_token(request, new_user, token):
33 | subject = f"{settings.SITE_TITLE} confirmation code: {token.code}"
34 | args = {
35 | "email_subject": subject,
36 | "verification_code": token.code,
37 | "app_name": settings.SITE_TITLE,
38 | "verification_link": request.build_absolute_uri(reverse("otp_verification", args=[token.uuid]))
39 | }
40 | send_custom_email(subject, [new_user.email], "email/login_code_email.html", args)
41 |
42 |
43 | def email_registration_token(request, new_user, verification_token):
44 | subject = f'Welcome to {settings.SITE_TITLE}!'
45 | args = {
46 | "email_subject": subject,
47 | "user_name": new_user.name,
48 | "app_name": settings.SITE_TITLE,
49 | "verification_link": request.build_absolute_uri(reverse("activate_account", args=[
50 | verification_token.uuid,
51 | verification_token.token
52 | ]))
53 | }
54 |
55 | send_custom_email(subject, [new_user.email], "email/acc_active_email.html", args)
56 |
--------------------------------------------------------------------------------
/bloggy/services/gravtar.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import urllib
3 |
4 |
5 | def get_gravatar(email, size=400):
6 | default = "identicon"
7 | params = urllib.parse.urlencode({'d': default, 's': str(size)})
8 | return f"https://www.gravatar.com/avatar/{hashlib.md5(email.lower().encode('utf-8')).hexdigest()}?{params}"
9 |
--------------------------------------------------------------------------------
/bloggy/services/quiz_service.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logger = logging.getLogger(__name__)
4 |
5 |
6 | def get_questions_json(post):
7 | questions = []
8 | for question in post.get_questions():
9 |
10 | answers = []
11 | correct_answer = []
12 | question_answers = question.get_answers()
13 | for index, answer in enumerate(question_answers):
14 | key_char = chr(index + 97)
15 | answers.append({
16 | "value": answer.content,
17 | "key": key_char
18 | })
19 | if answer.correct:
20 | correct_answer.append(key_char)
21 |
22 | questions.append({
23 | "id": question.id,
24 | "title": question.title,
25 | "description": question.description if question.description else "",
26 | "type": question.type,
27 | "explanation": question.explanation if question.explanation else "",
28 | "answers": answers,
29 | "correctAnswer": correct_answer
30 | })
31 |
32 | category = post.category
33 | return {
34 | 'id': post.id,
35 | 'title': post.title,
36 | 'slug': post.slug,
37 | 'content': post.content if post.content else "",
38 | 'questions_count': len(questions),
39 | 'duration': post.duration,
40 | 'logo': post.thumbnail.url if post.thumbnail else "",
41 | 'questions': questions,
42 | 'time': post.duration,
43 | 'category': {
44 | "title": category.title,
45 | "slug": category.slug,
46 | "id": category.id
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/bloggy/services/sitemaps.py:
--------------------------------------------------------------------------------
1 | from django.contrib import sitemaps
2 | from django.contrib.sitemaps import GenericSitemap
3 | from django.urls import reverse
4 | from bloggy.models import Post, Category, User
5 | from bloggy.models.course import Course
6 | from bloggy.models.page import Page
7 |
8 |
9 | class StaticPagesSitemap(sitemaps.Sitemap):
10 | priority = 0.5
11 | changefreq = 'daily'
12 |
13 | def items(self):
14 | items = []
15 | staticPages = [
16 | 'index',
17 | 'courses',
18 | 'posts',
19 | 'categories',
20 | 'authors']
21 |
22 | for staticPage in staticPages:
23 | items.append(reverse(staticPage))
24 |
25 | pages = Page.objects.filter(publish_status="LIVE").all()
26 | for page in pages:
27 | items.append(f'/{page.url}')
28 | return items
29 |
30 | def location(self, item):
31 | return item
32 |
33 |
34 | sitemaps_list = {
35 | 'pages': StaticPagesSitemap,
36 | 'articles': GenericSitemap({
37 | 'queryset': Post.objects.filter(publish_status="LIVE").order_by("-published_date").all(),
38 | 'date_field': 'published_date'
39 | }, priority=0.6, changefreq='daily'),
40 |
41 | 'courses': GenericSitemap({
42 | 'queryset': Course.objects.filter(publish_status="LIVE").order_by("-published_date").all(),
43 | 'date_field': 'published_date'
44 | }, priority=0.6, changefreq='daily'),
45 |
46 | 'categories': GenericSitemap({
47 | 'queryset': Category.objects.filter(publish_status="LIVE").all(), 'date_field': 'updated_date'
48 | }, priority=0.6, changefreq='daily'),
49 |
50 | 'users': GenericSitemap({
51 | 'queryset': User.objects.exclude(username__in=["siteadmin", "superadmin", "admin"]).filter(is_staff=True).all()
52 | }, priority=0.6, changefreq='daily'),
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/bloggy/services/token_generator.py:
--------------------------------------------------------------------------------
1 | import six
2 | from django.contrib.auth.tokens import PasswordResetTokenGenerator
3 |
4 | TOKEN_LENGTH = 48
5 |
6 |
7 | class TokenGenerator(PasswordResetTokenGenerator):
8 |
9 | def _make_hash_value(self, user, timestamp):
10 | return (
11 | six.text_type(user.pk) + six.text_type(timestamp) +
12 | six.text_type(user.is_active)
13 | )
14 |
--------------------------------------------------------------------------------
/bloggy/services/token_service.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 | from datetime import timedelta
4 |
5 | from django.utils.timezone import now
6 |
7 | from bloggy.models import VerificationToken
8 |
9 | TOKEN_VALIDITY = 30
10 |
11 |
12 | def create_token(user, token_type):
13 | """
14 | Generate token:: Token and uuid will be generated automatically
15 | """
16 | token = VerificationToken.objects.filter(user=user, token_type=token_type).first()
17 | if token:
18 | time_difference = now() - token.time_created
19 | time_difference_in_minutes = time_difference / timedelta(minutes=1)
20 |
21 | # return the existing token, if it is not expired
22 | if time_difference_in_minutes < TOKEN_VALIDITY:
23 | return token
24 |
25 | token.delete()
26 |
27 | return VerificationToken.objects.create(user=user, token_type=token_type, token=generate_verification_code())
28 |
29 |
30 | def get_token(uuid, verification_token, token_type):
31 | return VerificationToken.objects \
32 | .filter(uuid__exact=uuid, token=verification_token, token_type=token_type) \
33 | .first()
34 |
35 |
36 | def is_token_expired(token):
37 | if not token:
38 | return True
39 |
40 | time_difference = now() - token.time_created
41 | time_difference_in_minutes = time_difference / timedelta(minutes=1)
42 | if time_difference_in_minutes > TOKEN_VALIDITY:
43 | return True
44 |
45 | return False
46 |
47 |
48 | def delete_token_by_uuid(uuid):
49 | return VerificationToken.objects.filter(uuid=uuid).delete()
50 |
51 |
52 | def generate_verification_code():
53 | # Generate a random 20-digit alphanumeric code similar to "JtM8t-MaV8y"
54 | code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + '-' + ''.join(
55 | random.choices(string.ascii_uppercase + string.digits, k=10))
56 | return code.lower()
57 |
--------------------------------------------------------------------------------
/bloggy/services/url_shortener.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | import logging
4 | import os
5 |
6 | import requests
7 | from requests import RequestException
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | class UrlShortener:
13 | FIREBASE_API_KEY = os.getenv('FIREBASE_API_KEY')
14 | FIREBASE_DYNAMIC_LINKS_DOMAIN = os.getenv('FIREBASE_DYNAMIC_LINKS_DOMAIN')
15 |
16 | def shorten_url(self, original_link):
17 | response_json = self.firebase_api(original_link)
18 | logger.debug("Response from Firebase dynamic link %s", response_json)
19 | if response_json:
20 | response = json.loads(response_json)
21 | return response["shortLink"]
22 |
23 | return original_link
24 |
25 | def firebase_api(self, original_link):
26 | try:
27 | url = f"https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key={UrlShortener.FIREBASE_API_KEY}"
28 | headers = {'Content-Type': 'application/json'}
29 | payload = json.dumps({
30 | "longDynamicLink": f"{UrlShortener.FIREBASE_DYNAMIC_LINKS_DOMAIN}?link={original_link}"
31 | })
32 |
33 | response = requests.post(url, data=payload, headers=headers)
34 | if response.status_code == 200:
35 | return response.text
36 |
37 | except RequestException:
38 | print("ERROR: while shorting the url")
39 |
40 | return None
41 |
--------------------------------------------------------------------------------
/bloggy/shortcodes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/shortcodes/__init__.py
--------------------------------------------------------------------------------
/bloggy/signals.py:
--------------------------------------------------------------------------------
1 | import urllib.parse
2 | from urllib import request
3 | from urllib.error import URLError
4 |
5 | import django.core
6 | from django.db.models.signals import post_save
7 | from django.dispatch import receiver
8 |
9 | import bloggy.models
10 | from bloggy import settings
11 |
12 | PING_GOOGLE_URL = "https://www.google.com/webmasters/tools/ping"
13 | INDEX_NOW = "https://www.bing.com/indexnow?url={}&key={}"
14 |
15 |
16 | @receiver(post_save, sender=bloggy.models.Post)
17 | def post_saved_action_signal(sender, instance, created, **kwargs):
18 | # Update category count everytime three is a new object added
19 | if created:
20 | print(f"Sendr:{sender}, kwargs:{kwargs}")
21 | django.core.management.call_command('update_category_count')
22 |
23 | if instance.publish_status == "PUBLISHED":
24 | if settings.PING_GOOGLE_POST_UPDATE:
25 | ping_google()
26 |
27 | if settings.PING_INDEX_NOW_POST_UPDATE:
28 | ping_index_now(instance)
29 |
30 |
31 | def ping_google():
32 | try:
33 | sitemap_url = f"{settings.SITE_URL}/sitemap.xml"
34 | params = urllib.parse.urlencode({"sitemap": sitemap_url})
35 |
36 | with request.urlopen(f"{PING_GOOGLE_URL}?{params}") as response:
37 | if response.code == 200:
38 | print("Successfully pinged this page for Google!")
39 | except URLError as e:
40 | print(f"Error while pinging Google: {e}")
41 |
42 |
43 | def ping_index_now(article):
44 | try:
45 | url = INDEX_NOW.format(article.get_absolute_url(), settings.INDEX_NOW_API_KEY)
46 | with request.urlopen(url) as response:
47 | if response.code == 200:
48 | print("Successfully pinged this page for IndexNow!")
49 | except URLError as e:
50 | print(f"Error while pinging IndexNow: {e}")
51 |
--------------------------------------------------------------------------------
/bloggy/storage_backends.py:
--------------------------------------------------------------------------------
1 | from django.core.files.storage import FileSystemStorage
2 | from storages.backends.s3boto3 import S3Boto3Storage
3 |
4 |
5 | class StaticStorage(FileSystemStorage):
6 | location = 'media'
7 | default_acl = 'public-read'
8 |
9 |
10 | class PublicMediaStorage(S3Boto3Storage):
11 |
12 | location = 'media'
13 | default_acl = 'public-read'
14 | file_overwrite = False
15 |
16 |
17 | class PrivateMediaStorage(S3Boto3Storage):
18 | location = 'private'
19 | default_acl = 'private'
20 | file_overwrite = False
21 | custom_domain = False
22 |
--------------------------------------------------------------------------------
/bloggy/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 | {% load static %}
3 |
4 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
5 |
6 | {% block extrastyle %}{{ block.super }}
7 |
8 |
9 |
25 | {# #}
26 | {# #}
27 | {# #}
28 |
29 | {% endblock %}
30 |
31 | {% block branding %}
32 |
42 | {% endblock %}
43 |
44 | {% block nav-global %}{% endblock %}
--------------------------------------------------------------------------------
/bloggy/templates/base-with-header.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% include "seo/seotags.html" %}
9 | {% block jsonld %}{% endblock jsonld %}
10 | {% include "seo/header_scripts.html" %}
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
23 |
24 | {% if messages %}
25 |
26 | {% for message in messages %}
27 |
28 | {{ message }}
29 |
30 | {% endfor %}
31 |
32 | {% endif %}
33 | {% block content %}{% endblock content %}
34 |
35 |
36 |
--------------------------------------------------------------------------------
/bloggy/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% spaceless %}
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% include "seo/seotags.html" %}
10 | {% block jsonld %}{% endblock jsonld %}
11 | {% include "seo/header_scripts.html" %}
12 |
13 |
14 |
15 | {% if not debug %}
16 |
17 |
18 |
20 |
21 |
22 | {% endif %}
23 | {% block content %}{% endblock content %}
24 |
25 |
26 | {% endspaceless %}
27 |
--------------------------------------------------------------------------------
/bloggy/templates/email/contact_form.tpl:
--------------------------------------------------------------------------------
1 | {% extends "mail_templated/base.tpl" %}
2 |
3 | {% block subject %}
4 | Contact form
5 | {% endblock %}
6 |
7 | {% block body %}
8 |
9 | {% endblock %}
10 |
11 | {% block html %}
12 | Name: {{ name }}
13 | Website: {{ website }}
14 | Email: {{email}}
15 | Message: {{message}}
16 | {% endblock %}
--------------------------------------------------------------------------------
/bloggy/templates/email/newsletter_verification_token.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ email_subject }}
7 |
33 |
34 |
35 | Thank you for signing up for my email newsletter!
36 | Please complete the process by
37 | clicking here to confirm your registration.
38 | Verify your email
39 | If you didn't request this email, there's nothing to worry about - you can safely ignore
40 | it.
41 |
42 | Thank you,
43 | StackTips Team
44 |
45 |
--------------------------------------------------------------------------------
/bloggy/templates/errors/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% load custom_widgets %}
4 | {% load define_action %}
5 | {% block content %}
6 |
7 |
{{ errorCode }}
8 | {{ errorMessage }}
9 |
10 |
{{ errorDescription }}.
11 | Please do return to our homepage and continue learning.
12 |
13 |
14 |
15 |
Here are some of the useful links:
16 | {% include "errors/error_page_links.html" %}
17 |
18 |
19 | {% include "errors/search_widget.html" %}
20 |
21 |
22 |
23 | {% endblock content %}
24 |
--------------------------------------------------------------------------------
/bloggy/templates/errors/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 |
4 |
{{ errorCode }}
5 | {{ errorMessage }}
6 |
7 |
{{ errorDescription }}.
8 | Please do return to our homepage and continue learning.
9 |
10 |
11 |
12 |
Here are some of the useful links:
13 | {% include "errors/error_page_links.html" %}
14 |
15 |
16 | {% include "errors/search_widget.html" %}
17 |
18 |
19 |
20 | {% endblock content %}
21 |
--------------------------------------------------------------------------------
/bloggy/templates/errors/error_page_links.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/errors/search_widget.html:
--------------------------------------------------------------------------------
1 |
2 |
Or, try searching..
3 |
11 |
--------------------------------------------------------------------------------
/bloggy/templates/forms/widgets/non_clearable_imagefield.html:
--------------------------------------------------------------------------------
1 | {% if widget.is_initial %}
2 |
3 |
7 |
8 | {% endif %}
9 |
10 |
11 | {% if widget.is_initial %}
12 | {% endif %}
--------------------------------------------------------------------------------
/bloggy/templates/pages/archive/courses.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% spaceless %}
4 | {% load custom_widgets %}
5 | {% load define_action %}
6 | {% block content %}
7 |
8 |
9 |
10 |
Courses
11 |
12 | Want to learn a topic? StackTips offers free courses on Java, Python, HTML, JavaScript and CSS.
13 | Enroll in
14 | these free courses and learn a new programming language.
15 |
16 |
17 |
18 |
19 | {% if courses|length <= 0 %}
20 |
No contents found!
21 | {% endif %}
22 |
23 | {% for course in courses %}
24 | {% include 'partials/course_grid_column.html' %}
25 | {% endfor %}
26 |
27 |
28 | {% endblock content %}
29 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/pages/authors.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% load custom_widgets %}
4 | {% block content %}
5 |
6 |
7 |
Our Authors
8 |
9 |
10 |
11 |
12 | {% for author in authors %}
13 |
14 |
15 |
17 |
21 |
22 |
23 |
24 |
26 | {{ author.get_full_name }}
27 |
28 | {% if author.bio %}
29 |
{{ author.bio |striptags }}
31 | {% endif %}
32 |
33 |
34 | {% include 'partials/user_profile_social_media_links.html' with user=author %}
35 |
36 |
37 | {% endfor %}
38 |
39 |
40 |
41 | {% endblock %}
--------------------------------------------------------------------------------
/bloggy/templates/pages/contact.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load custom_widgets %}
3 | {% load static %}
4 | {% block content %}
5 |
6 |
7 |
10 |
11 |
12 | Get in touch
13 |
14 |
15 |
16 | We are here to answer any questions you may have about this website experience or have
17 | suggestions to improve the quality, let us know.
18 |
19 |
20 | Please note, we do not respond to questions on any specific tutorial or code error via
21 | this
22 | contact form. If you have any such questions, post your comment on the tutorial in the
23 | comments
24 | section.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {% endblock content %}
38 |
--------------------------------------------------------------------------------
/bloggy/templates/pages/page.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% load custom_widgets %}
4 | {% load define_action %}
5 | {% block content %}
6 |
7 |
8 |
11 |
12 |
13 | {{ page.title }}
14 | {% if page.excerpt %}
15 | {{ page.excerpt }}
16 | {% endif %}
17 | {{ page.content|expand_media_url }}
18 |
19 |
20 |
21 |
22 | {% endblock %}
--------------------------------------------------------------------------------
/bloggy/templates/pages/user.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% block content %}
4 | {% load custom_widgets %}
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 | {% if user.get_full_name %}
20 |
{{ user.get_full_name }}
21 | {% else %}
22 |
{{ user.username }}
23 | {% endif %}
24 |
25 | {% if user.bio %}
26 |
{{ user.bio | safe }}
27 | {% endif %}
28 |
29 |
30 | {% include 'partials/user_profile_social_media_links.html' with user=user %}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
My articles(so far)
41 |
42 | {% for post in posts %}
43 |
{% include "partials/post_list_item.html" with post=post cssClass="" %}
44 | {% endfor %}
45 |
46 | {% include 'partials/paging.html' with posts=posts %}
47 |
48 |
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/article_bookmark_row_list.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% spaceless %}
3 | {% load custom_widgets %}
4 | {% load define_action %}
5 |
6 |
7 |
8 |
9 |
12 |
13 | Updated on {{ post.updated_date|date:"M j, Y" }}
14 | {{ post.publish_status | capfirst }}
15 | {{ post.post_type | capfirst }}
16 |
17 |
18 |
19 |
20 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/article_meta_container.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% load define_action %}
4 | {% load shortcodes_filters %}
5 | {% spaceless %}
6 | {% define post.author as author %}
7 |
8 |
9 |
15 |
16 |
17 |
38 |
39 | {% endspaceless %}
40 |
41 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/article_row_mini_grid.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% load define_action %}
4 | {% spaceless %}
5 |
6 |
7 |
8 |
9 |
10 | {% pretty_date post.updated_date %}
11 |
12 |
13 |
14 |
17 |
18 |
19 | {% if post.video_id %}
20 |
21 |
22 |
25 |
26 |
27 | {% elif post.thumbnail %}
28 |
29 |
30 |
32 |
33 |
34 | {% endif %}
35 |
36 |
37 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/author_widget.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/category_archive_banner.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% spaceless %}
3 | {% if category %}
4 |
5 |
6 |
7 | {% if category.thumbnail %}
8 |
9 |
10 |
11 | {% endif %}
12 |
13 |
14 |
{{ category.title }}
15 | {% if category.excerpt %}
{{ category.excerpt }}
{% endif %}
16 |
17 |
18 |
19 | {% endif %}
20 | {% endspaceless %}
21 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/course_grid_column.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% if course.thumbnail %}
4 |
6 | {% endif %}
7 |
8 |
12 |
{{ course.excerpt|truncatechars:110 }}
13 |
15 | Start Course →
16 |
17 |
28 |
29 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/course_row_grid.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% load define_action %}
4 | {% spaceless %}
5 |
6 | {% for course in courses %}
7 |
8 |
9 |
10 |
12 |
{{ course.excerpt|slice:"0:160" }}..
14 |
Start Course →
16 |
17 |
18 |
19 | {% endfor %}
20 |
21 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/dashboard_menu.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 |
20 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/footer.html:
--------------------------------------------------------------------------------
1 | {% spaceless %}
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/github_widget.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% spaceless %}
4 | {% if githubLink %}
5 |
6 |
9 |
17 |
18 | {% endif %}
19 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/home_article_breadcrumb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Home
7 |
8 |
9 | Articles
10 |
11 |
12 | {{ post.title }}
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/home_lesson_breadcrumb.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/home_widget_contribute_cta.html:
--------------------------------------------------------------------------------
1 |
2 |
Write for us
3 |
We need your help in writing fresh new content for this site. Write for StackTips and earn while
4 | you learn!
5 |
Start here
6 |
7 |
Say hello, to our authors
8 |
9 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/home_widget_course_toc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ course.title }}
4 |
5 | {% for lesson in course.get_lessons %}
6 | {% url 'lesson_single' course=course.slug slug=lesson.slug as current_url %}
7 |
8 | {{ lesson.title }}
10 |
11 | {% endfor %}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/home_widget_join_cta.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/templates/partials/home_widget_join_cta.html
--------------------------------------------------------------------------------
/bloggy/templates/partials/lesson_single_toc.html:
--------------------------------------------------------------------------------
1 | {% load custom_widgets %}
2 | {% load define_action %}
3 | {% if course %}
4 |
5 |
6 |
{{ course.title }}
7 |
8 | {% for lesson in course.get_lessons %}
9 | {% url 'lesson_single' course=course.slug slug=lesson.slug as current_url %}
10 |
11 | {{ lesson.title }}
13 |
14 | {% endfor %}
15 |
16 |
17 |
18 | {% endif %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/newsletter.html:
--------------------------------------------------------------------------------
1 | {% if user.is_authenticated == False %}
2 |
24 | {% endif %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/pages_menu.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 |
27 |
28 |
31 |
32 | Select page
33 |
34 |
35 | About us
36 |
37 |
38 | Privacy policy
39 |
40 |
41 | Terms of service
42 |
43 |
44 | Cookie policy
45 |
46 |
47 | Write for us
48 |
49 |
50 | Code of conduct
51 |
52 |
53 | Get in touch
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/paging.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% spaceless %}
3 | {% load custom_widgets %}
4 | {% if posts.has_other_pages %}
5 |
6 |
7 | Showing page {{ posts.number }} of {{ posts.paginator.num_pages }}
8 |
9 |
23 |
24 |
25 | {% endif %}
26 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/partials/social_share.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/theme_switch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {# Change Theme #}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/toc_widget.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
--------------------------------------------------------------------------------
/bloggy/templates/partials/video_widget.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% load define_action %}
4 | {% load shortcodes_filters %}
5 | {% if post.video_id %}
6 |
7 | {% with '[youtube=https://www.youtube.com/watch?v='|addstr:post.video_id|addstr:']' as youtubeUrl %}
8 | {{ youtubeUrl|shortcodes|safe }}
9 | {% endwith %}
10 |
11 |
16 | {% endif %}
--------------------------------------------------------------------------------
/bloggy/templates/profile/user_bookmarks.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% load custom_widgets %}
4 | {% block base_css_class %}bg-light{% endblock base_css_class %}
5 | {% block content %}
6 |
7 |
8 |
9 | {% include 'partials/dashboard_menu.html' %}
10 |
11 |
12 |
13 | {% if posts %}
14 |
15 |
Bookmarks
16 | {% for post in posts %}
17 | {% include 'partials/article_bookmark_row_list.html' with post=post %}
18 | {% endfor %}
19 |
20 | {% endif %}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/bloggy/templates/profile/user_dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "base-with-header-footer.html" %}
2 | {% load static %}
3 | {% load custom_widgets %}
4 | {% block base_css_class %}bg-light{% endblock base_css_class %}
5 | {% block content %}
6 |
7 |
8 |
9 |
10 | {% include 'partials/dashboard_menu.html' %}
11 |
12 |
13 |
14 | {% if posts %}
15 |
16 |
My articles
17 |
18 | {% for post in posts %}
19 | {% include 'partials/article_row_list.html' with posts=posts %}
20 | {% endfor %}
21 |
22 | {% include 'partials/paging.html' with posts=posts %}
23 |
24 | {% endif %}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/bloggy/templates/seo/article_jsonld.html:
--------------------------------------------------------------------------------
1 | {% load custom_widgets %}
2 |
--------------------------------------------------------------------------------
/bloggy/templates/seo/course_jsonld.html:
--------------------------------------------------------------------------------
1 | {% load custom_widgets %}
2 |
3 |
--------------------------------------------------------------------------------
/bloggy/templates/seo/footer_scripts.html:
--------------------------------------------------------------------------------
1 | {% load static %}
--------------------------------------------------------------------------------
/bloggy/templates/seo/header_scripts.html:
--------------------------------------------------------------------------------
1 | {% spaceless %}
2 | {% load static %}
3 | {% if debug %}
4 |
5 | {% else %}
6 |
7 | {% endif %}
8 |
9 |
10 |
11 | {% if LOAD_GOOGLE_TAG_MANAGER %}
12 |
13 |
14 |
15 |
16 |
22 |
23 | {% endif %}
24 |
25 |
26 |
27 | {% if request.resolver_match.url_name == 'post_single' or request.resolver_match.url_name == 'lesson_single' %}
28 |
29 | {% endif %}
30 |
31 | {% if not request.resolver_match.url_name == 'index'%}
32 |
33 | {% endif %}
34 |
35 | {% if LOAD_GOOGLE_ADS %}
36 |
37 | {% endif %}
38 | {% csrf_token %}
39 |
40 | {% endspaceless %}
--------------------------------------------------------------------------------
/bloggy/templates/seo/lesson_jsonld.html:
--------------------------------------------------------------------------------
1 | {% load custom_widgets %}
2 |
3 |
--------------------------------------------------------------------------------
/bloggy/templates/sitemap_template.html:
--------------------------------------------------------------------------------
1 | {{ url.lastmod|date:"Y-m-d" }}
--------------------------------------------------------------------------------
/bloggy/templates/social_share/copy_script.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/social_share/copy_to_clipboard.html:
--------------------------------------------------------------------------------
1 |
3 |
4 | {{ link_text }}
5 |
6 |
--------------------------------------------------------------------------------
/bloggy/templates/social_share/post_to_facebook.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/social_share/post_to_linkedin.html:
--------------------------------------------------------------------------------
1 | {% load i18n social_share %}{% get_current_language as LANGUAGE_CODE %}
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/bloggy/templates/social_share/post_to_twitter.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/social_share/send_email.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy/templates/widgets/categories.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% if categories %}
3 | {% if widgetStyle == "home" %}
4 |
16 | {% else %}
17 |
23 | {% endif %}
24 | {% endif %}
25 |
--------------------------------------------------------------------------------
/bloggy/templates/widgets/related_posts.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load custom_widgets %}
3 | {% load define_action %}
4 | {% if relatedPosts|length > 0 %}
5 |
6 | {% if widgetStyle == "list" %}
7 | {{ widgetTitle }}
8 |
9 | {% for post in relatedPosts %}
10 |
11 | {{ post.title }}
13 |
14 | {% endfor %}
15 |
16 |
17 |
18 | {% elif widgetStyle == "grid" %}
19 |
20 | {% for post in relatedPosts %}
21 |
{% include "partials/post_list_item.html" with post=post cssClass="" %}
22 | {% endfor %}
23 |
24 |
25 |
26 | {% elif widgetStyle == "miniGrid" %}
27 |
28 |
{{ widgetTitle }}
29 |
30 | {% for post in relatedPosts %}
31 |
{% include "partials/article_row_mini_grid.html" with post=post cssClass="" %}
32 | {% endfor %}
33 |
34 |
35 | {% endif %}
36 |
37 | {% endif %}
--------------------------------------------------------------------------------
/bloggy/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/templatetags/__init__.py
--------------------------------------------------------------------------------
/bloggy/templatetags/define_action.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import stringfilter
3 | from django.utils.safestring import mark_safe
4 |
5 | from bloggy import settings
6 |
7 | register = template.Library()
8 |
9 |
10 | @register.simple_tag
11 | def define(val=None):
12 | return val
13 |
14 |
15 | @register.filter(is_safe=True)
16 | @stringfilter
17 | def expand_media_url(value):
18 | """Mark the value as a string that should not be auto-escaped."""
19 | if not settings.DEBUG:
20 | value = value.replace("\"/media/uploads/", "\"" + settings.ASSETS_DOMAIN + "/media/uploads/")
21 |
22 | return mark_safe(value)
23 |
--------------------------------------------------------------------------------
/bloggy/templatetags/shortcodes_filters.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from bloggy.shortcodes import parser
4 |
5 | register = template.Library()
6 |
7 |
8 | def shortcodes_replace(value):
9 | return parser.parse(value)
10 |
11 |
12 | register.filter('shortcodes', shortcodes_replace)
13 |
--------------------------------------------------------------------------------
/bloggy/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy/utils/__init__.py
--------------------------------------------------------------------------------
/bloggy/utils/string_utils.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.core import serializers
4 |
5 |
6 | class StringUtils:
7 | @staticmethod
8 | def is_blank(text):
9 | return not (text and text.strip())
10 |
11 | @staticmethod
12 | def is_not_blank(text):
13 | return bool(text and text.strip())
14 |
15 | @staticmethod
16 | def to_json(text):
17 | tmp_json = serializers.serialize("json", text)
18 | return json.dumps(json.loads(tmp_json))
19 |
--------------------------------------------------------------------------------
/bloggy/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .edit_profile_view import EditProfileView
2 | from .error_views import handler_404
3 | from .error_views import handler_500
4 | from .pages import IndexView
5 | from .register import RegisterView
6 | from .user import MyProfileView
7 | from .user import PublicProfileView
8 |
--------------------------------------------------------------------------------
/bloggy/views/account.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.contrib.auth.models import Group
3 | from django.shortcuts import redirect
4 | from django.views import View
5 |
6 | from bloggy import settings
7 | from bloggy.models import User
8 | from bloggy.services.token_service import get_token, is_token_expired
9 |
10 |
11 | class AccountActivationView(View):
12 | def get(self, request, uuid, token):
13 |
14 | verification_token = get_token(uuid, token, token_type="signup")
15 | if is_token_expired(verification_token):
16 | messages.error(request, "The verification link is expired or malformed.")
17 | return redirect('index')
18 |
19 | # activate user
20 | user = User.objects.get(email=verification_token.user.email)
21 | user.is_active = True
22 | user.is_staff = False
23 | group = Group.objects.get_or_create(name=settings.AUTH_USER_DEFAULT_GROUP)
24 | user.groups.add(group[0].id)
25 | user.save()
26 |
27 | # delete token as it
28 | verification_token.delete()
29 |
30 | messages.success(request, "You're all set! Your account is now active and ready to use.")
31 | return redirect('login')
32 |
--------------------------------------------------------------------------------
/bloggy/views/error_views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 |
4 | def handler_404(request, exception):
5 | context = {
6 | 'meta_title': "404 Page not found",
7 | 'errorCode': '404.',
8 | 'errorMessage': "Oops! That's an error.",
9 | 'errorDescription': "The requested URL was not found on this server."
10 | }
11 | return render(request, 'errors/404.html', context)
12 |
13 |
14 | def handler_500(request):
15 | context = {
16 | 'meta_title': "500 Server error",
17 | 'errorCode': '500.',
18 | 'errorMessage': "Oops! That's an error.",
19 | 'errorDescription': "The server encountered an error and couldn't complete your request."
20 | }
21 | return render(request, 'errors/500.html', context)
22 |
--------------------------------------------------------------------------------
/bloggy/views/login.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.views import LoginView
2 | from django.urls import reverse
3 |
4 |
5 | class MyLoginView(LoginView):
6 |
7 | def get_success_url(self):
8 | redirect_url = self.request.GET.get('next')
9 | if redirect_url:
10 | return redirect_url
11 |
12 | return reverse('index')
13 |
--------------------------------------------------------------------------------
/bloggy/views/quizzes_view.py:
--------------------------------------------------------------------------------
1 | from django.utils.decorators import method_decorator
2 | from django.views.decorators.cache import cache_page
3 | from django.views.decorators.vary import vary_on_cookie
4 | from django.views.generic import ListView
5 | from hitcount.views import HitCountDetailView
6 |
7 | from bloggy import settings
8 | from bloggy.models.quizzes import Quiz
9 | from bloggy.services.post_service import DEFAULT_PAGE_SIZE, get_recent_quizzes, set_seo_settings
10 |
11 |
12 | @method_decorator([
13 | cache_page(settings.CACHE_TTL, key_prefix="quizzes"),
14 | vary_on_cookie],
15 | name='dispatch'
16 | )
17 | class QuizListView(ListView):
18 | model = Quiz
19 | template_name = "pages/archive/quizzes.html"
20 | paginate_by = DEFAULT_PAGE_SIZE
21 |
22 | def get_context_data(self, **kwargs):
23 | context = super(QuizListView, self).get_context_data(**kwargs)
24 | context['quizzes'] = get_recent_quizzes()
25 | return context
26 |
27 |
28 | @method_decorator(
29 | [cache_page(settings.CACHE_TTL, key_prefix="quiz_single"), vary_on_cookie],
30 | name='dispatch'
31 | )
32 | class QuizDetailView(HitCountDetailView):
33 | model = Quiz
34 | template_name = "pages/single/quiz.html"
35 |
36 | def dispatch(self, request, *args, **kwargs):
37 | return super().dispatch(request, *args, **kwargs)
38 |
39 | def get_context_data(self, **kwargs):
40 | context = super().get_context_data(**kwargs)
41 | set_seo_settings(post=self.object, context=context)
42 | return context
43 |
--------------------------------------------------------------------------------
/bloggy/views/register.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect, reverse
2 | from django.views import View
3 |
4 | from bloggy.forms.signup_form import SignUpForm
5 | from bloggy.services import email_service
6 | from bloggy.services.token_service import create_token
7 |
8 |
9 | class RegisterView(View):
10 |
11 | def get(self, request):
12 | return render(request, 'auth/register.html', {'form': SignUpForm()})
13 |
14 | def post(self, request):
15 | form = SignUpForm(request.POST)
16 | if form.is_valid():
17 | user = form.save()
18 |
19 | verification_token = create_token(user, token_type="signup")
20 | email_service.email_registration_token(request, user, verification_token)
21 | return redirect(reverse('login'))
22 |
23 | return render(request, 'auth/register.html', {'form': form})
24 |
--------------------------------------------------------------------------------
/bloggy/views/search.py:
--------------------------------------------------------------------------------
1 | from itertools import chain
2 |
3 | from django.views.generic import ListView
4 |
5 | from bloggy.models import Category, Post
6 | from bloggy.utils.string_utils import StringUtils
7 |
8 | DEFAULT_PAGE_SIZE = 30
9 |
10 |
11 | class SearchListView(ListView):
12 | model = Post
13 | template_name = "pages/search_result.html"
14 | paginate_by = DEFAULT_PAGE_SIZE
15 |
16 | def get_context_data(self, **kwargs):
17 | search_query = self.request.GET.get("q")
18 | context = super().get_context_data(**kwargs)
19 |
20 | if StringUtils.is_not_blank(search_query):
21 | categories = Category.objects.filter(slug__icontains=search_query)[:5]
22 | results = chain(
23 | Post.objects.filter(title__icontains=search_query, excerpt__icontains=search_query, publish_status="LIVE"),
24 | )
25 |
26 | context['posts'] = results
27 | context['categories'] = categories
28 | context['search_query'] = search_query
29 | context['meta_title'] = f"Search result for {search_query}"
30 | context['meta_description'] = "Search articles"
31 |
32 | return context
33 |
--------------------------------------------------------------------------------
/bloggy/views/user_collections.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import get_object_or_404
2 | from django.views.generic import TemplateView
3 |
4 | from bloggy.models import User, Post
5 |
6 |
7 | class UserBookmarksView(TemplateView):
8 | template_name = "profile/user_bookmarks.html"
9 |
10 | def get_context_data(self, *args, **kwargs):
11 | context = super().get_context_data(*args, **kwargs)
12 | username = self.request.user.username
13 | user = get_object_or_404(User, username=username)
14 |
15 | articles = Post.objects.raw('''
16 | select a.id as id, a.title as title, a.slug as slug, a.publish_status as publish_status, a.thumbnail as thumbnail, b.updated_date as bookmark_date from bloggy_article a JOIN bloggy_bookmarks b on a.id=b.post_id where b.user_id=%s and b.post_type=%s
17 | ''', ([user.id], "article"))
18 |
19 | context.update({
20 | 'articles': articles,
21 | 'userProfile': user,
22 | 'userType': "self",
23 | })
24 | return context
25 |
--------------------------------------------------------------------------------
/bloggy/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for bloggy 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/2.1/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', 'bloggy.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/bloggy_api/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_api/.DS_Store
--------------------------------------------------------------------------------
/bloggy_api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_api/__init__.py
--------------------------------------------------------------------------------
/bloggy_api/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class BloggyApiConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'bloggy_api'
7 |
--------------------------------------------------------------------------------
/bloggy_api/exception/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_api/exception/__init__.py
--------------------------------------------------------------------------------
/bloggy_api/exception/not_found_exception.py:
--------------------------------------------------------------------------------
1 | from rest_framework.exceptions import APIException
2 |
3 |
4 | class NotFoundException(APIException):
5 | status_code = 400
6 | default_detail = "Content not found"
7 | default_code = "bad_request"
8 |
--------------------------------------------------------------------------------
/bloggy_api/exception/rest_exception_handler.py:
--------------------------------------------------------------------------------
1 | from rest_framework.views import exception_handler
2 |
3 |
4 | def rest_exception_handler(exc, context):
5 | # Call REST framework's default exception handler first,
6 | # to get the standard error response.
7 | response = exception_handler(exc, context)
8 |
9 | # Now add the HTTP status code to the response.
10 | if response is not None:
11 | response.data['status_code'] = response.status_code
12 |
13 | return response
14 |
--------------------------------------------------------------------------------
/bloggy_api/exception/unauthorized_access.py:
--------------------------------------------------------------------------------
1 | from rest_framework.exceptions import APIException
2 |
3 |
4 | class UnauthorizedAccess(APIException):
5 | status_code = 401
6 | default_detail = "You're not authorized to perform this action."
7 | default_code = "unauthorized"
8 |
--------------------------------------------------------------------------------
/bloggy_api/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_api/migrations/__init__.py
--------------------------------------------------------------------------------
/bloggy_api/pagination.py:
--------------------------------------------------------------------------------
1 | import math
2 | from collections import OrderedDict
3 |
4 | from rest_framework.pagination import PageNumberPagination
5 | from rest_framework.response import Response
6 |
7 |
8 | class CustomPaginatedResponse(PageNumberPagination):
9 |
10 | def get_paginated_response(self, data):
11 | return Response(OrderedDict([
12 | ('count', self.page.paginator.count),
13 | ('size', self.page_size),
14 | ('page', self.page.number),
15 | ('total_pages', math.ceil(self.page.paginator.count / self.page_size)),
16 | ('items', data)
17 | ]))
18 |
19 | def get_next_link(self):
20 | if not self.page.has_next():
21 | return None
22 | page_number = self.page.next_page_number()
23 | return page_number
24 |
25 | def get_previous_link(self):
26 | if not self.page.has_previous():
27 | return None
28 | page_number = self.page.previous_page_number()
29 | return page_number
30 |
--------------------------------------------------------------------------------
/bloggy_api/service/recaptcha.py:
--------------------------------------------------------------------------------
1 | import json
2 | from urllib import request
3 | from urllib.error import URLError
4 |
5 | from bloggy import settings
6 |
7 |
8 | def recaptcha_verify(recaptcha_response):
9 | data = {
10 | 'secret': settings.GOOGLE_RECAPTHCA_SECRET_KEY,
11 | 'response': recaptcha_response
12 | }
13 |
14 | try:
15 | with request.urlopen(settings.GOOGLE_RECAPTHCA_TOKEN_VERIFY_URL,
16 | data=json.dumps(data).encode('utf-8')) as response:
17 | if response.status == 200:
18 | result = json.loads(response.read().decode('utf-8'))
19 | print("Recaptcha verification:", result)
20 | return True
21 | except URLError as e:
22 | print("Recaptcha verification error:", e)
23 | return False
24 |
--------------------------------------------------------------------------------
/bloggy_api/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import login_required
2 | from django.urls import path
3 |
4 | from bloggy_api.views import *
5 | # from bloggy_api.views.category_api import CategoryAPIView
6 | # from bloggy_api.views.comments_api_view import CommentsAPIView
7 | # from bloggy_api.views.course_api import CoursesAPIView
8 | # from bloggy_api.views.newsletter_api import NewsletterApi
9 | # from bloggy_api.views.user_api import UsersAPIView
10 | # from bloggy_api.views.vote_api import VoteAPIView
11 | # from bloggy_api.views.bookmark_api import BookmarkAPIView
12 | # from bloggy_api.views.quizzes_api import QuizDetailsAPIView, QuizzesAPIView
13 | # from bloggy_api.views.articles_api import ArticleAPIView, ArticleDetailsAPIView
14 |
15 | urlpatterns = [
16 | path('categories', CategoryAPIView.as_view()),
17 | path('courses', CoursesAPIView.as_view()),
18 |
19 | path('articles', ArticleAPIView.as_view()),
20 | path('articles/', ArticleDetailsAPIView.as_view()),
21 |
22 | path('users/', login_required(UsersAPIView.as_view())),
23 |
24 | path('vote', VoteAPIView.as_view(), name='vote'),
25 | path('bookmark', BookmarkAPIView.as_view()),
26 |
27 | path('newsletter/subscribe', NewsletterApi.as_view({'post': 'subscribe'})),
28 | path('newsletter/confirm//',
29 | NewsletterApi.as_view({'get': 'confirm'}), name='newsletter_verification'),
30 |
31 | path('comments', CommentsAPIView.as_view()),
32 | path('comments/', CommentsAPIView.as_view()),
33 |
34 | path('quizzes', QuizzesAPIView.as_view()),
35 | path('quizzes/', QuizDetailsAPIView.as_view()),
36 | ]
37 |
--------------------------------------------------------------------------------
/bloggy_api/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .user_api import UsersAPIView
2 | from .category_api import CategoryAPIView
3 | from .articles_api import ArticleAPIView, ArticleDetailsAPIView
4 | from .bookmark_api import BookmarkAPIView
5 | from .comments_api_view import CommentsAPIView
6 | from .course_api import CoursesAPIView
7 | from .newsletter_api import SubscriberSerializer, NewsletterApi
8 | from .quizzes_api import QuizzesAPIView, QuizDetailsAPIView
9 | from .vote_api import VoteAPIView
10 |
--------------------------------------------------------------------------------
/bloggy_api/views/articles_api.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 | from rest_framework.pagination import PageNumberPagination
3 |
4 | from bloggy.models import Post
5 | from bloggy_api.serializers import ArticleSerializer
6 |
7 |
8 | class StandardResultsSetPagination(PageNumberPagination):
9 | page_size = 50
10 | page_size_query_param = 'page_size'
11 |
12 |
13 | class ArticleAPIView(generics.ListAPIView):
14 | serializer_class = ArticleSerializer
15 |
16 | def get_queryset(self):
17 | queryset = Post.objects.filter(publish_status="LIVE") \
18 | .order_by("-published_date")
19 |
20 | categories = self.request.query_params.getlist('category', None)
21 | if categories and len(categories) > 0:
22 | queryset = queryset.filter(category__slug__in=categories)
23 |
24 | post_type = self.request.query_params.getlist('post_type', None)
25 | if post_type and len(post_type) > 0:
26 | queryset = queryset.filter(post_type__in=post_type)
27 |
28 | return queryset
29 |
30 |
31 | class ArticleDetailsAPIView(generics.RetrieveAPIView):
32 | serializer_class = ArticleSerializer
33 | queryset = Post.objects.filter(publish_status="LIVE").all()
34 | lookup_field = 'slug'
35 |
--------------------------------------------------------------------------------
/bloggy_api/views/category_api.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 |
3 | from bloggy.models import Category
4 | from bloggy_api.serializers import CategorySerializer
5 |
6 |
7 | class CategoryAPIView(generics.ListCreateAPIView):
8 | queryset = Category.objects.filter(article_count__gt=0).order_by("-article_count").all()
9 | serializer_class = CategorySerializer
10 |
--------------------------------------------------------------------------------
/bloggy_api/views/course_api.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 |
3 | from bloggy.models.course import Course
4 | from bloggy_api.serializers import CourseSerializer
5 |
6 |
7 | class CoursesAPIView(generics.ListAPIView):
8 | queryset = Course.objects.filter(publish_status="LIVE") \
9 | .order_by("-published_date")
10 | serializer_class = CourseSerializer
11 |
--------------------------------------------------------------------------------
/bloggy_api/views/quizzes_api.py:
--------------------------------------------------------------------------------
1 | from django.http import Http404
2 | from rest_framework import generics
3 | from rest_framework.response import Response
4 | from rest_framework.views import APIView
5 |
6 | from bloggy.models import Quiz
7 | from bloggy.services.post_service import get_recent_quizzes
8 | from bloggy_api.serializers import QuizSerializer
9 |
10 |
11 | class QuizzesAPIView(generics.ListCreateAPIView):
12 | serializer_class = QuizSerializer
13 |
14 | def get_queryset(self):
15 | return get_recent_quizzes()
16 |
17 |
18 | class QuizDetailsAPIView(APIView):
19 | def get_object(self, pk):
20 | try:
21 | return Quiz.objects.get(pk=pk)
22 | except Quiz.DoesNotExist:
23 | raise Http404
24 |
25 | def get(self, request, pk):
26 | quiz = self
27 | return Response(quiz.get_questions_json)
28 |
--------------------------------------------------------------------------------
/bloggy_api/views/user_api.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 |
3 | from bloggy.models import User
4 | from bloggy_api.serializers import UserSerializer
5 |
6 |
7 | class UsersAPIView(generics.ListCreateAPIView):
8 | serializer_class = UserSerializer
9 |
10 | def get_queryset(self):
11 | return User.objects.filter(username=self.kwargs['username'])
12 |
--------------------------------------------------------------------------------
/bloggy_api/views/vote_api.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.contrib import auth
4 | from django.http import HttpResponse
5 | from rest_framework.views import APIView
6 |
7 | from bloggy.models import Vote
8 | from bloggy_api.exception.unauthorized_access import UnauthorizedAccess
9 |
10 |
11 | class VoteAPIView(APIView):
12 | model = Vote
13 |
14 | def post(self, request):
15 |
16 | user = auth.get_user(request)
17 | if user.is_anonymous:
18 | raise UnauthorizedAccess()
19 |
20 | json_body = request.data
21 | post_id = json_body.get('post_id')
22 | post_type = json_body.get('post_type')
23 |
24 | vote, created = Vote.objects.get_or_create(
25 | user=user, post_id=post_id, post_type=post_type)
26 | if not created:
27 | vote.delete()
28 |
29 | return HttpResponse(json.dumps({
30 | "result": created,
31 | "userVoteCount": Vote.objects.filter(user=user, post_id=post_id, post_type=post_type).count(),
32 | "count": self.model.objects.filter(post_id=post_id, post_type=post_type).count()
33 | }), content_type="application/json")
34 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/caret-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/caret-filled-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/caret-filled-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/caret-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/default-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/default-banner.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/default_avatar.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/favicon/favicon.ico
--------------------------------------------------------------------------------
/bloggy_frontend/assets/full-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/full-logo-light.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/full-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/full-logo.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/github-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/github-bg.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/hero-img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/hero-img.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/home-bg-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/home-bg-image.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/icon-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/icon-blue.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/icon-dark.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/icon-gray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/icon-gray.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/index__hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/index__hero.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/like.svg:
--------------------------------------------------------------------------------
1 |
3 | Like comment:
4 |
5 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/liked.svg:
--------------------------------------------------------------------------------
1 |
4 | Like comment:
5 |
6 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/logo-dark.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/logo-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/logo-icon.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/logo.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/love-selected.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/love.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/playbutton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/playbutton.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/reply.svg:
--------------------------------------------------------------------------------
1 | Comment button
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/base-64.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/base-64.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/file-yaml-o.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/http-status.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/http-status.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/icons8-links-66.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/icons8-links-66.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/internet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/internet.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/ip-scanner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/ip-scanner.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/json.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/json.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/links.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/links.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/mime.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/mime.gif
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/url-encode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/url-encode.png
--------------------------------------------------------------------------------
/bloggy_frontend/assets/resources/xml.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/assets/resources/xml.gif
--------------------------------------------------------------------------------
/bloggy_frontend/js/app/cookie.js:
--------------------------------------------------------------------------------
1 | window.Cookies = require('../../vendor/js.cookie');
2 |
3 | (function (root, $, undefined) {
4 | "use strict";
5 | jQuery(function () {
6 | // DOM ready, take it away
7 | var cache = {
8 | $cookieCompliance: $('.header__cookies'),
9 | $cookieConfirmation: $('.button--cookie')
10 | };
11 |
12 | /**
13 | * Init function for icocookies module.
14 | * @override
15 | */
16 | function init() {
17 | if (!Cookies.get('__stacktips_web_cookies')) {
18 | showCookieCompliance();
19 | addEventListeners();
20 | } else {
21 | removeCookieCompliance();
22 | }
23 | }
24 |
25 | function showCookieCompliance() {
26 | cache.$cookieCompliance.slideDown();
27 | }
28 |
29 | function removeCookieCompliance() {
30 | cache.$cookieCompliance.slideUp(400, function () {
31 | cache.$cookieCompliance.remove();
32 | });
33 | }
34 |
35 | function addEventListeners() {
36 | cache.$cookieConfirmation.on('click', acceptCookies);
37 | }
38 |
39 | function acceptCookies(e) {
40 | e.preventDefault();
41 | Cookies.set("__stacktips_web_cookiess", "true", {
42 | expires: 365,
43 | path: "/"
44 | });
45 | removeCookieCompliance();
46 | }
47 |
48 | init();
49 |
50 | // Public API
51 | return {};
52 | });
53 |
54 | }(this, jQuery));
55 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/app/heading.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 | let answerContent = document.getElementById("article-content")
3 | if (answerContent) {
4 | let headings = answerContent.querySelectorAll('h1, h2, h3, h4');
5 |
6 | for (let i = 0; i < headings.length; i++) {
7 | let headingText = headings[i].textContent;
8 | const slugifyHeader = headingText
9 | .toLowerCase()
10 | .trim()
11 | .replace(/[^\w\s-]/g, '')
12 | .replace(/[\s_-]+/g, '-')
13 | .replace(/^-+|-+$/g, '');
14 |
15 | var a = document.createElement("a");
16 | a.href = '#' + slugifyHeader
17 | a.className = "hash-anchor"
18 | a.type = "button"
19 | a.setAttribute("aria-hidden", true)
20 |
21 | headings[i].id = slugifyHeader;
22 | insertAfter(headings[i].firstChild, a);
23 | }
24 | }
25 |
26 | });
27 |
28 | function insertAfter(referenceNode, newNode) {
29 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
30 | }
--------------------------------------------------------------------------------
/bloggy_frontend/js/app/newsletter.js:
--------------------------------------------------------------------------------
1 | function validateEmail(email) {
2 | const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
3 | return emailPattern.test(email);
4 | }
5 |
6 |
7 | document.addEventListener('DOMContentLoaded', function () {
8 | const form = document.getElementById('subscription-form');
9 | if (null == form) return;
10 |
11 | form.addEventListener('submit', function (event) {
12 | event.preventDefault();
13 | const nameInput = document.getElementById('name');
14 | const emailInput = document.getElementById('email');
15 | // const name = nameInput.value;
16 | const email = emailInput.value;
17 |
18 | if (!validateEmail(email)) {
19 | emailInput.classList.add('is-invalid');
20 | return;
21 | } else {
22 | emailInput.classList.remove('is-invalid');
23 | }
24 |
25 | const subscriptionData = {
26 | is_active: true, name: document.getElementById('name').value, email: document.getElementById('email').value
27 | };
28 |
29 | fetch('/api/1.0/newsletter/subscribe', {
30 | method: 'POST', headers: {
31 | 'Content-Type': 'application/json',
32 | 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
33 | }, body: JSON.stringify(subscriptionData)
34 | })
35 | .then(response => response.json())
36 | .then(data => {
37 | showToast('Subscription successful! Thank you for subscribing.', "success")
38 | form.reset();
39 | })
40 | .catch(error => {
41 | showToast('Subscription failed. Please try again later.', "error")
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/app/toast.js:
--------------------------------------------------------------------------------
1 | window.showToast = function(message, type) {
2 | const toastContainer = document.getElementById('toastContainer');
3 |
4 | // Create the toast element
5 | const toast = document.createElement('div');
6 | toast.className = 'toast';
7 | toast.textContent = message;
8 |
9 | // Append the toast to the container
10 | toastContainer.appendChild(toast);
11 |
12 | // Show the toast with animation
13 | setTimeout(() => {
14 | toast.classList.add('show-toast');
15 | toast.classList.add(type);
16 | }, 100);
17 |
18 | // Remove the toast after a certain duration
19 | setTimeout(() => {
20 | toast.classList.remove('show-toast');
21 | setTimeout(() => {
22 | toastContainer.removeChild(toast);
23 | }, 300);
24 | }, 3000);
25 | }
--------------------------------------------------------------------------------
/bloggy_frontend/js/components.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Vue is a modern JavaScript library for building interactive web interfaces
3 | * using reactive data binding and reusable components. Vue's API is clean
4 | * and simple, leaving you to focus on building your next great project.
5 | */
6 |
7 | import Vue from 'vue/dist/vue.js';
8 |
9 | Vue.component("comments", () => import("./vue/disqus/Comments.vue"));
10 | Vue.component('contact-form', () => import('./vue/disqus/ContactForm.vue'));
11 | Vue.component('cookie-consent', () => import('./vue/CookieConsent.vue'));
12 | Vue.component('quizlet', () => import('./vue/quiz/Quizlet.vue'));
13 |
14 | window.addEventListener('load', function () {
15 | window.axios.defaults.headers.common['X-CSRFToken'] = document.querySelector('[name=csrfmiddlewaretoken]').value;
16 | const app = new Vue({
17 | el: '#vueRoot',
18 | });
19 | }, false);
20 |
21 |
22 | Vue.config.devtools = false;
23 | Vue.config.productionTip = false
24 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/html-table-of-contents.js:
--------------------------------------------------------------------------------
1 | /*jslint
2 | white: true,
3 | browser: true,
4 | vars: true
5 | https://github.com/matthewkastor/html-table-of-contents/blob/master/src/html-table-of-contents.js
6 | * Generates a table of contents for your document based on the headings
7 | * present. Anchors are injected into the document and the
8 | * entries in the table of contents are linked to them. The table of
9 | * contents will be generated inside the first element with the id `toc`.
10 | * @param {HTMLDOMDocument} documentRef Optional A reference to the document
11 | * object. Defaults to `document`.
12 | * @author Matthew Christopher Kastor-Inare III
13 | * @version 20130726
14 | * @example
15 | * // call this after the page has loaded
16 | * htmlTableOfContents();
17 | */
18 | function htmlTableOfContents() {
19 | const toc = document.getElementById('toc');
20 | const headings = [].slice.call(document.getElementById('article-content')
21 | .querySelectorAll('h1, h2, h3, h4')
22 | );
23 | headings.forEach(function (heading, index) {
24 | const anchor = document.createElement('a');
25 | anchor.setAttribute('name', 'toc' + index);
26 | anchor.setAttribute('id', 'toc' + index);
27 |
28 | const link = document.createElement('a');
29 | link.setAttribute('href', '#toc' + index);
30 | link.textContent = heading.textContent;
31 |
32 | const div = document.createElement('div');
33 | div.setAttribute('class', heading.tagName.toLowerCase());
34 |
35 | div.appendChild(link);
36 | toc.appendChild(div);
37 | heading.parentNode.insertBefore(anchor, heading);
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/index.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap';
2 | // const jQuery = require("jquery")
3 | // require("bootstrap")
4 | // require("@popperjs/core")
5 |
6 |
7 | global.$ = global.jQuery = require('jquery');
8 | import "../sass/style.scss";
9 |
10 | window.axios = require('axios');
11 | // require('./app/cookie');
12 | require('./app/toast');
13 | require('./app/heading');
14 | require('./app/toc');
15 | require('./app/bookmark');
16 | require('./app/copyCode');
17 | require('./app/newsletter');
18 |
19 | function animateValue(obj, start, end, duration) {
20 | let startTimestamp = null;
21 | const step = (timestamp) => {
22 | if (!startTimestamp) startTimestamp = timestamp;
23 | const progress = Math.min((timestamp - startTimestamp) / duration, 1);
24 | obj.innerHTML = Math.floor(progress * (end - start) + start);
25 | if (progress < 1) {
26 | window.requestAnimationFrame(step);
27 | }
28 | };
29 | window.requestAnimationFrame(step);
30 | }
31 |
32 | $(document).ready(function () {
33 | console.log("Jquery loaded")
34 | // Animated Counter for home page
35 | const counters = document.getElementsByClassName("value-counter");
36 | for (const element of counters) {
37 | let duration = element.getAttribute("duration")
38 | if (duration === 'underfined') {
39 | duration = 1000;
40 | }
41 | animateValue(element, 1, element.getAttribute("max-value"), duration);
42 | }
43 |
44 | $(window).scroll(function () {
45 | var y = $(window).scrollTop();
46 | if (y > 0) {
47 | $("#main-navbar").addClass('--not-top');
48 | } else {
49 | $("#main-navbar").removeClass('--not-top');
50 | }
51 | });
52 |
53 | })
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/Api.js:
--------------------------------------------------------------------------------
1 | export default {
2 | comments: 'api/1.0/comments',
3 | users: 'api/1.0/users',
4 | vote: 'api/1.0/vote',
5 | newsletter: 'api/1.0/newsletter/subscribe',
6 | contact: 'api/1.0/contact',
7 | }
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/disqus/CommentForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/quiz/QuestionState.js:
--------------------------------------------------------------------------------
1 | const QuestionState = Object.freeze({
2 | ANSWERED: "answered",
3 | UNANSWERED: "unanswered"
4 | });
5 | export default QuestionState;
6 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/quiz/QuestionType.js:
--------------------------------------------------------------------------------
1 | const QuestionType = Object.freeze({
2 | BINARY: "binary",
3 | MULTIPLE: "multiple"
4 | });
5 | export default QuestionType
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/quiz/QuizLandingComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ quiz.title }}
5 |
Interactive Quiz {{ quiz.questions_count }}
6 | Questions {{ quiz.duration }} Minutes
7 |
8 |
Start Quiz
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bloggy_frontend/js/vue/quiz/QuizState.js:
--------------------------------------------------------------------------------
1 | const QuizState = Object.freeze({
2 | UNDEFINED: "undefined",
3 | COMPLETED: "completed",
4 | IN_PROGRESS: "in_progress"
5 | });
6 | export default QuizState
--------------------------------------------------------------------------------
/bloggy_frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bloggify",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "js/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack --mode=development",
9 | "build": "webpack --mode=production"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "axios": "^0.27.2",
15 | "copy-webpack-plugin": "^6.4.1",
16 | "css-loader": "^5.2.6",
17 | "mini-css-extract-plugin": "^1.6.0",
18 | "vue-loader": "^15.9.7",
19 | "webpack": "^5.0.0",
20 | "webpack-cli": "^5.0.1",
21 | "sass-loader": "^10.1.1",
22 | "style-loader": "^2.0.0",
23 | "sass": "^1.34.0"
24 | },
25 | "dependencies": {
26 | "@popperjs/core": "^2.11.6",
27 | "bootstrap": "^5.3.0-alpha1",
28 | "jquery": "^3.6.0",
29 | "moment": "^2.29.4",
30 | "vue": "^2.7.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/bloggy_frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'cssnano': {}
4 | }
5 | };
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_category_widget.scss:
--------------------------------------------------------------------------------
1 | .category__items {
2 | text-align: center;
3 |
4 | li {
5 | box-sizing: border-box;
6 | //border: 1px solid $border-color;
7 | border: 2px solid var(--tags-list-a-border-color);
8 | border-radius: 58px;
9 | display: inline-flex;
10 | align-items: center;
11 | margin: 0 20px 20px 0;
12 | -webkit-transition: all 0.3s ease-in-out;
13 | -o-transition: all 0.3s ease-in-out;
14 | transition: all 0.3s ease-in-out;
15 |
16 | &:hover {
17 | box-shadow: 0 3px 7px rgba(0, 0, 0, .07);
18 |
19 | }
20 |
21 | a {
22 | letter-spacing: 0.01em;
23 | display: flex;
24 | align-items: center;
25 | padding: 8px 12px 8px 20px;
26 | transition: all 0.25s ease-out;
27 | }
28 |
29 | img {
30 | margin-left: 10px;
31 | object-fit: contain;
32 | border-radius: 50%;
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_code.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | font-size: 0.95rem;
3 | color: rgb(47, 51, 55);
4 | background: $body-bg;
5 | margin: 1rem 0;
6 | padding: 1rem;
7 | line-height: 1.3rem;
8 | font-family: $font-family-monospace;
9 | position: relative;
10 |
11 | & > code {
12 | padding: 0 !important;
13 | }
14 | }
15 |
16 | .codeBox {
17 | position: relative;
18 | }
19 |
20 | pre[class*="lang-"],
21 | pre[class*="language-"] {
22 | padding: 1rem;
23 | }
24 |
25 | .btn__copyCode {
26 | background: hsl(207.7, 12.9%, 19.8%);
27 | font-size: 0.75rem;
28 | position: absolute;
29 | padding: 2px 2px;
30 | top: 1px;
31 | right: 1px;
32 | min-width: 55px;
33 | color: #e3e3e3;
34 | border-bottom-left-radius: 6px;
35 | border: none;
36 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_cookie-consent.scss:
--------------------------------------------------------------------------------
1 | .cookie-consent-banner {
2 | position: fixed;
3 | @extend .shadow-sm;
4 | bottom: 0;
5 | right: 0;
6 | z-index: 2147483645;
7 | box-sizing: border-box;
8 | width: 500px;
9 | max-width: 100%;
10 | background-color: #EDF0F4;
11 | padding: 1rem;
12 | }
13 |
14 | .cookie-consent-banner__inner {
15 | @extend .container;
16 | margin: 0 auto;
17 | padding: 1rem;
18 | border: 2px dashed $secondary;
19 | }
20 |
21 | .cookie-consent-banner__description {
22 | font-size: 0.9rem;
23 | }
24 |
25 |
26 | .bounce-enter-active {
27 | animation: bounce-in 0.5s;
28 | }
29 |
30 | .bounce-leave-active {
31 | animation: bounce-in 0.5s reverse;
32 | }
33 |
34 | @keyframes bounce-in {
35 | 0% {
36 | transform: scale(0);
37 | }
38 | 50% {
39 | transform: scale(1.05);
40 | }
41 | 100% {
42 | transform: scale(1);
43 | }
44 | }
45 |
46 |
47 | .cookies-banner__close-button {
48 | position: absolute;
49 | top: 8px;
50 | left: 8px;
51 | z-index: 1;
52 | margin: 0;
53 | cursor: pointer;
54 | font: 12px/16px Menlo, Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
55 | line-height: 16px;
56 | font-weight: 500;
57 | border: 0;
58 | transition: all 0.3s;
59 | background: #edf0f4;
60 | text-align: center;
61 | padding: 6px;
62 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_courses.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/bloggy_frontend/sass/content/_courses.scss
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_hero.scss:
--------------------------------------------------------------------------------
1 |
2 | #about-page-hero {
3 | width: 100%;
4 | background: #fff !important;
5 |
6 | h1 {
7 | font-size: 3rem;
8 | }
9 | }
10 |
11 | #about-page-hero .hero-img img {
12 | //width: 72%;
13 | text-align: right;
14 | float: right;
15 | }
16 |
17 |
18 | .cta-section {
19 | background: #e6f4fbb3;
20 |
21 | h3 {
22 | font-size: 40px;
23 | font-weight: 300;
24 | }
25 | }
26 |
27 |
28 | .icon-box {
29 | border-radius: $card-border-radius;
30 | background: #FFF;
31 | margin: 0.6rem 0;
32 | padding: 0.75rem;
33 | transition: all 0.2s ease-in-out !important;
34 | box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
35 | border: 1px solid $secondary;
36 |
37 | &:hover {
38 | box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
39 | transform: translateY(-5px);
40 | }
41 |
42 | .title {
43 | color: $bg-dark;
44 | font-size: 25px;
45 | font-weight: 300;
46 | margin: 0;
47 | }
48 |
49 | p {
50 | color: $bg-dark;
51 | margin-bottom: 0;
52 | -webkit-line-clamp: 2;
53 | line-clamp: 2;
54 | }
55 | }
56 |
57 |
58 | .icon-box2 {
59 | text-align: center;
60 | align-items: center;
61 | font-size: 1.5rem;
62 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_home.scss:
--------------------------------------------------------------------------------
1 | #homeIntroSection {
2 | background-repeat: repeat;
3 |
4 | .text-gradient {
5 | background: -webkit-linear-gradient(30deg, #769BF2, #15CA82);
6 | background-clip: border-box;
7 | -webkit-background-clip: text;
8 | -webkit-text-fill-color: transparent;
9 | }
10 | }
11 |
12 | @keyframes scale {
13 | 100% {
14 | transform: scale(1);
15 | }
16 | }
17 |
18 |
19 | .hover-effect {
20 | &:hover {
21 | background: #ebfcff !important;
22 | -webkit-transition: background-color 300ms linear;
23 | -ms-transition: background-color 300ms linear;
24 | transition: background-color 300ms linear, transform .2s;
25 | }
26 | }
27 |
28 | .scrolling-wrapper {
29 | overflow-x: auto;
30 | -ms-overflow-style: none;
31 | scrollbar-width: none;
32 |
33 | &::-webkit-scrollbar {
34 | display: none;
35 | }
36 | }
37 |
38 |
39 | .topics-tag {
40 | padding: 1.1rem 1rem;
41 | background: #f0f9ff;
42 | border-radius: $card-border-radius;
43 | border: 0.09rem solid #b5c4cc;
44 | transition: all 0.2s ease-in-out !important;
45 | position: relative;
46 |
47 | &:hover {
48 | transform: translateY(-5px);
49 | }
50 |
51 | .tag__chevron {
52 | height: 24px;
53 | width: 24px;
54 | color: $body-color
55 | }
56 |
57 | .tag__image {
58 | -o-object-fit: contain;
59 | object-fit: contain;
60 | width: 32px;
61 | height: 32px;
62 | }
63 |
64 | }
65 |
66 |
67 | @media screen and (min-width: 992px) {
68 | .latest-feed-container {
69 | padding: 0 1rem;
70 | margin: 0 0 0 1rem;
71 |
72 | //.col:first-child {
73 | // margin-top: 0;
74 | //}
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_listGroup.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 |
4 | .list-group-item {
5 | padding: 0.5rem 0;
6 | background: none;
7 | }
8 |
9 | .list-group-item.active a {
10 | color: $primary !important;
11 | font-weight: 600;
12 | }
13 |
14 | .list-group-item:last-child,
15 | .list-group-item:first-child {
16 | border-radius: 0;
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_login.scss:
--------------------------------------------------------------------------------
1 | #login-wrapper {
2 | height: calc(100vh - 75px);
3 | //background: $body-bg;
4 | }
5 |
6 |
7 | .login-wrapper-inner,
8 | .register-wrapper-inner {
9 | position: relative;
10 | margin: 0 auto;
11 | max-width: 520px;
12 | border-radius: $card-border-radius;
13 | background: var(--bs-card-bg);
14 | padding: 2.5rem;
15 | //border-radius: var(--radius-auto) !important;
16 | //box-shadow: rgba(23, 23, 23, 0.05) 0 0 0 1px;
17 | //box-shadow: 5px 15px 15px rgba(89, 152, 152, 0.05);
18 | transition: all 0.2s ease-in-out !important;
19 | box-sizing: border-box
20 | }
21 |
22 | .login-with-github,
23 | .login-with-twitter {
24 | padding: 0.65rem 0.25rem;
25 | -webkit-transition: background-color 300ms linear;
26 | -ms-transition: background-color 300ms linear;
27 | transition: background-color 300ms linear, transform .2s;
28 |
29 | svg {
30 | height: 18px;
31 | width: auto;
32 | margin-right: 3px;
33 | }
34 |
35 | &:hover {
36 | background: $blue-bg-01;
37 | border: 1px solid $secondary;
38 | }
39 | }
40 |
41 | .login-btn {
42 | padding: 0.65rem 0.25rem;
43 | }
44 |
45 |
46 | $--color-twitter: rgb(29, 161, 242);
47 | $--color-github: #171515;
48 |
49 | .login-with-twitter {
50 | border-color: $--color-twitter !important;
51 | color: $--color-twitter !important;
52 |
53 | svg {
54 | fill: $--color-twitter;
55 | }
56 |
57 |
58 | }
59 |
60 | .login-with-github {
61 | border-color: $--color-github;
62 | color: $--color-github;
63 |
64 | svg {
65 | fill: $--color-github;
66 | }
67 |
68 | }
69 |
70 | .label.form-text {
71 | font-weight: 400;
72 | }
73 |
74 | .help-block {
75 | margin: 0 0 3px 0;
76 | font-size: 0.75rem;
77 | color: $gray-600;
78 | }
79 |
80 | .errorlist {
81 | @extend .list-unstyled;
82 | margin: 4px 0;
83 |
84 | li {
85 | font-size: 0.8rem;
86 | color: $danger;
87 | }
88 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_newsletter.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .newsletter-subscribe {
4 | align-items: center !important;
5 | }
6 |
7 | .newsletter-subscribe .intro {
8 | font-size: 16px;
9 | max-width: 750px;
10 | margin: 0 0 1rem
11 | }
12 |
13 | .newsletter-subscribe .intro p {
14 | margin-bottom: 1rem;
15 | }
16 |
17 | .newsletter-subscribe form {
18 | justify-content: left
19 | }
20 |
21 | .tools-form-container .form-control,
22 | .newsletter-subscribe form .form-control {
23 | padding: 0.6rem 0.75rem;
24 | border: 1px solid $secondary;
25 | outline: none;
26 | background: #fff;
27 | }
28 |
29 | .newsletter-subscribe form .btn:active {
30 | transform: translateY(1px)
31 | }
32 |
33 | .newsletter-subscribe form .btn-primary {
34 | outline: none !important;
35 | box-shadow: none !important;
36 | }
37 |
38 | .newsletter {
39 | color: $text-color-black !important
40 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_pageContent.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | #sidebar, #sidebar-right {
4 | padding: 1.5rem 0.5rem;
5 | }
6 |
7 | #main-content {
8 | min-height: 100vh;
9 | }
10 |
11 | .padding-medium {
12 | padding: 1.25rem;
13 | }
14 |
15 |
16 | @media screen and (max-width: 960px) {
17 | #main-content {
18 | padding-left: 0;
19 | padding-right: 0;
20 | }
21 |
22 | }
23 |
24 | @media screen and (min-width: 992px) {
25 | .padding-medium {
26 | padding: 0 1.5rem;
27 | }
28 | }
29 |
30 | ul.static-pages-menu {
31 | li a {
32 | transition: all 0.2s ease-in-out !important;
33 | margin: 1rem 0.5rem 1rem 0;
34 | font-weight: 500;
35 | text-decoration: none;
36 | display: block;
37 | color: $gray-600;
38 | font-size: 1rem;
39 |
40 | &.active,
41 | &:hover {
42 | color: #1976D2;
43 | }
44 | }
45 |
46 | }
47 |
48 | .static-page {
49 | overflow: hidden;
50 |
51 | ol, ul, dl {
52 | margin: 0 0 1rem 1.5rem;
53 | }
54 | }
55 |
56 | .static-page-container {
57 | .toc-list {
58 |
59 | border-radius: 8px;
60 | margin-bottom: 1rem;
61 | padding: 0 1rem;
62 | display: contents;
63 | min-width: 400px;
64 | background: var(--bs-body-bg);
65 |
66 | a {
67 | text-decoration: none;
68 | font-weight: 400;
69 | padding: 0.2rem 0;
70 | font-size: 0.9rem;
71 | margin-left: 0;
72 | }
73 |
74 | a:last-child {
75 | margin-bottom: 0;
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_pagination.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .page-link {
4 | border-radius: 0.35rem !important;
5 | display: inline-block;
6 | text-align: center;
7 | color: white;
8 | text-decoration: none;
9 | background: $primary;
10 | padding: 0.55rem 2rem;
11 | margin: 0 0.75rem;
12 | border: none;
13 |
14 | &:hover, &:focus {
15 | background: $secondary;
16 | color: white;
17 | box-shadow: none;
18 |
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_print.scss:
--------------------------------------------------------------------------------
1 | @media print {
2 | pre {
3 | //border: 1px solid #222222 !important;
4 | background: hsl(219, 23%, 97%) !important;
5 | -webkit-print-color-adjust: exact;
6 | box-shadow: inset 0 0 0 1000px hsl(219, 23%, 97%);
7 | }
8 |
9 | .cookie-consent-banner,
10 | .btn__copyCode {
11 | display: none;
12 | }
13 |
14 | .container-wrap.px-5 {
15 | padding: 0 !important;
16 | }
17 |
18 | .lessons-main-content {
19 | width: 100% !important;
20 | }
21 |
22 |
23 | .col-print-1 {width:8%; float:left;}
24 | .col-print-2 {width:16%; float:left;}
25 | .col-print-3 {width:25%; float:left;}
26 | .col-print-4 {width:33%; float:left;}
27 | .col-print-5 {width:42%; float:left;}
28 | .col-print-6 {width:50%; float:left;}
29 | .col-print-7 {width:58%; float:left;}
30 | .col-print-8 {width:66%; float:left;}
31 | .col-print-9 {width:75%; float:left;}
32 | .col-print-10{width:83%; float:left;}
33 | .col-print-11{width:92%; float:left;}
34 | .col-print-12{width:100%; float:left;}
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_questions.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_quiz.scss:
--------------------------------------------------------------------------------
1 | .home-quiz-card {
2 | position: relative;
3 | padding: 1rem 1rem;
4 | margin-bottom: 1rem;
5 | background: #e1ecfd no-repeat right bottom;
6 | background-size: contain;
7 | height: 100%;
8 | max-height: 140px;
9 | box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
10 | //border: 1px solid #8fcaff;
11 |
12 | &:hover {
13 | transform: translateY(-5px);
14 | background-color: #e3eeff;
15 | border-color: $primary;
16 | -webkit-transition: background-color 300ms linear;
17 | -ms-transition: background-color 300ms linear;
18 | transition: background-color, border-color 300ms linear, transform 0.2s;
19 | }
20 |
21 |
22 | .highlight-container {
23 | max-width: 80%;
24 | min-height: 55px;
25 | display: block;
26 | }
27 |
28 | .quiz-title {
29 | color: #174755;
30 | background: #e1ecfe;
31 | opacity: 0.98;
32 | font-size: $font-size-base;
33 | }
34 |
35 | .quiz-meta {
36 | color: #f57359;
37 | font-weight: 400;
38 | }
39 |
40 |
41 | }
42 |
43 | .quiz-link {
44 | @extend .stretched-link;
45 | color: var(--quiz-card-link-color);
46 | -webkit-text-decoration-skip-ink: none;
47 | text-decoration-skip-ink: none;
48 | text-decoration-color: rgba(0, 0, 0, 0);
49 |
50 | &:focus,
51 | &:hover {
52 | text-decoration: underline;
53 | text-decoration-thickness: 2px;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_search.scss:
--------------------------------------------------------------------------------
1 | span.highlight {
2 | color: $primary;
3 | }
4 |
5 | .search-result-row {
6 | .result-heading {
7 | text-decoration: none;
8 |
9 | &:hover {
10 | text-decoration: underline;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_table.scss:
--------------------------------------------------------------------------------
1 | table {
2 | margin: 1.5rem 0;
3 | width: 100%;
4 |
5 | thead,
6 | tbody,
7 | tfoot,
8 | tr,
9 | td,
10 | th {
11 | border: 1px solid var(--table-border-color) !important;
12 | font-weight: initial;
13 | }
14 |
15 | th,
16 | td {
17 | padding: 4px 8px !important;
18 | }
19 |
20 | th {
21 | background: var(--table-column-bg);
22 | font-weight: 500 !important;
23 | }
24 |
25 | }
26 |
27 | .table-borderless th,
28 | .table-borderless tr,
29 | .table-borderless td,
30 | .table-borderless tbody,
31 | .table-borderless thead,
32 | .table-borderless thead th,
33 | .table-borderless tbody + tbody {
34 | border: 0 !important;
35 | background: transparent;
36 | }
37 |
38 |
39 | .table-sm th,
40 | .table-sm td {
41 | padding: 0.3rem !important;
42 | }
43 |
44 | .table-striped tbody tr:nth-of-type(odd) {
45 | background-color: $body-bg;
46 | }
47 |
48 | .table-striped tbody tr:nth-of-type(even) {
49 | background-color: #FFFFFF;
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_toc.scss:
--------------------------------------------------------------------------------
1 | .toc-widget {
2 | margin-bottom: 1.5rem;
3 |
4 | .collapsible {
5 | background-color: var(--toc-header-bg);
6 | cursor: pointer;
7 | padding: 0.75rem 1rem;
8 | width: 100%;
9 | border: none;
10 | text-align: left;
11 | outline: none;
12 | font-weight: 500;
13 | color: var(--heading-link-color);
14 |
15 | &:after {
16 | content: '\02795';
17 | font-size: 13px;
18 | color: var(--heading-link-color);
19 | float: right;
20 | margin-left: 5px;
21 | line-height: 1.3rem;
22 | }
23 |
24 | &:hover {
25 | background-color: var(--toc-header-bg);
26 | }
27 | }
28 |
29 | .active {
30 | background-color: var(--toc-header-bg);
31 | border-bottom-right-radius: 0;
32 | border-bottom-left-radius: 0;
33 |
34 | &:after {
35 | content: "\2796";
36 | }
37 |
38 | }
39 |
40 | .toc-list {
41 | padding: 10px 0;
42 | }
43 |
44 | .content {
45 | padding: 0 1rem;
46 | background-color: var(--toc-content-bg);
47 | max-height: 0;
48 | overflow: hidden;
49 | transition: max-height 0.2s ease-out;
50 | border-bottom-left-radius: $card-border-radius !important;
51 | border-bottom-right-radius: $card-border-radius;
52 | }
53 |
54 | }
55 |
56 | .toc-content-link {
57 | &.h1,
58 | &.h2,
59 | &.h3,
60 | &.h4,
61 | &.h5 {
62 | font-size: 0.85rem;
63 | font-weight: normal;
64 | text-decoration: none;
65 | margin-top: 0.75rem;
66 | }
67 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_update-profile.scss:
--------------------------------------------------------------------------------
1 | .update-profile-form {
2 |
3 | .form-control {
4 | padding: 0.6rem 0.75rem;
5 | border: 1px solid var(--form-control-border-color);
6 | background: var(--form-control-bg);
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_voting.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .pulse-button {
4 | align-items: center;
5 | justify-content: center;
6 |
7 | padding: 6px 12px;
8 | width: fit-content;
9 | background: $body-bg;
10 | margin: 0 0 0 8px;
11 | border: none;
12 | border-radius: 6px;
13 | text-align: center;
14 | outline: none;
15 | cursor: pointer;
16 | min-width: 48px;
17 |
18 | &:hover {
19 | background: #eceef2;
20 | }
21 |
22 | &:active {
23 | background: rgb(233, 236, 239);
24 | }
25 |
26 | .vote-count {
27 | color: $gray-900;
28 | font-size: $font-size-sm;
29 | }
30 |
31 | }
32 |
33 |
34 | .pulse-button.horizontal-layout {
35 | display: flex;
36 |
37 | .vote-count {
38 | margin-left: 6px;
39 | }
40 | }
41 |
42 |
43 | .reaction__icon--active {
44 | fill: #fc4300;
45 | }
46 |
47 | .reaction__icon--inactive {
48 | fill: $gray-800;
49 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/_widget.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .sidebar-widget {
4 |
5 | }
6 |
7 | .share-appeal-widget {
8 | border: 1px solid $secondary;
9 | background: var(--feed-card-bg);
10 | box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
11 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/content/model.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | left: 0;
4 | top: 0;
5 | width: 100%;
6 | height: 100%;
7 | background-color: rgba(0, 0, 0, 0.5);
8 | opacity: 0;
9 | visibility: hidden;
10 | transform: scale(1.1);
11 | transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
12 | }
13 |
14 |
15 |
16 | .modal-content {
17 | position: absolute;
18 | top: 50%;
19 | left: 50%;
20 | transform: translate(-50%, -50%);
21 | background-color: white;
22 | padding: 1rem 1.5rem;
23 | width: 24rem;
24 | border-radius: 0.5rem;
25 | }
26 |
27 | .close-button {
28 | float: right;
29 | width: 1.5rem;
30 | line-height: 1.5rem;
31 | text-align: center;
32 | cursor: pointer;
33 | border-radius: 0.25rem;
34 | background-color: lightgray;
35 | }
36 |
37 | .close-button:hover {
38 | background-color: darkgray;
39 | }
40 |
41 | .show-modal {
42 | opacity: 1;
43 | visibility: visible;
44 | transform: scale(1);
45 | transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
46 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/style.scss:
--------------------------------------------------------------------------------
1 | @import "customVariables";
2 | @import "content/darkMode";
3 | @import "bootstrap";
4 | @import "vendor/navbar";
5 | @import "vendor/header";
6 | @import "vendor/card";
7 | @import "vendor/alert";
8 | @import "content/pageContent";
9 | @import "content/listGroup";
10 | @import "content/print";
11 | @import "content/home";
12 | @import "content/pagination";
13 | @import "vendor/button";
14 | @import "content/widget";
15 | //@import "content/newsletter";
16 | @import "utils";
17 | @import "content/post";
18 | @import "content/quiz";
19 | @import "content/toc";
20 |
21 | [data-bs-theme=light] {
22 | @import "content/highlightjs";
23 | }
24 |
25 | [data-bs-theme=dark] {
26 | @import "content/highlightjsDark";
27 | }
28 |
29 | @import "content/code";
30 | @import "content/comments";
31 | @import "content/search";
32 | //@import "content/cookie-consent";
33 | @import "content/hero";
34 | @import "content/social";
35 | @import "content/login";
36 | @import "content/update-profile";
37 | @import "content/table";
38 | @import "vendor/footer";
39 | @import "vendor/author";
40 | @import "vendor/bookmark_button";
41 | @import "vendor/toast";
42 | @import "vendor/dark_mode_toggle";
43 | @import "vendor/timeline";
44 | @import "content/quizlet";
45 | @import "content/category_widget";
46 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_alert.scss:
--------------------------------------------------------------------------------
1 | .alert {
2 | padding: 0.55rem 0.75rem;
3 | border-radius: 6px;
4 | border: 0;
5 | font-size: $font-size-sm;
6 | }
7 |
8 | .alert-info {
9 | --bs-alert-color: #006699;
10 | --bs-alert-bg: #cceeff;
11 | }
12 |
13 |
14 | .alert-success {
15 | --bs-alert-color: #032c15;
16 | --bs-alert-bg: #AEEEC791;
17 | }
18 |
19 | .alert-warning {
20 | --bs-alert-color: #2f1710;
21 | --bs-alert-bg: #ffb626;
22 | }
23 |
24 | .alert-error,
25 | .alert-danger {
26 | --bs-alert-color: #fff;
27 | --bs-alert-bg: #ff4040;
28 | }
29 |
30 |
31 | .alert-light {
32 | border-radius: 0.5rem;
33 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);
34 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
35 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
36 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_author.scss:
--------------------------------------------------------------------------------
1 | .author-image {
2 | -o-object-fit: cover;
3 | object-fit: cover;
4 | display: block;
5 | margin: 0 auto 10px;
6 | border: 5px solid transparent;
7 | box-sizing: border-box;
8 | box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
9 | transition: all 0.2s ease-in-out !important;
10 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_bookmark_button.scss:
--------------------------------------------------------------------------------
1 | #bookmarkButton {
2 | background-color: var(--toc-header-bg);
3 | border: none;
4 | padding: 6px 12px;
5 | cursor: pointer;
6 | border-radius: 5px;
7 | transition: background-color 0.3s ease;
8 | text-transform: capitalize;
9 | text-align: left;
10 |
11 |
12 | .bookmarked {
13 | background-color: $body-bg;
14 |
15 | i {
16 | color: $secondary;
17 | }
18 | }
19 |
20 | i {
21 | margin-right: 5px;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_button.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .btn {
4 | transition: all .3s ease-in-out;
5 | border-radius: $btn-border-radius;
6 |
7 | &:hover {
8 | box-shadow: none;
9 | transform: translateY(-4px);
10 | }
11 | }
12 |
13 | .btn-primary,
14 | .btn-outline-warning,
15 | .btn-warning,
16 | .btn-outline-primary,
17 | .btn-outline-secondary {
18 |
19 | &:hover,
20 | &:active {
21 | color: $white;
22 |
23 | svg {
24 | fill: $white;
25 | }
26 | }
27 | }
28 |
29 | .btn-info {
30 | color: $gray-800;
31 | }
32 |
33 | .btn-primary,
34 | .btn-warning {
35 | color: $white;
36 | }
37 |
38 |
39 | .btn-sm {
40 | padding: 6px 10px;
41 | font-size: $font-size-sm;
42 | border-radius: $btn-border-radius;
43 | box-shadow: none;
44 | transition: all .3s ease-in-out;
45 |
46 | &:hover {
47 | box-shadow: none;
48 | transform: translateY(0);
49 | }
50 | }
51 |
52 | .btn-xs {
53 | padding: 3px 8px !important;
54 | border-radius: 5px !important;
55 | font-size: 0.7rem !important;
56 | text-transform: uppercase !important;
57 | }
58 |
59 | .btn-secondary {
60 | &:hover,
61 | &:active,
62 | &:focus {
63 | color: white;
64 | }
65 | }
66 |
67 | .btn-lg {
68 | padding: 0.65rem 1.25rem;
69 | font-size: $font-size-base;
70 | border-radius: $btn-border-radius !important;
71 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_card.scss:
--------------------------------------------------------------------------------
1 | @import "../customVariables";
2 |
3 | .card {
4 | overflow: hidden;
5 | border: none;
6 | background: var(--feed-card-bg);
7 | border-radius: $card-border-radius;
8 | box-sizing: border-box;
9 | //box-shadow: 5px 15px 15px rgb(89 152 152 / 5%);
10 | //background: var(--default-card-bg);
11 | transition: all 0.2s ease-in-out !important;
12 | //box-shadow: 0 0 0 1px rgba(23,23,23,0.05);
13 | }
14 |
15 | .card-rounded {
16 | border-radius: $card-border-radius;
17 | }
18 |
19 | .card-text {
20 | color: var(--card-body-text-color);
21 | }
22 |
23 | .card-title {
24 | font-size: $h4-font-size;
25 | font-weight: 500;
26 | }
27 |
28 | .card-img-top {
29 | width: 100%;
30 | object-fit: cover;
31 | border-top-left-radius: 0;
32 | border-top-right-radius: 0;
33 | }
34 |
35 | .card-footer {
36 | //padding: 0 0 0 0;
37 | //margin: 0 1rem 0 1rem;
38 | //border-top: none;
39 | border-color: var(--table-border-color);
40 | background-color: inherit;
41 | }
42 |
43 | .card-bordered {
44 | border: 1px solid $border-color !important;
45 | }
46 |
47 | .card-active {
48 | border-color: $primary !important;
49 | background: #e8f2ff;
50 | color: $primary;
51 | }
52 |
53 |
54 | .feed-card-course {
55 | background: var(--course-card-bg) !important;
56 | border: 1px solid #444444 !important;
57 | }
58 |
59 | .card-light-blue {
60 | background: #eff6ff !important;
61 | border: 1px solid #93c5fd;
62 | }
63 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_dark_mode_toggle.scss:
--------------------------------------------------------------------------------
1 | .theme-toggler-container {
2 | margin: 0 1rem;
3 |
4 | input[type=checkbox] {
5 | display: none;
6 | }
7 |
8 | input[type=checkbox] + label {
9 | font-size: 20px;
10 |
11 | &:before {
12 | content: "\263E";
13 | display: inline-block;
14 | font-size: inherit;
15 | text-rendering: auto;
16 | color: #fff17f;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale;
19 | font-variation-settings: normal;
20 | moz-transition: all .4s ease-in-out;
21 | -o-transition: all .4s ease-in-out;
22 | -webkit-transition: all .4s ease-in-out;
23 | transition: all .4s ease-in-out;
24 | padding: 2px 4px;
25 | border-radius: 4px;
26 | width: 34px;
27 | text-align: center;
28 | background: var(--main-navbar-dark-color);
29 | }
30 | }
31 | }
32 |
33 | input[type=checkbox]:checked + label {
34 | &:before {
35 | content: "\2600";
36 | display: inline-block;
37 | font-size: inherit;
38 | text-rendering: auto;
39 | -webkit-font-smoothing: antialiased;
40 | -moz-osx-font-smoothing: grayscale;
41 | font-variation-settings: normal;
42 | //color: #1b1b1b;
43 | color: #ffffff;
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 |
3 | .footer-text {
4 | color: var(--card-body-text-color);
5 | text-decoration: none;
6 | @extend .font-sm;
7 | letter-spacing: -3;
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_header.scss:
--------------------------------------------------------------------------------
1 | .has-search {
2 | max-width: 400px;
3 |
4 | .form-control {
5 | padding: 5px 2rem 5px 2rem;
6 | border: 1px solid var(--main-navbar-dark-color);
7 | background: var(--main-navbar-dark-color);
8 | color: #f8f9fa !important;
9 | border-radius: 20px;
10 |
11 | &::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
12 | color: var(--searchbar-text-color);
13 | opacity: 1;
14 | }
15 |
16 | &:-ms-input-placeholder { /* Internet Explorer 10-11 */
17 | color: var(--searchbar-text-color);
18 | }
19 |
20 | &::-ms-input-placeholder { /* Microsoft Edge */
21 | color: var(--searchbar-text-color);
22 | }
23 |
24 | &:hover, &:active, &:focus, &:active {
25 | --docsearch-searchbox-focus-background: rgba(0, 0, 0, 0.15);
26 | --docsearch-searchbox-shadow: 0 0 0 0.25rem rgba(255, 228, 132, 0.4);
27 | border-color: #ffe484;
28 | background: var(--docsearch-searchbox-focus-background);
29 | box-shadow: var(--docsearch-searchbox-shadow);
30 | color: var(--docsearch-text-color);
31 | outline: none;
32 | }
33 |
34 | }
35 |
36 |
37 | .form-control-feedback {
38 | position: absolute;
39 | text-align: center;
40 | pointer-events: none;
41 | color: var(--searchbar-text-color);;
42 | margin: 10px;
43 | line-height: 0.9rem;
44 | font-size: 20px;
45 | display: inline-block;
46 | -webkit-transform: rotate(-45deg);
47 | -moz-transform: rotate(-45deg);
48 | -o-transform: rotate(-45deg);
49 | -ms-transform: rotate(-45deg);
50 |
51 | }
52 | }
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_timeline.scss:
--------------------------------------------------------------------------------
1 | .timeline-container {
2 |
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | border-radius: $card-border-radius;
7 | background: rgb(226, 244, 255);
8 | background: linear-gradient(197deg, rgb(226, 244, 255) 0%, rgb(237, 248, 255) 25%, rgb(230, 246, 255) 87%, rgb(223, 243, 255) 100%);
9 | //border: 1px solid #068bea;
10 |
11 |
12 | ul, li {
13 | list-style: none;
14 | padding: 0;
15 | }
16 |
17 |
18 | li {
19 | border-left: 1px solid #abaaed;
20 | position: relative;
21 | padding-left: 20px;
22 | margin-left: 10px;
23 | padding-bottom: 1rem;
24 |
25 | &:last-child {
26 | border: 0px;
27 | padding-bottom: 0;
28 | }
29 |
30 | &:before {
31 | content: '';
32 | width: 15px;
33 | height: 15px;
34 | background: white;
35 | border: 1px solid #4e5ed3;
36 | box-shadow: 3px 3px 0px #bab5f8;
37 | border-radius: 50%;
38 | position: absolute;
39 | left: -10px;
40 | top: 0px;
41 | }
42 | }
43 |
44 | //.post-title {
45 | // //color: #2a2839;
46 | // //font-family: 'Poppins', sans-serif;
47 | // font-weight: 500;
48 | // //@include mobile-and-up {
49 | // // font-size: .9rem;
50 | // //}
51 | // //@include mobile-only {
52 | // // margin-bottom: .3rem;
53 | // // font-size: 0.85rem;
54 | // //}
55 | //
56 | //}
57 |
58 | p {
59 | color: #4f4f4f;
60 | font-family: sans-serif;
61 | line-height: 1.5;
62 | margin-top: 0.4rem;
63 | //@include mobile-only {
64 | // font-size: .9rem;
65 | //}
66 | }
67 | }
68 |
69 | //.wrapper {
70 | // background: #eaf6ff;
71 | // padding: 2rem;
72 | // border-radius: 15px;
73 | //}
74 |
75 | //h1 {
76 | // font-size: 1.1rem;
77 | // font-family: sans-serif;
78 | //}
79 |
80 |
--------------------------------------------------------------------------------
/bloggy_frontend/sass/vendor/_toast.scss:
--------------------------------------------------------------------------------
1 | #toastContainer {
2 | position: fixed;
3 | bottom: 20px;
4 | right: 20px;
5 | z-index: 1000;
6 | }
7 |
8 | .toast {
9 | background-color: white;
10 | color: $text-color-black;
11 | padding: 20px 12px;
12 | border-radius: $card-border-radius;
13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
14 | opacity: 0;
15 | transform: translateY(100%);
16 | transition: opacity 0.3s ease, transform 0.3s ease;
17 | font-size: 0.85rem;
18 | max-width: 420px;
19 |
20 | &.error {
21 | background-color: #f13637;
22 | color: white;
23 |
24 | }
25 |
26 | &.warning {
27 | background-color: #f4bb1b;
28 | color: white;
29 | }
30 |
31 | &.primary {
32 | background-color: $primary;
33 | color: white;
34 | }
35 |
36 | &.success {
37 | background-color: #3dc03c;
38 | color: white;
39 | }
40 |
41 | &.info {
42 | background-color: $info;
43 | color: $text-color-black;
44 | }
45 | }
46 |
47 | .show-toast {
48 | opacity: 1;
49 | transform: translateY(0);
50 | }
--------------------------------------------------------------------------------
/demo_content/categories.csv:
--------------------------------------------------------------------------------
1 | title,slug,description,logo,color,publish_status
2 | Spring,spring,An application framework and inversion of control container for the Java platform,uploads/categories/spring.png,#FF5733,LIVE
3 | Java,java,"Java is a high-level, class-based, object-oriented programming language",uploads/categories/java.png,#3366FF,LIVE
4 | MicroServices,microservices,"An architectural approach to software development where software is composed of small independent, self-contained services.",uploads/categories/microservices.png,#33FF33,LIVE
5 | Android,android,"Operating system that was developed by Google, used in smartphones and tablets.",uploads/categories/android.png,#FF9933,LIVE
6 | Git,git,Git is a DevOps tool used for source code management,uploads/categories/git.png,#FF3333,LIVE
7 | Wordpress,wordpress,A free and open-source content management system written in PHP and paired with a MySQL or MariaDB database.,uploads/categories/wordpress.png,#FF33FF,LIVE
8 | Xamarin,xamarin,Developer's tool for cross-platform mobile application development.,uploads/categories/xamarin.png,#33FFFF,LIVE
9 | Spring Boot,spring-boot,"Popular, open-source, enterprise-level framework for creating standalone, production-grade applications that run on the JVM",uploads/categories/spring-boot.png,#33FF99,LIVE
10 | Kotlin,kotlin,"Kotlin is a cross-platform, statically typed, general-purpose programming language with type inference.",uploads/categories/kotlin.png,#3333FF,LIVE
11 | AWS,aws,"Amazon Web Services (AWS) is the world's most comprehensive, scalable, and cost-effective cloud computing solution.",uploads/categories/aws.png,#99FF33,DRAFT
12 | Django,django,A high-level Python web framework for rapid development.,uploads/categories/django.png,#FFFF33,DRAFT
13 | Tech,Tech,Tech blog posts,uploads/categories/microservices.png,#33FFFF,LIVE
14 | Blog,Blog,Tech blog posts,,#33FF33,LIVE
--------------------------------------------------------------------------------
/demo_content/redirect_rules.csv:
--------------------------------------------------------------------------------
1 | source,destination,status_code,note
2 | /tutorials/spring/how-to-configure-pojos-with-java-collection-attributes,/articles/configure-pojos-with-java-collection-attributes-in-spring,301,
3 | /android/download-and-display-image-in-android-gridview,/articles/download-and-display-image-in-android-gridview,301,
4 | /article/how-to-format-your-post,/how-to-format-your-post,301,
5 | /android/android-framelayout-example,/articles/android-framelayout-example,301,
6 | /interview-questions,/articles,301,
7 | /courses/maven,/courses/maven-for-beginners,301,
8 | /how-to-format-your-post,/contribute,301,
9 | /article/download-and-display-image-in-android-gridview,/articles/download-and-display-image-in-android-gridview,301,
10 | /articles/how-to-configure-pojos-with-java-collection-attributes,/articles/configure-pojos-with-java-collection-attributes-in-spring,301,
11 | /courses/maven,/courses/maven-for-beginners,301,
--------------------------------------------------------------------------------
/demo_content/users.csv:
--------------------------------------------------------------------------------
1 | password,username,email,name,is_staff,is_active,website,twitter,linkedin,youtube,github,bio
2 | admin@123,admin,admin@stacktips.com,Nilan,1,1,http://example.com,https://twitter.com/stacktips,https://www.linkedin.com/groups/5008366/,NULL,https://github.com/StackTipsLab,A perfectly crafted blog that developers love.
3 | admin@123,john.doe,john.doe@example.com,John Doe,0,1,http://example.com,https://twitter.com/stacktips,https://www.linkedin.com/groups/5008366/,NULL,https://github.com/StackTipsLab,A perfectly crafted blog that developers love.
--------------------------------------------------------------------------------
/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', 'bloggy.settings')
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/media/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StackTipsLab/bloggy/5420462e170e792c8a416be66406267d51e1874a/media/.DS_Store
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==4.2.7
2 | mysql-connector-python==8.1.0
3 | # mysqlclient==2.2.0
4 | pymysql==1.1.0
5 | django-tinymce==3.6.1
6 | django-widget-tweaks==1.5.0
7 | Pillow==10.1.0
8 | python-dateutil==2.8.2
9 | numerize==0.12
10 | django-summernote==0.8.20.0
11 | beautifulsoup4==4.12.2
12 | html5lib==1.1
13 | pytz==2023.3
14 | djangorestframework==3.14.0
15 | serializers==0.2.4
16 | python-dotenv==1.0.0
17 | # django-ses==3.5.0
18 | django-mail-templated==2.6.5
19 | # redis==4.5.1
20 | # django-redis==5.2.0
21 | six==1.16.0
22 | gunicorn==21.2.0
23 | whitenoise==6.6.0
24 | boto3==1.28.68
25 | django-storages==1.14.2
26 | django-debug-toolbar==4.2.0
27 | bleach==6.1.0
28 | pylibmc==1.6.3
29 | django-hitcount==1.3.5
30 | django-colorfield==0.10.1
31 | requests==2.31.0
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.9.13
--------------------------------------------------------------------------------
/vetur.config.js:
--------------------------------------------------------------------------------
1 | // vetur.config.js
2 | /** @type {import('vls').VeturConfig} */
3 | module.exports = {
4 | // **optional** default: `{}`
5 | // override vscode settings
6 | // Notice: It only affects the settings used by Vetur.
7 | settings: {
8 | "vetur.useWorkspaceDependencies": true,
9 | "vetur.experimental.templateInterpolationService": true
10 | },
11 | // **optional** default: `[{ root: './' }]`
12 | // support monorepos
13 | projects: [
14 | './packages/repo2', // Shorthand for specifying only the project root location
15 | {
16 | // **required**
17 | // Where is your project?
18 | // It is relative to `vetur.config.js`.
19 | root: '/bloggy/static',
20 | // **optional** default: `'package.json'`
21 | // Where is `package.json` in the project?
22 | // We use it to determine the version of vue.
23 | // It is relative to root property.
24 | package: '/bloggy/static/package.json',
25 | // **optional**
26 | // Where is TypeScript config file in the project?
27 | // It is relative to root property.
28 | tsconfig: './tsconfig.json',
29 | // **optional** default: `'./.vscode/vetur/snippets'`
30 | // Where is vetur custom snippets folders?
31 | snippetFolder: './.vscode/vetur/snippets',
32 | // **optional** default: `[]`
33 | // Register globally Vue component glob.
34 | // If you set it, you can get completion by that components.
35 | // It is relative to root property.
36 | // Notice: It won't actually do it. You need to use `require.context` or `Vue.component`
37 | globalComponents: [
38 | './js/vue/*.vue'
39 | ]
40 | }
41 | ]
42 | }
--------------------------------------------------------------------------------