diff --git a/.pylintrc b/.pylintrc
index 0111a6b..ff28161 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,2 +1,3 @@
[MASTER]
-ignore=migrations
\ No newline at end of file
+ignore=migrations
+disable=C0114, # missing-module-docstring
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5d23c7f..8e5f771 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,6 +5,7 @@
"editor.formatOnSave": false,
"python.linting.enabled": true,
"python.linting.lintOnSave": true,
- // "editor.fontFamily": "Dank Mono, JetBrains Mono NL, Fira Code, Menlo, Monaco, 'Courier New', monospace",
- // "editor.fontSize": 14,
+// "editor.fontFamily": "Dank Mono, JetBrains Mono NL, Fira Code, Menlo, Monaco, 'Courier New', monospace",
+// "editor.fontSize": 14,
+ "python.linting.pylintArgs": ["--disable=C0111"]
}
\ No newline at end of file
diff --git a/bloggy/.DS_Store b/bloggy/.DS_Store
index 8e1f43b..9ca3d85 100644
Binary files a/bloggy/.DS_Store and b/bloggy/.DS_Store differ
diff --git a/bloggy/admin/__init__.py b/bloggy/admin/__init__.py
index 118d098..1d787a5 100644
--- a/bloggy/admin/__init__.py
+++ b/bloggy/admin/__init__.py
@@ -7,3 +7,4 @@
from .user_admin import *
from .page_admin import *
from .quiz_admin import *
+from .newsletter_admin import *
diff --git a/bloggy/admin/course_admin.py b/bloggy/admin/course_admin.py
index 138c5b0..3bbd8c9 100644
--- a/bloggy/admin/course_admin.py
+++ b/bloggy/admin/course_admin.py
@@ -1,6 +1,9 @@
+from datetime import timezone
+
from django.contrib import admin
from bloggy.admin import BloggyAdmin, BloggyAdminForm, seo_fieldsets, publication_fieldsets
from bloggy.models.course import Course
+from bloggy.services.post_service import cleanse_html
class CourseForm(BloggyAdminForm):
@@ -45,5 +48,6 @@ class CourseAdmin(BloggyAdmin):
list_display_links = ['title']
form = CourseForm
-
-
+ def save_model(self, request, obj, form, change):
+ obj.description = cleanse_html(obj.description)
+ super().save_model(request, obj, form, change)
diff --git a/bloggy/admin/misc_admin.py b/bloggy/admin/misc_admin.py
index 88f58d7..efed2b7 100644
--- a/bloggy/admin/misc_admin.py
+++ b/bloggy/admin/misc_admin.py
@@ -2,19 +2,9 @@
import bloggy.models.option
from bloggy import settings
-from bloggy.models import RedirectRule
admin.site.site_header = settings.SITE_TITLE.upper()
admin.site.site_title = settings.SITE_TITLE
admin.site.index_title = "Dashboard"
admin.site.register(bloggy.models.option.Option)
-
-@admin.register(RedirectRule)
-class RedirectRuleAdmin(admin.ModelAdmin):
- list_display = (
- 'source',
- 'destination',
- 'status_code',
- 'note',
- )
diff --git a/bloggy/admin/newsletter_admin.py b/bloggy/admin/newsletter_admin.py
new file mode 100644
index 0000000..b35f773
--- /dev/null
+++ b/bloggy/admin/newsletter_admin.py
@@ -0,0 +1,36 @@
+from django import forms
+from django.contrib import admin
+
+from bloggy.admin import BloggyAdmin, publication_fieldsets
+from bloggy.models.newsletter import Newsletter
+
+
+class NewsletterForm(forms.ModelForm):
+ model = Newsletter
+
+
+@admin.register(Newsletter)
+class PageAdmin(BloggyAdmin):
+ prepopulated_fields = {"url": ("title",)}
+ list_display = (
+ 'id',
+ 'title',
+ 'url',
+ 'publish_status',
+ )
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', 'url', 'content',)
+ }), publication_fieldsets)
+
+ search_fields = ['title']
+ summernote_fields = ('content',)
+ readonly_fields = ['updated_date', 'created_date']
+ date_hierarchy = 'published_date'
+ form = NewsletterForm
+ ordering = ('-created_date',)
+ list_display_links = ['title']
+
+ def get_form(self, request, obj=None, change=False, **kwargs):
+ return super().get_form(request, obj, change, **kwargs)
diff --git a/bloggy/admin/page_admin.py b/bloggy/admin/page_admin.py
index 63ad13d..0fbec7d 100644
--- a/bloggy/admin/page_admin.py
+++ b/bloggy/admin/page_admin.py
@@ -14,6 +14,7 @@ class PageAdmin(BloggyAdmin):
list_display = (
'id',
'title',
+ 'template_type',
'url',
'excerpt',
'publish_status',
@@ -21,7 +22,7 @@ class PageAdmin(BloggyAdmin):
fieldsets = (
(None, {
- 'fields': ('title', 'excerpt', 'url', 'content',)
+ 'fields': ('title', 'template_type', 'excerpt', 'url', 'content',)
}), publication_fieldsets, seo_fieldsets)
search_fields = ['title']
diff --git a/bloggy/admin/post_admin.py b/bloggy/admin/post_admin.py
index ac04af1..98f36aa 100644
--- a/bloggy/admin/post_admin.py
+++ b/bloggy/admin/post_admin.py
@@ -7,6 +7,7 @@
from bloggy.admin import BloggyAdmin, BloggyAdminForm, publication_fieldsets, seo_fieldsets
from bloggy.models import Post
+from bloggy.services.post_service import cleanse_html
class PostForm(BloggyAdminForm):
@@ -93,14 +94,14 @@ def author_link(self, obj):
author_link.short_description = 'Author'
def view_on_site(self, obj):
- url = reverse('post_single', kwargs={'slug': obj.slug})
- return url + "?context=preview"
+ return obj.get_absolute_url() + "?context=preview"
def save_model(self, request, obj, form, change):
if "publish_status" in form.changed_data and obj.publish_status == "LIVE" and not obj.published_date:
obj.published_date = timezone.now()
if not obj.pk:
obj.author = request.user
+ obj.content = cleanse_html(obj.content)
super().save_model(request, obj, form, change)
diff --git a/bloggy/admin/quiz_admin.py b/bloggy/admin/quiz_admin.py
index 17def79..c414a79 100644
--- a/bloggy/admin/quiz_admin.py
+++ b/bloggy/admin/quiz_admin.py
@@ -35,7 +35,7 @@ class QuizAdmin(BloggyAdmin):
}),
publication_fieldsets, seo_fieldsets)
- summernote_fields = ('description',)
+ summernote_fields = ('content',)
readonly_fields = ['thumbnail_tag']
ordering = ('-display_order',)
list_display_links = ['title']
diff --git a/bloggy/forms/change_password_form.py b/bloggy/forms/change_password_form.py
new file mode 100644
index 0000000..d4482b2
--- /dev/null
+++ b/bloggy/forms/change_password_form.py
@@ -0,0 +1,30 @@
+from django import forms
+from django.contrib.auth.forms import PasswordChangeForm
+
+
+class ChangePasswordForm(PasswordChangeForm):
+ old_password = forms.CharField(
+ label="Old password",
+ strip=False,
+ widget=forms.PasswordInput(
+ attrs={"autocomplete": "current-password", "autofocus": True, 'class': 'form-control'}
+ ),
+ )
+
+ new_password1 = forms.CharField(
+ label="New Password",
+ help_text='Please use 8 or more characters with a mix of letters, numbers & symbols',
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}, )
+ )
+ new_password2 = forms.CharField(
+ label="Confirm New Password",
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ )
+
+ def clean_new_password2(self):
+ new_password1 = self.cleaned_data.get('new_password1')
+ new_password2 = self.cleaned_data.get('new_password2')
+
+ if new_password1 and new_password2 and new_password1 != new_password2:
+ raise forms.ValidationError("Passwords do not match.")
+ return new_password2
diff --git a/bloggy/forms/create_newsletter_form.py b/bloggy/forms/create_newsletter_form.py
new file mode 100644
index 0000000..7447efc
--- /dev/null
+++ b/bloggy/forms/create_newsletter_form.py
@@ -0,0 +1,43 @@
+from django import forms
+
+
+class CreateNewsletterForm(forms.Form):
+ title = forms.CharField(
+ label="Newsletter title",
+ max_length=254,
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'placeholder': 'Weekly newsletter'}),
+ )
+
+ # published_date = forms.DateTimeField(
+ # label="Published date",
+ # help_text="Date time for sending the newsletter",
+ # widget=forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'date'})
+ # )
+
+ json_file = forms.FileField(
+ label='Upload JSON File',
+ help_text="Upload a valid json file",
+ widget=forms.ClearableFileInput(attrs={'class': 'form-control'})
+ )
+
+ send_to_all = forms.BooleanField(
+ label="Send to all",
+ required=False,
+ help_text="This newsletter will be send all users",
+ widget=forms.CheckboxInput(
+ attrs={
+ 'class': 'form-check-input mx-2',
+ }),
+ )
+
+ send_to_users_only = forms.BooleanField(
+ label="Send to registered users only",
+ required=False,
+ help_text="This newsletter will be send only to registered users",
+ widget=forms.CheckboxInput(
+ attrs={
+ 'class': 'form-check-input mx-2',
+ }),
+
+ )
diff --git a/bloggy/forms/custom_input_fields.py b/bloggy/forms/custom_input_fields.py
new file mode 100644
index 0000000..44ccb9a
--- /dev/null
+++ b/bloggy/forms/custom_input_fields.py
@@ -0,0 +1,3 @@
+from django.forms import ClearableFileInput
+class NonClearableFileInput(ClearableFileInput):
+ template_name = 'forms/widgets/non_clearable_imagefield.html'
diff --git a/bloggy/forms/edit_profile_form.py b/bloggy/forms/edit_profile_form.py
index 68c789b..65c26e6 100644
--- a/bloggy/forms/edit_profile_form.py
+++ b/bloggy/forms/edit_profile_form.py
@@ -1,25 +1,26 @@
from django import forms
-from django.forms import ClearableFileInput
+from bloggy.forms.custom_input_fields import NonClearableFileInput
from bloggy.models import User
-class NonClearableFileInput(ClearableFileInput):
- template_name = 'forms/widgets/non_clearable_imagefield.html'
-
-
class EditProfileForm(forms.ModelForm):
class Meta:
model = User
+ help_texts = {
+ "bio": "This will be displayed publicly on your profile. Keep it short and crisp.",
+ 'receive_news_updates': "News about product and feature updates",
+ 'receive_new_content': "Get notified when new content is added"
+ }
+
+ labels = {
+ 'receive_news_updates': "News and updates",
+ 'receive_new_content': "New tutorials & courses"
+ }
+
fields = [
- 'profile_photo',
- 'name',
- 'website',
- 'twitter',
- 'linkedin',
- 'youtube',
- 'github',
- 'bio'
+ 'profile_photo', 'name', 'website', 'twitter', 'linkedin', 'youtube', 'github', 'bio',
+ 'receive_news_updates', 'receive_new_content'
]
widgets = {
@@ -61,4 +62,13 @@ class Meta:
'rows': 5,
'placeholder': 'Your github'
}),
+
+ 'receive_news_updates': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input',
+ }),
+
+ 'receive_new_content': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input',
+ }),
+
}
diff --git a/bloggy/forms/manage_newsletter_form.py b/bloggy/forms/manage_newsletter_form.py
new file mode 100644
index 0000000..d2d492e
--- /dev/null
+++ b/bloggy/forms/manage_newsletter_form.py
@@ -0,0 +1,60 @@
+from django import forms
+
+from bloggy.models.newsletter import Newsletter
+
+
+class ManageNewsLetterForm(forms.ModelForm):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.instance = kwargs.get('instance')
+
+ class Meta:
+ model = Newsletter
+ fields = [
+ 'title', 'url',
+ 'content',
+ 'publish_status',
+ 'send_to_users_only',
+ 'send_to_all'
+ ]
+
+ widgets = {
+ 'title': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'rows': 5,
+ 'placeholder': 'Enter title'
+ }),
+
+ 'url': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'rows': 5,
+ 'placeholder': 'Enter url'
+ }),
+
+ 'content': forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 15,
+ 'placeholder': "url",
+ }),
+
+ 'publish_status': forms.Select(attrs={'class': 'form-control'}),
+ 'receive_news_updates': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input',
+ }),
+
+ 'receive_new_content': forms.CheckboxInput(attrs={
+ 'class': 'form-check-input',
+ }),
+
+ 'send_to_all': forms.CheckboxInput(
+ attrs={
+ 'class': 'form-check-input mx-2',
+ }),
+
+ 'send_to_users_only': forms.CheckboxInput(
+ attrs={
+ 'class': 'form-check-input mx-2',
+ }),
+
+ }
diff --git a/bloggy/forms/password_reset_form.py b/bloggy/forms/password_reset_form.py
new file mode 100644
index 0000000..be949e0
--- /dev/null
+++ b/bloggy/forms/password_reset_form.py
@@ -0,0 +1,16 @@
+from django import forms
+from django.contrib.auth.forms import PasswordResetForm
+
+
+class CustomPasswordResetForm(PasswordResetForm):
+ def __init__(self, *args, **kwargs):
+ super(CustomPasswordResetForm, self).__init__(*args, **kwargs)
+
+ email = forms.EmailField(
+ label="Email",
+ max_length=254,
+ widget=forms.EmailInput(
+ attrs={"autocomplete": "email", 'class': 'form-control', 'placeholder': 'Enter your registered email'}),
+ )
+
+
diff --git a/bloggy/forms/set_password_form.py b/bloggy/forms/set_password_form.py
new file mode 100644
index 0000000..41c2c62
--- /dev/null
+++ b/bloggy/forms/set_password_form.py
@@ -0,0 +1,20 @@
+from django import forms
+from django.contrib.auth import password_validation
+from django.contrib.auth.forms import SetPasswordForm
+
+
+class CustomSetPasswordForm(SetPasswordForm):
+ def __init__(self, *args, **kwargs):
+ super(CustomSetPasswordForm, self).__init__(*args, **kwargs)
+
+ new_password1 = forms.CharField(
+ label="New password",
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
+ strip=False,
+ help_text=password_validation.password_validators_help_text_html(),
+ )
+ new_password2 = forms.CharField(
+ label="Confirm password",
+ strip=False,
+ widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
+ )
diff --git a/bloggy/forms/update_username_form.py b/bloggy/forms/update_username_form.py
new file mode 100644
index 0000000..8bf2914
--- /dev/null
+++ b/bloggy/forms/update_username_form.py
@@ -0,0 +1,31 @@
+from django import forms
+from django.contrib.auth import get_user_model
+
+
+class UpdateUsernameForm(forms.ModelForm):
+ class Meta:
+ model = get_user_model()
+ fields = ['username']
+ help_texts={
+ 'username': 'Letters, digits and "_" only allowed.'
+ }
+ labels = {
+ "username": "New username",
+ }
+
+ widgets = {
+ 'username': forms.TextInput(attrs={
+ 'class': 'form-control',
+ 'placeholder': 'Username',
+ }),
+ }
+
+ def clean_username(self):
+ new_username = self.cleaned_data['username']
+ user_model = get_user_model()
+
+ # Check if the new username is available
+ if user_model.objects.filter(username=new_username).exclude(pk=self.instance.pk).exists():
+ raise forms.ValidationError('This username is already in use. Please choose a different one.')
+
+ return new_username
diff --git a/bloggy/management/commands/newsletter.json b/bloggy/management/commands/newsletter.json
new file mode 100644
index 0000000..52b7f64
--- /dev/null
+++ b/bloggy/management/commands/newsletter.json
@@ -0,0 +1,53 @@
+{
+ "updates": {
+ "heading": "In the news this week..",
+ "list": [
+ {
+ "title": "Project Lombok-Is it Still Relevant in 2023?",
+ "thumbnail": "https://media.stacktips.com/media/uploads/posts/Project_Lombok.jpg",
+ "link": "https://stacktips.com/articles/project-lombok-is-it-still-relevant-in-2023",
+ "excerpt": "What is Project Lombok? Have you used this magical library? With the new Java Records feature, you might wonder if Lombok is still relevant. Let's weigh the pros and cons and see if this is suitable for you."
+ },
+ {
+ "title": "Notes to Crack CLF-C01 AWS Certified Cloud Practitioner Exam on First Attempt",
+ "thumbnail": "https://media.stacktips.com/media/uploads/articles/crack-clfc01-aws-certified-cloud-practitioner-in-first-attempt.jpeg",
+ "link": "https://stacktips.com/articles/crack-clfc01-aws-certified-cloud-practitioner-on-first-attempt",
+ "excerpt": "A quick guide to CLF-C01 AWS Certified Cloud Practitioner Practice exam notes covers the introduction to all the services provided by AWS."
+ },
+ {
+ "title": "Replace Embedded Tomcat with Jetty or Undertow Server in Spring Boot 3",
+ "thumbnail": "https://i3.ytimg.com/vi/1gEoiMVULt4/maxresdefault.jpg",
+ "link": "https://youtu.be/1gEoiMVULt4",
+ "excerpt": "This video explains how to replace the default embedded Tomcat with Jetty or Undertow servers"
+ },
+ {
+ "title": "Schedule Task in Spring Boot Using @Scheduled Annotation",
+ "thumbnail": "",
+ "link": "https://stacktips.com/articles/schedule-task-in-spring-boot-using-scheduled-annotation",
+ "excerpt": "Scheduling task in Spring boot using @Scheduled annotation with examples showcasing fixed rate, fixed delay, and using cron expressions."
+ }
+ ]
+ },
+
+ "news": {
+ "heading": "In the news this week..",
+ "list": [
+ {
+ "title": "Elon Musk's 'Anti-Woke' AI Is Here, Snowflakes Need Not Apply",
+ "link": "#"
+ },
+ {
+ "title": "Channel 4 and Snapchat extend and enhance partnership with 'Snap-first' programming",
+ "link": "#"
+ },
+ {
+ "title": "TikTok adds comment filtering tools to better handle Israel-Hamas war content",
+ "link": "#"
+ },
+ {
+ "title": "GTA trailer dropped early after leak and breaks all records.",
+ "link": "#"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/bloggy/management/commands/runseed.py b/bloggy/management/commands/runseed.py
index b8658bc..848a651 100644
--- a/bloggy/management/commands/runseed.py
+++ b/bloggy/management/commands/runseed.py
@@ -19,7 +19,6 @@ def handle(self, *args, **options):
('seed_categories', 'categories.csv'),
('seed_posts', 'posts.csv'),
('seed_pages', 'pages.csv'),
- ('seed_redirectrules', 'redirect_rules.csv'),
('update_category_count', None),
]
diff --git a/bloggy/management/commands/seed_redirectrules.py b/bloggy/management/commands/seed_redirectrules.py
deleted file mode 100644
index a37e6a7..0000000
--- a/bloggy/management/commands/seed_redirectrules.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import csv
-
-from django.core.management.base import BaseCommand
-from django.utils.text import slugify
-
-from bloggy.models import Category, RedirectRule
-
-
-class Command(BaseCommand):
- help = 'Importing redirect rules'
-
- def add_arguments(self, parser):
- parser.add_argument('-f', '--file', type=str,
- help="File path to import, e.g. ~/bloggy/demo_content/redirect_rules.csv")
-
- def handle(self, *args, **options):
- file_path = options['file']
-
- counter = 0
- with open(file_path, encoding="utf-8") as f:
- reader = csv.reader(f)
- print('Importing redirect rules', file_path)
-
- for index, row in enumerate(reader):
- if index > 0:
- counter = counter + 1
- RedirectRule.objects.get_or_create(
- from_url=row[0],
- to_url=row[1],
- status_code=row[2],
- note=row[3]
- )
-
- self.stdout.write(self.style.SUCCESS(f"%s redirect rules imported" % counter))
diff --git a/bloggy/management/commands/send_account_activation_reminders.py b/bloggy/management/commands/send_account_activation_reminders.py
new file mode 100644
index 0000000..c68dce7
--- /dev/null
+++ b/bloggy/management/commands/send_account_activation_reminders.py
@@ -0,0 +1,47 @@
+from django.core.management.base import BaseCommand
+from django.urls import reverse
+
+from bloggy import settings
+from bloggy.services import email_service
+from bloggy.services.token_service import create_token
+from bloggy.models import User
+
+
+class Command(BaseCommand):
+ help = 'Send email reminders to users who have not activated their accounts'
+
+ def handle(self, *args, **kwargs):
+ # Get a list of users who have not activated their accounts
+ inactive_users = User.objects.filter(is_active=False)
+
+ email_count = 0
+ for user in inactive_users:
+ subject = 'Reminder: Verify your email to activate your account'
+
+ verification_token = create_token(user=user, token_type="signup")
+ verification_link = reverse("activate_account", args=[
+ verification_token.uuid,
+ verification_token.token
+ ])
+
+ args = {
+ "email_subject": subject,
+ "app_name": settings.SITE_TITLE,
+ "verification_link": settings.SITE_URL + verification_link
+ }
+
+ try:
+ email_service.send_html_email(
+ subject,
+ [user.email],
+ "email/account_activation_reminder_email.html",
+ args)
+ print('Success: Account activation reminder mail sent to {}', user.email)
+
+ except Exception as ex:
+ print('Error: sending email to {}: {}', user.email, ex)
+
+ finally:
+ email_count = email_count + 1
+
+ self.stdout.write(self.style.SUCCESS(f"Reminder sent to {email_count} users"))
diff --git a/bloggy/management/commands/send_card.py b/bloggy/management/commands/send_card.py
new file mode 100644
index 0000000..7c60d51
--- /dev/null
+++ b/bloggy/management/commands/send_card.py
@@ -0,0 +1,51 @@
+from django.core.management.base import BaseCommand
+from bloggy import settings
+from bloggy.models.subscriber import Subscribers
+from bloggy.services import email_service
+from bloggy.models import User
+from itertools import chain
+
+
+# python3 manage.py send_card --cardImgUrl="https://media.stacktips.com/media/emails/christmas-card.gif" --subject="Merry Christmas." --targetLink="https://stacktips.com"
+class Command(BaseCommand):
+ help = 'Send wish card to users'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--cardImgUrl', type=str, required=True, help='URL of the card')
+ parser.add_argument('--subject', type=str, required=True, help='Email subject')
+ parser.add_argument('--targetLink', type=str, required=True, help='Target link')
+
+ def handle(self, *args, **options):
+ card_img_url = options['cardImgUrl']
+ subject = options['subject']
+ target_link = options['targetLink']
+
+ if card_img_url is None or target_link is None or subject is None:
+ self.stdout.write(
+ self.style.ERROR(f"Missing mandatory arguments --cardImgUrl or --subject or --targetLink"))
+
+ else:
+ users = chain(
+ User.objects.all()
+ # Subscribers.objects.all(),
+ )
+
+ email_count = 0
+ for user in users:
+ args = {
+ "user_name": user.name,
+ "email_subject": subject,
+ "app_name": settings.SITE_TITLE,
+ "card_img_url": card_img_url,
+ "card_target_link": target_link
+ }
+
+ try:
+ email_service.send_html_email(subject, [user.email], "email/wish_card_email.html", args)
+ print('Success: Card sent to {}', user.email)
+ except Exception as ex:
+ print('Error sending card to {}: {}', user.email, ex)
+ finally:
+ email_count = email_count + 1
+
+ self.stdout.write(self.style.SUCCESS(f"Reminder sent to {email_count} users"))
diff --git a/bloggy/management/commands/send_newsletter.py b/bloggy/management/commands/send_newsletter.py
new file mode 100644
index 0000000..0e14144
--- /dev/null
+++ b/bloggy/management/commands/send_newsletter.py
@@ -0,0 +1,47 @@
+from django.core.management.base import BaseCommand
+from bloggy import settings
+from bloggy.models.subscriber import Subscribers
+from bloggy.services import email_service
+from bloggy.models import User
+from itertools import chain
+
+
+class Command(BaseCommand):
+ help = 'Send wish card to users'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--content', type=str, required=True, help='URL of the card')
+ parser.add_argument('--subject', type=str, required=True, help='Email subject')
+
+ def handle(self, *args, **options):
+ content = options['content']
+ subject = options['subject']
+
+ if content is None or subject is None:
+ self.stdout.write(
+ self.style.ERROR(f"Missing mandatory arguments --content or --subject"))
+
+ else:
+ users = chain(
+ User.objects.all(),
+ Subscribers.objects.all(),
+ )
+
+ email_count = 0
+ for user in users:
+ args = {
+ "user_name": user.name,
+ "email_subject": subject,
+ "app_name": settings.SITE_TITLE,
+ "updates": ""
+ }
+
+ try:
+ email_service.send_html_email(subject, [user.email], "email/wish_card_email.html", args)
+ print('Success: Card sent to {}', user.email)
+ except Exception as ex:
+ print('Error sending card to {}: {}', user.email, ex)
+ finally:
+ email_count = email_count + 1
+
+ self.stdout.write(self.style.SUCCESS(f"Reminder sent to {email_count} users"))
diff --git a/bloggy/middleware/redirect.py b/bloggy/middleware/redirect.py
deleted file mode 100644
index 19d6044..0000000
--- a/bloggy/middleware/redirect.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import logging
-
-from django.http import HttpResponsePermanentRedirect
-from django.utils.deprecation import MiddlewareMixin
-
-from bloggy import settings
-from bloggy.models import RedirectRule
-
-logger = logging.getLogger(__name__)
-
-
-class RedirectMiddleware(MiddlewareMixin):
-
- def process_response(self, request, response):
- request_path = request.path
-
- # Don't do anything for /api endpoints
- if request_path.startswith("/api/"):
- return response
-
- if response.status_code == 404:
- logger.warning("ERROR 404:: %s", request_path)
- redirect_rule = RedirectRule.objects.filter(source__exact=request_path).first()
-
- if redirect_rule:
- logger.warning("Explicit redirect rule found %s ==> %s", redirect_rule.source,
- redirect_rule.destination)
- return HttpResponsePermanentRedirect(settings.SITE_URL + redirect_rule.destination)
-
- return response
diff --git a/bloggy/migrations/0005_page_template_type_alter_post_post_type_and_more.py b/bloggy/migrations/0005_page_template_type_alter_post_post_type_and_more.py
new file mode 100644
index 0000000..e5b500d
--- /dev/null
+++ b/bloggy/migrations/0005_page_template_type_alter_post_post_type_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.7 on 2023-12-22 15:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0004_remove_redirectrule_is_regx'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='page',
+ name='template_type',
+ field=models.CharField(blank=True, choices=[('naked', 'Naked'), ('default', 'Default'), ('wide', 'Wide')], default='default', help_text='Template type', max_length=20, null=True, verbose_name='Template type'),
+ ),
+ migrations.AlterField(
+ model_name='post',
+ name='post_type',
+ field=models.CharField(blank=True, choices=[['post', 'Post']], default='article', help_text='Post type', max_length=20, null=True, verbose_name='Post type'),
+ ),
+ migrations.AlterField(
+ model_name='vote',
+ name='post_type',
+ field=models.CharField(choices=[['post', 'Post']], help_text='Select content type', max_length=20, verbose_name='Content type'),
+ ),
+ ]
diff --git a/bloggy/migrations/0006_alter_page_template_type.py b/bloggy/migrations/0006_alter_page_template_type.py
new file mode 100644
index 0000000..dcfe03f
--- /dev/null
+++ b/bloggy/migrations/0006_alter_page_template_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2023-12-22 16:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0005_page_template_type_alter_post_post_type_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='page',
+ name='template_type',
+ field=models.CharField(blank=True, choices=[('newsletter', 'Newsletter'), ('naked', 'Naked'), ('default', 'Default')], default='default', help_text='Template type', max_length=20, null=True, verbose_name='Template type'),
+ ),
+ ]
diff --git a/bloggy/migrations/0007_alter_post_difficulty_alter_post_post_type_and_more.py b/bloggy/migrations/0007_alter_post_difficulty_alter_post_post_type_and_more.py
new file mode 100644
index 0000000..e4bb12a
--- /dev/null
+++ b/bloggy/migrations/0007_alter_post_difficulty_alter_post_post_type_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.7 on 2024-01-04 00:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0006_alter_page_template_type'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='post',
+ name='difficulty',
+ field=models.CharField(blank=True, choices=[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advance', 'Advance')], default='easy', help_text='Select difficulty', max_length=20, null=True, verbose_name='Difficulty level'),
+ ),
+ migrations.AlterField(
+ model_name='post',
+ name='post_type',
+ field=models.CharField(blank=True, choices=[['post', 'Post'], ['lesson', 'Lesson']], default='article', help_text='Post type', max_length=20, null=True, verbose_name='Post type'),
+ ),
+ migrations.AlterField(
+ model_name='vote',
+ name='post_type',
+ field=models.CharField(choices=[['post', 'Post'], ['lesson', 'Lesson']], help_text='Select content type', max_length=20, verbose_name='Content type'),
+ ),
+ ]
diff --git a/bloggy/migrations/0008_user_receive_new_content_user_receive_news_updates.py b/bloggy/migrations/0008_user_receive_new_content_user_receive_news_updates.py
new file mode 100644
index 0000000..60ce011
--- /dev/null
+++ b/bloggy/migrations/0008_user_receive_new_content_user_receive_news_updates.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.7 on 2024-01-10 14:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0007_alter_post_difficulty_alter_post_post_type_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='receive_new_content',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='receive_news_updates',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/bloggy/migrations/0009_newsletter_delete_redirectrule.py b/bloggy/migrations/0009_newsletter_delete_redirectrule.py
new file mode 100644
index 0000000..9bf6082
--- /dev/null
+++ b/bloggy/migrations/0009_newsletter_delete_redirectrule.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.2.7 on 2024-01-12 22:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0008_user_receive_new_content_user_receive_news_updates'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Newsletter',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_date', models.DateTimeField(auto_created=True, auto_now_add=True, null=True)),
+ ('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('title', models.CharField(help_text='Enter title', max_length=300)),
+ ('url', models.CharField(help_text='Enter url', max_length=150, unique=True)),
+ ('content', models.JSONField()),
+ ('content_html', models.TextField(help_text='Newsletter content', null=True)),
+ ('published_date', models.DateTimeField(blank=True, null=True)),
+ ('publish_status', models.CharField(blank=True, choices=[('DRAFT', 'DRAFT'), ('LIVE', 'LIVE'), ('DELETED', 'DELETED')], default='DRAFT', help_text='Select publish status', max_length=20, null=True, verbose_name='Publish status')),
+ ],
+ options={
+ 'verbose_name': 'Newsletter',
+ 'verbose_name_plural': 'Newsletters',
+ },
+ ),
+ migrations.DeleteModel(
+ name='RedirectRule',
+ ),
+ ]
diff --git a/bloggy/migrations/0010_newsletter_send_to_all_newsletter_send_to_users_only.py b/bloggy/migrations/0010_newsletter_send_to_all_newsletter_send_to_users_only.py
new file mode 100644
index 0000000..0bd0154
--- /dev/null
+++ b/bloggy/migrations/0010_newsletter_send_to_all_newsletter_send_to_users_only.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.7 on 2024-01-12 23:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bloggy', '0009_newsletter_delete_redirectrule'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newsletter',
+ name='send_to_all',
+ field=models.BooleanField(default=False, help_text='Send to all'),
+ ),
+ migrations.AddField(
+ model_name='newsletter',
+ name='send_to_users_only',
+ field=models.BooleanField(default=False, help_text='Send to registered users only'),
+ ),
+ ]
diff --git a/bloggy/models/__init__.py b/bloggy/models/__init__.py
index 53a3f97..a6165dd 100644
--- a/bloggy/models/__init__.py
+++ b/bloggy/models/__init__.py
@@ -7,6 +7,5 @@
from .post import Post
from .course import Course
from .quizzes import Quiz, QuizQuestion, QuizAnswer, UserQuizScore
-from .redirect_rule import RedirectRule
from .verification_token import VerificationToken
diff --git a/bloggy/models/categories.py b/bloggy/models/categories.py
index 5ea3204..907c5de 100644
--- a/bloggy/models/categories.py
+++ b/bloggy/models/categories.py
@@ -43,6 +43,9 @@ def save(self, *args, **kwargs):
def get_absolute_url(self):
return reverse('categories_single', args=[str(self.slug)])
+ def get_admin_url(self):
+ return reverse(f'admin:{self._meta.app_label}_{self._meta.model_name}_change', args=[self.id])
+
def thumbnail_tag(self):
if self.thumbnail_tag:
return format_html(f'')
diff --git a/bloggy/models/course.py b/bloggy/models/course.py
index 5550186..06d6bc8 100644
--- a/bloggy/models/course.py
+++ b/bloggy/models/course.py
@@ -46,6 +46,9 @@ class Meta:
def get_absolute_url(self):
return reverse("courses_single", kwargs={"slug": str(self.slug)})
+ def get_admin_url(self):
+ return reverse(f'admin:{self._meta.app_label}_{self._meta.model_name}_change', args=[self.id])
+
@property
def get_lessons(self):
return self.post_set.filter(publish_status="LIVE").order_by("display_order").all()
diff --git a/bloggy/models/newsletter.py b/bloggy/models/newsletter.py
new file mode 100644
index 0000000..3704e80
--- /dev/null
+++ b/bloggy/models/newsletter.py
@@ -0,0 +1,39 @@
+from django.db import models
+from django.db.models import TextField
+
+from bloggy.models.mixin.updatable import Updatable
+
+
+class Newsletter(Updatable):
+ title = models.CharField(max_length=300, help_text='Enter title')
+ url = models.CharField(max_length=150, help_text='Enter url', unique=True)
+ content = models.JSONField()
+ content_html = TextField(null=True, help_text='Newsletter content')
+ published_date = models.DateTimeField(null=True, blank=True)
+ publish_status = models.CharField(
+ max_length=20,
+ choices=[
+ ('DRAFT', 'DRAFT'),
+ ('LIVE', 'LIVE'),
+ ('DELETED', 'DELETED')
+ ],
+ default='DRAFT', blank=True, null=True,
+ help_text="Select publish status",
+ verbose_name="Publish status")
+
+ send_to_users_only = models.BooleanField(
+ default=False,
+ help_text="Send to registered users only"
+ )
+
+ send_to_all = models.BooleanField(
+ default=False,
+ help_text="Send to all users"
+ )
+
+ def __str__(self):
+ return str(self.title)
+
+ class Meta:
+ verbose_name = 'Newsletter'
+ verbose_name_plural = 'Newsletters'
diff --git a/bloggy/models/page.py b/bloggy/models/page.py
index 87143cb..6a5e664 100644
--- a/bloggy/models/page.py
+++ b/bloggy/models/page.py
@@ -1,5 +1,6 @@
from django.db import models
from django.db.models import TextField
+from django.urls import reverse
from bloggy.models.mixin.SeoAware import SeoAware
from bloggy.models.mixin.updatable import Updatable
@@ -15,6 +16,16 @@ class Page(Updatable, SeoAware):
"""
title = models.CharField(max_length=300, help_text='Enter title')
+ template_type = models.CharField(
+ max_length=20, choices=[
+ ('newsletter', 'Newsletter'),
+ ('naked', 'Naked'),
+ ('default', 'Default'),
+ ],
+ default='default', blank=True, null=True,
+ help_text="Template type",
+ verbose_name="Template type")
+
excerpt = models.CharField(
max_length=500,
help_text='Enter excerpt',
@@ -36,6 +47,14 @@ class Page(Updatable, SeoAware):
help_text="Select publish status",
verbose_name="Publish status")
+ def get_admin_url(self):
+ return reverse(
+ f'admin:{self._meta.app_label}_{self._meta.model_name}_change'
+ , args=[self.id])
+
+ def get_admin_url(self):
+ return reverse(f'admin:{self._meta.app_label}_{self._meta.model_name}_change', args=[self.id])
+
def __str__(self):
return str(self.title)
diff --git a/bloggy/models/post.py b/bloggy/models/post.py
index 235fdd6..dbb0834 100644
--- a/bloggy/models/post.py
+++ b/bloggy/models/post.py
@@ -23,7 +23,7 @@ class Post(Content):
max_length=20, choices=[
('beginner', 'Beginner'),
('intermediate', 'Intermediate'),
- ('advance', 'advance'),
+ ('advance', 'Advance'),
],
default='easy', blank=True, null=True,
help_text="Select difficulty",
diff --git a/bloggy/models/quizzes.py b/bloggy/models/quizzes.py
index 30da1c8..bc0ee7d 100644
--- a/bloggy/models/quizzes.py
+++ b/bloggy/models/quizzes.py
@@ -1,6 +1,7 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import TextField
+from django.urls import reverse
from hitcount.models import HitCount
from bloggy import settings
@@ -58,6 +59,9 @@ class Quiz(Content):
def get_questions_json(self):
return get_questions_json(self)
+ def get_admin_url(self):
+ return reverse(f'admin:{self._meta.app_label}_{self._meta.model_name}_change', args=[self.id])
+
def get_questions(self):
return self.quizquestion_set.all()
diff --git a/bloggy/models/redirect_rule.py b/bloggy/models/redirect_rule.py
deleted file mode 100644
index 102f060..0000000
--- a/bloggy/models/redirect_rule.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.db import models
-
-from bloggy.models.mixin.updatable import Updatable
-
-
-class RedirectRule(Updatable):
- source = models.CharField(max_length=300, help_text='Enter from url')
- destination = models.CharField(max_length=300, help_text='Enter to url')
- status_code = models.IntegerField(
- default='standard', blank=True, null=True,
- choices=[
- (301, '301 Moved Permanently'),
- (307, '307 Temporary Redirect'),
- ],
- help_text="Redirect type",
- verbose_name="Redirect type")
-
- note = models.CharField(
- max_length=500,
- help_text='Enter note',
- null=True,
- blank=True
- )
-
- def __str__(self):
- return f"{self.status_code}::{self.source}"
-
-
-class Meta:
- verbose_name = "Redirect"
- verbose_name_plural = "Redirects"
diff --git a/bloggy/models/user.py b/bloggy/models/user.py
index 77133f4..2d3357d 100644
--- a/bloggy/models/user.py
+++ b/bloggy/models/user.py
@@ -66,6 +66,8 @@ class User(AbstractBaseUser, ResizeImageMixin, PermissionsMixin):
youtube = models.CharField(max_length=100, null=True, blank=True)
github = models.CharField(max_length=100, null=True, blank=True)
bio = models.TextField(max_length=250, null=True, blank=True)
+ receive_news_updates = models.BooleanField(default=True)
+ receive_new_content = models.BooleanField(default=True)
class Meta:
db_table = "bloggy_user"
@@ -81,9 +83,11 @@ def get_bookmarks_count(self):
def get_full_name_or_username(self):
if self.name:
return self.name
-
return self.username
+ def __str__(self):
+ return self.get_full_name_or_username()
+
def get_full_name(self):
full_name = f"{self.name}"
return full_name.strip()
diff --git a/bloggy/services/email_service.py b/bloggy/services/email_service.py
index a0a556f..2531979 100644
--- a/bloggy/services/email_service.py
+++ b/bloggy/services/email_service.py
@@ -1,55 +1,36 @@
from django.core.mail import send_mail
from django.template.loader import render_to_string
-from django.urls import reverse
from bloggy import settings
-def send_custom_email(subject, recipients, template, args, from_email=settings.DEFAULT_FROM_EMAIL):
+def send_html_email(subject, to_email, template, args, from_email=settings.DEFAULT_FROM_EMAIL):
email_body = render_to_string(template, args)
send_mail(
subject,
email_body,
from_email,
- recipients,
+ to_email,
fail_silently=False,
html_message=email_body
)
-def send_newsletter_verification_token(request, email, uuid, token):
- subject = f'Confirm to {settings.SITE_TITLE} newsletter'
-
- args = {
- "email_subject": subject,
- "app_name": settings.SITE_TITLE,
- "verification_link": request.build_absolute_uri(reverse("newsletter_verification", args=[uuid, token]))
- }
-
- send_custom_email(subject, [email], "email/newsletter_verification_token.html", args)
-
-
-def email_verification_token(request, new_user, token):
- subject = f"{settings.SITE_TITLE} confirmation code: {token.code}"
- args = {
- "email_subject": subject,
- "verification_code": token.code,
- "app_name": settings.SITE_TITLE,
- "verification_link": request.build_absolute_uri(reverse("otp_verification", args=[token.uuid]))
- }
- send_custom_email(subject, [new_user.email], "email/login_code_email.html", args)
-
-
-def email_registration_token(request, new_user, verification_token):
- subject = f'Welcome to {settings.SITE_TITLE}!'
- args = {
- "email_subject": subject,
- "user_name": new_user.name,
- "app_name": settings.SITE_TITLE,
- "verification_link": request.build_absolute_uri(reverse("activate_account", args=[
- verification_token.uuid,
- verification_token.token
- ]))
- }
+def send_plain_email(subject, to_email, message_content, from_email=settings.DEFAULT_FROM_EMAIL):
+ send_mail(
+ subject,
+ message_content,
+ from_email,
+ to_email,
+ fail_silently=False,
+ )
- send_custom_email(subject, [new_user.email], "email/acc_active_email.html", args)
+# def email_verification_token(request, new_user, token):
+# subject = f"{settings.SITE_TITLE} confirmation code: {token.code}"
+# args = {
+# "email_subject": subject,
+# "verification_code": token.code,
+# "app_name": settings.SITE_TITLE,
+# "verification_link": request.build_absolute_uri(reverse("otp_verification", args=[token.uuid]))
+# }
+# send_html_email(subject, [new_user.email], "email/login_code_email.html", args)
diff --git a/bloggy/services/post_service.py b/bloggy/services/post_service.py
index e3ec89c..876dfdd 100644
--- a/bloggy/services/post_service.py
+++ b/bloggy/services/post_service.py
@@ -2,6 +2,7 @@
from bloggy.models import Post, Quiz
from bloggy.utils.string_utils import StringUtils
+from bs4 import BeautifulSoup
DEFAULT_PAGE_SIZE = 20
@@ -64,3 +65,17 @@ def set_seo_settings(post, context):
if post.thumbnail:
context['meta_image'] = post.thumbnail.url
+
+
+def cleanse_html(html_string):
+ soup = BeautifulSoup(html_string, 'html.parser')
+ for tag in soup.find_all(True, {'style': True}):
+ del tag['style']
+
+ # Find and remove empty
, , and tags
+ for tag in soup.find_all(['p', 'a', 'span']):
+ if not tag and (not tag.contents or (len(tag.contents) == 1 and not tag.contents[0].strip())):
+ tag.extract()
+
+ # Get the modified HTML string
+ return str(soup)
diff --git a/bloggy/services/token_service.py b/bloggy/services/token_service.py
index 347f98a..ba08e71 100644
--- a/bloggy/services/token_service.py
+++ b/bloggy/services/token_service.py
@@ -11,7 +11,9 @@
def create_token(user, token_type):
"""
- Generate token:: Token and uuid will be generated automatically
+ Generate token:: Token and uuid will be generated automatically,
+
+ if expired, it will automatically renew new token.
"""
token = VerificationToken.objects.filter(user=user, token_type=token_type).first()
if token:
diff --git a/bloggy/settings.py b/bloggy/settings.py
index 900ec43..a0f37c0 100644
--- a/bloggy/settings.py
+++ b/bloggy/settings.py
@@ -57,9 +57,8 @@
'mail_templated', # Used for templated email https://github.com/artemrizhov/django-mail-templated
'storages',
'debug_toolbar', # dev only
-
'hitcount',
- 'colorfield'
+ 'colorfield',
]
MIDDLEWARE = [
@@ -83,7 +82,6 @@
# Social login
# 'social_django.middleware.SocialAuthExceptionMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
- 'bloggy.middleware.redirect.RedirectMiddleware', # new articles mismatch url redirect
]
ROOT_URLCONF = 'bloggy.urls'
@@ -364,4 +362,4 @@ def get_post_types():
"propagate": False,
},
},
-}
\ No newline at end of file
+}
diff --git a/bloggy/templates/.DS_Store b/bloggy/templates/.DS_Store
new file mode 100644
index 0000000..441bf42
Binary files /dev/null and b/bloggy/templates/.DS_Store differ
diff --git a/bloggy/templates/auth/login.html b/bloggy/templates/auth/login.html
index 2ae8608..8d08526 100644
--- a/bloggy/templates/auth/login.html
+++ b/bloggy/templates/auth/login.html
@@ -23,7 +23,7 @@ {{ form.password.errors }} To reset your account password, enter your registered email address and we will send you the
+ password reset instructions on your email. Please enter your new password. A strong password helps to prevent unauthorized access to your account! Please check your inbox and follow the instruction to reset your password.Sign in
Forgot password
+ Change password
+ Reset password
+ Create your free account!
{{ form.password2|add_class:"form-control" }}
{{ form.password2.errors }}
{% for error in form.non_field_errors %}
Create your free account!
By continuing, you indicate that you have read and agree to stacktips.com's Terms of Service and Privacy Policy.
-
-
-
-
-
|
-
-
-
-
-
-
|
-
-
-
-
-
-
|
-
-
-
-
-
-
|
-
diff --git a/bloggy/templates/base-with-header.html b/bloggy/templates/base-with-header.html
index bf4ce6d..1c96e00 100644
--- a/bloggy/templates/base-with-header.html
+++ b/bloggy/templates/base-with-header.html
@@ -21,14 +21,8 @@
{% include "partials/header.html" %}
-
- - - - - - - - - - - - -
-