Add weekly new products email and related features
Introduces a weekly summary email for newly added products, including a new email template and Celery periodic task. Adds 'include_in_week_summary_email' to Product and 'newsletter' to CustomUser. Provides an admin endpoint to manually trigger the weekly email, updates Celery Beat schedule, and adds email templates for verification and password reset.
This commit is contained in:
@@ -63,6 +63,7 @@ class CustomUser(SoftDeleteModel, AbstractUser):
|
|||||||
email_verification_token = models.CharField(max_length=128, null=True, blank=True, db_index=True)
|
email_verification_token = models.CharField(max_length=128, null=True, blank=True, db_index=True)
|
||||||
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
|
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
newsletter = models.BooleanField(default=True)
|
||||||
|
|
||||||
#misc
|
#misc
|
||||||
gdpr = models.BooleanField(default=False)
|
gdpr = models.BooleanField(default=False)
|
||||||
|
|||||||
@@ -405,4 +405,7 @@ class PasswordResetConfirmView(APIView):
|
|||||||
user.set_password(serializer.validated_data['password'])
|
user.set_password(serializer.validated_data['password'])
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"detail": "Heslo bylo úspěšně změněno."})
|
return Response({"detail": "Heslo bylo úspěšně změněno."})
|
||||||
return Response(serializer.errors, status=400)
|
return Response(serializer.errors, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
from venv import create
|
||||||
from account.tasks import send_email_with_context
|
from account.tasks import send_email_with_context
|
||||||
from configuration.models import SiteConfiguration
|
from configuration.models import SiteConfiguration
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
|
||||||
|
from commerce.models import Product
|
||||||
|
import datetime
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_contact_me_email_task(client_email, message_content):
|
def send_contact_me_email_task(client_email, message_content):
|
||||||
context = {
|
context = {
|
||||||
@@ -18,13 +22,28 @@ def send_contact_me_email_task(client_email, message_content):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_newly_added_items_to_store_email_task_last_week(item_id):
|
@shared_task
|
||||||
|
def send_newly_added_items_to_store_email_task_last_week():
|
||||||
|
last_week_date = datetime.datetime.now() - datetime.timedelta(days=7)
|
||||||
|
|
||||||
|
"""
|
||||||
|
__lte -> Less than or equal
|
||||||
|
__gte -> Greater than or equal
|
||||||
|
__lt -> Less than
|
||||||
|
__gt -> Greater than
|
||||||
|
"""
|
||||||
|
|
||||||
|
products_of_week = Product.objects.filter(
|
||||||
|
include_in_week_summary_email=True,
|
||||||
|
created_at__gte=last_week_date
|
||||||
|
)
|
||||||
|
|
||||||
send_email_with_context(
|
send_email_with_context(
|
||||||
recipients=SiteConfiguration.get_solo().contact_email,
|
recipients=SiteConfiguration.get_solo().contact_email,
|
||||||
subject="Nový produkt přidán do obchodu",
|
subject="Nový produkt přidán do obchodu",
|
||||||
template_path="email/new_item_added.html",
|
template_path="email/advertisement/commerce/new_items_added_this_week.html",
|
||||||
context={
|
context={
|
||||||
"item": item,
|
"products_of_week": products_of_week,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<style>
|
||||||
|
.summary {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.product-item {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding: 15px 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.product-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.product-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.product-price {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #007bff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.product-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.product-date {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.no-products {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 30px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h2 style="color: #007bff; margin: 0 0 20px 0;">🆕 Nové produkty v obchodě</h2>
|
||||||
|
<p style="margin: 0 0 20px 0;">Týdenní přehled nově přidaných produktů</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h3 style="margin: 0 0 10px 0;">📊 Celkem nových produktů: {{ products_of_week|length }}</h3>
|
||||||
|
<p style="margin: 0;">Přehled produktů přidaných za posledních 7 dní</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products_of_week %}
|
||||||
|
{% for product in products_of_week %}
|
||||||
|
<div class="product-item">
|
||||||
|
<div class="product-name">{{ product.name }}</div>
|
||||||
|
|
||||||
|
{% if product.price %}
|
||||||
|
<div class="product-price">
|
||||||
|
{{ product.price|floatformat:0 }} {{ product.currency|default:"Kč" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.short_description %}
|
||||||
|
<div class="product-description">
|
||||||
|
{{ product.short_description|truncatewords:20 }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-date">
|
||||||
|
Přidáno: {{ product.created_at|date:"d.m.Y H:i" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="no-products">
|
||||||
|
<h3 style="margin: 0 0 15px 0;">🤷♂️ Žádné nové produkty</h3>
|
||||||
|
<p style="margin: 0;">Za posledních 7 dní nebyly přidány žádné nové produkty, které by měly být zahrnuty do týdenního přehledu.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .views import ContactMePublicView, ContactMeAdminViewSet
|
from .views import ContactMePublicView, ContactMeAdminViewSet, trigger_weekly_email
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r"contact-messages", ContactMeAdminViewSet, basename="contactme")
|
router.register(r"contact-messages", ContactMeAdminViewSet, basename="contactme")
|
||||||
@@ -12,4 +12,5 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Admin endpoints
|
# Admin endpoints
|
||||||
path("", include(router.urls)),
|
path("", include(router.urls)),
|
||||||
|
path("trigger-weekly-email/", trigger_weekly_email, name="trigger-weekly-email"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||||
|
|
||||||
from .models import ContactMe
|
from .models import ContactMe
|
||||||
from .serializer import ContactMeSerializer
|
from .serializer import ContactMeSerializer
|
||||||
from .tasks import send_contact_me_email_task
|
from .tasks import send_contact_me_email_task, send_newly_added_items_to_store_email_task_last_week
|
||||||
|
|
||||||
|
|
||||||
@extend_schema(tags=["advertisement", "public"])
|
@extend_schema(tags=["advertisement", "public"])
|
||||||
@@ -54,3 +55,32 @@ class ContactMeAdminViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ContactMeSerializer
|
serializer_class = ContactMeSerializer
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["advertisement"],
|
||||||
|
summary="Manually trigger weekly new items email",
|
||||||
|
description="Triggers the weekly email task that sends a summary of newly added products from the last week. Only accessible by admin users.",
|
||||||
|
methods=["POST"]
|
||||||
|
)
|
||||||
|
@api_view(['POST'])
|
||||||
|
@permission_classes([IsAdminUser])
|
||||||
|
def trigger_weekly_email(request):
|
||||||
|
"""
|
||||||
|
Manually trigger the weekly new items email task.
|
||||||
|
Only accessible by admin users.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Trigger the task asynchronously
|
||||||
|
task = send_newly_added_items_to_store_email_task_last_week.delay()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Weekly email task triggered successfully',
|
||||||
|
'task_id': task.id
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Failed to trigger weekly email task: {str(e)}'
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class Product(models.Model):
|
|||||||
"Carrier", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_products"
|
"Carrier", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_products"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
include_in_week_summary_email = models.BooleanField(default=False)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
78
backend/templates/email/email_verification.html
Normal file
78
backend/templates/email/email_verification.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<style>
|
||||||
|
.verification-container {
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.verification-title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.verification-text {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
.verification-button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white !important;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.verification-info {
|
||||||
|
color: #0c5460;
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.welcome-note {
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="verification-container">
|
||||||
|
<h1 class="verification-title">✉️ Ověření e-mailové adresy</h1>
|
||||||
|
|
||||||
|
<p class="verification-text">
|
||||||
|
Vítejte {{ user.first_name|default:user.username }}!<br><br>
|
||||||
|
Děkujeme za registraci. Pro dokončení vytvoření účtu je nutné
|
||||||
|
ověřit vaši e-mailovou adresu kliknutím na tlačítko níže.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="{{ action_url }}" class="verification-button">{{ cta_label }}</a>
|
||||||
|
|
||||||
|
<div class="welcome-note">
|
||||||
|
<strong>🎉 Těšíme se na vás!</strong><br>
|
||||||
|
Po ověření e-mailu budete moci využívat všechny funkce naší platformy
|
||||||
|
a začít nakupovat nebo prodávat.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-info">
|
||||||
|
<strong>ℹ️ Co dělat, když tlačítko nefunguje?</strong><br>
|
||||||
|
Zkopírujte a vložte následující odkaz do adresního řádku prohlížeče:
|
||||||
|
<br><br>
|
||||||
|
<span style="word-break: break-all; font-family: monospace; font-size: 12px;">{{ action_url }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="verification-text" style="font-size: 14px; color: #999;">
|
||||||
|
Odkaz pro ověření je platný po omezenou dobu.
|
||||||
|
Pokud jste se neregistrovali na našich stránkách, ignorujte tento e-mail.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
75
backend/templates/email/password_reset.html
Normal file
75
backend/templates/email/password_reset.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<style>
|
||||||
|
.reset-container {
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.reset-title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.reset-text {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
.reset-button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white !important;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.reset-warning {
|
||||||
|
color: #856404;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.security-note {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="reset-container">
|
||||||
|
<h1 class="reset-title">🔐 Obnova hesla</h1>
|
||||||
|
|
||||||
|
<p class="reset-text">
|
||||||
|
Dobrý den {{ user.first_name|default:user.username }},<br><br>
|
||||||
|
Obdrželi jsme požadavek na obnovení hesla k vašemu účtu.
|
||||||
|
Klikněte na tlačítko níže pro vytvoření nového hesla.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="{{ action_url }}" class="reset-button">{{ cta_label }}</a>
|
||||||
|
|
||||||
|
<div class="reset-warning">
|
||||||
|
<strong>⚠️ Bezpečnostní upozornění:</strong><br>
|
||||||
|
Pokud jste o obnovu hesla nepožádali, ignorujte tento e-mail.
|
||||||
|
Váše heslo zůstane nezměněno.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="reset-text">
|
||||||
|
Odkaz pro obnovu hesla je platný pouze po omezenou dobu.
|
||||||
|
Pokud odkaz nefunguje, zkopírujte a vložte následující adresu do prohlížeče:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="word-break: break-all; color: #007bff; font-size: 14px;">
|
||||||
|
{{ action_url }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="security-note">
|
||||||
|
Tento odkaz je určen pouze pro vás a nelze ho sdílet s ostatními.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -19,6 +19,7 @@ from django.db import OperationalError, connections
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from celery.schedules import crontab
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv() # Pouze načte proměnné lokálně, pokud nejsou dostupné
|
load_dotenv() # Pouze načte proměnné lokálně, pokud nejsou dostupné
|
||||||
|
|
||||||
@@ -508,6 +509,14 @@ CELERY_TASK_SERIALIZER = os.getenv("CELERY_TASK_SERIALIZER", "json")
|
|||||||
CELERY_RESULT_SERIALIZER = os.getenv("CELERY_RESULT_SERIALIZER", "json")
|
CELERY_RESULT_SERIALIZER = os.getenv("CELERY_RESULT_SERIALIZER", "json")
|
||||||
CELERY_TIMEZONE = os.getenv("CELERY_TIMEZONE", TIME_ZONE)
|
CELERY_TIMEZONE = os.getenv("CELERY_TIMEZONE", TIME_ZONE)
|
||||||
CELERY_BEAT_SCHEDULER = os.getenv("CELERY_BEAT_SCHEDULER")
|
CELERY_BEAT_SCHEDULER = os.getenv("CELERY_BEAT_SCHEDULER")
|
||||||
|
|
||||||
|
# Celery Beat Schedule for periodic tasks
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
'send-weekly-new-items-email': {
|
||||||
|
'task': 'advertisement.tasks.send_newly_added_items_to_store_email_task_last_week',
|
||||||
|
'schedule': crontab(hour=9, minute=0, day_of_week=1), # Every Monday at 9:00 AM
|
||||||
|
},
|
||||||
|
}
|
||||||
#-------------------------------------END CELERY 📅------------------------------------
|
#-------------------------------------END CELERY 📅------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user