├── .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 |

33 | 34 | Logo 39 | {{ site_header|default:_('Django administration') }} 40 | 41 |

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 | 19 | 20 |
21 | {% include "partials/header.html" %} 22 |
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 | 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 | {{ widget.name }} 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 | {{ author.get_full_name }} 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 | {{ user.name }} 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 |

10 | {{ post.title }} 11 |

12 | 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 | {{ author.username }} avtar 15 | 16 |
17 |
18 | 19 | {{ author.get_full_name_or_username }} 20 | 21 |   |   22 | {% for category in post.category.all %} 23 | 26 | #{{ category.title }} 27 | {% if forloop.last != True %} 28 | 29 | {% endif %} 30 | 31 | {% endfor %} 32 |   |   33 | 34 | {{ post.updated_date|date:"M d, Y" }},  35 | 36 | 37 |
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 |

15 | {{ post.title }} 16 |

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 |
4 |
5 |
6 | {{ author.username }} avtar 13 | 14 |
15 |
16 |
17 |

{{ author.get_full_name_or_username }}

18 |

{{ author.bio | safe }}

19 |
20 | {% include 'partials/user_profile_social_media_links.html' with user=author %} 21 | 22 | {% if author.username %} 23 | Read all articles → 25 | {% endif %} 26 |
27 |
28 |
29 |
30 |
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 | {{ category.title }} 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 | {{ course.title }} 6 | {% endif %} 7 |
8 |

9 | {{ course.title }} 11 |

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 |

{{ course.title }}

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 | Contact 11 | · 12 | Privacy policy 13 | · 14 | Terms of service 15 | · 16 | Cookies 17 | · 18 | Code of conduct 19 | · 20 | About 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 |
10 |

11 |   Checkout source code

12 |

Checkout the complete source code from our GitHub repo.

13 | 14 | 15 | Download project 16 |
17 |
18 | {% endif %} 19 | {% endspaceless %} -------------------------------------------------------------------------------- /bloggy/templates/partials/home_article_breadcrumb.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 16 |
17 |
-------------------------------------------------------------------------------- /bloggy/templates/partials/home_lesson_breadcrumb.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 19 |
20 |
-------------------------------------------------------------------------------- /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 | 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 | 16 |
17 |
18 | {% endif %} -------------------------------------------------------------------------------- /bloggy/templates/partials/newsletter.html: -------------------------------------------------------------------------------- 1 | {% if user.is_authenticated == False %} 2 |
3 |

Join the list

4 | Sign up for weekly tutorials, and (exclusive) freebies directly in your inbox. 5 |
6 |
7 | 9 |
10 | Please enter a valid email address. 11 |
12 |
13 |
14 | 16 |
17 | Please enter your name. 18 |
19 |
20 | 21 |
22 | Great content, no spam, easy unsubscribe 23 |
24 | {% endif %} -------------------------------------------------------------------------------- /bloggy/templates/partials/pages_menu.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load custom_widgets %} 3 | 27 |
28 | 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 | 24 |
25 | {% endif %} 26 | {% endspaceless %} -------------------------------------------------------------------------------- /bloggy/templates/partials/social_share.html: -------------------------------------------------------------------------------- 1 |
2 | {% load social_share %} 3 | {% spaceless %} 4 |
5 | {% if authorName %} 6 |

Did you like what {{ authorName }} wrote? 7 |
Thank them for their work by sharing it on social 8 | media.

9 | {% else %} 10 |

Was this post helpful to you? Please consider sharing this on your social media 11 | accounts.

12 | {% endif %} 13 | {% post_to_facebook object "Facebook" %} 14 | {% post_to_twitter "{{ object.title }}" object "Share on X" %} 15 | {% send_email object.title "{{ object.title }}. Check it out!" object_or_url "Email link" %} 16 | {% copy_to_clipboard object "Copy link" %} 17 | {% add_copy_script %} 18 |
19 | {% endspaceless %} 20 |
-------------------------------------------------------------------------------- /bloggy/templates/partials/theme_switch.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /bloggy/templates/partials/toc_widget.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
3 | 4 |
5 |
6 |
-------------------------------------------------------------------------------- /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 |
12 | 14 | Watch on YouTube 15 |
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 | 6 | -------------------------------------------------------------------------------- /bloggy/templates/social_share/post_to_facebook.html: -------------------------------------------------------------------------------- 1 |
2 | {{ link_text }} 3 |
-------------------------------------------------------------------------------- /bloggy/templates/social_share/post_to_linkedin.html: -------------------------------------------------------------------------------- 1 | {% load i18n social_share %}{% get_current_language as LANGUAGE_CODE %} 2 | -------------------------------------------------------------------------------- /bloggy/templates/social_share/post_to_twitter.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ link_text }} 4 |
-------------------------------------------------------------------------------- /bloggy/templates/social_share/send_email.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{link_text}} 4 |
-------------------------------------------------------------------------------- /bloggy/templates/widgets/categories.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% if categories %} 3 | {% if widgetStyle == "home" %} 4 |
5 | 15 |
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 | 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 | -------------------------------------------------------------------------------- /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 | 10 | 11 | -------------------------------------------------------------------------------- /bloggy_frontend/assets/love.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 |