converter

This commit is contained in:
2025-10-30 01:58:28 +01:00
parent dd9d076bd2
commit 8dd4f6e731
23 changed files with 1142 additions and 1286 deletions

View File

@@ -1,8 +1,9 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager, Group, Permission
from django.core.validators import RegexValidator, MinLengthValidator, MaxValueValidator, MinValueValidator
from django.core.validators import RegexValidator
from django.utils.crypto import get_random_string
from django.conf import settings
from django.db import models
from django.utils import timezone
@@ -61,6 +62,10 @@ class CustomUser(SoftDeleteModel, AbstractUser):
email_verified = models.BooleanField(default=False)
email = models.EmailField(unique=True, db_index=True)
# + fields for email verification flow
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)
gdpr = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
@@ -124,5 +129,32 @@ class CustomUser(SoftDeleteModel, AbstractUser):
self.groups.set([group])
return super().save(*args, **kwargs)
def generate_email_verification_token(self, length: int = 48, save: bool = True) -> str:
token = get_random_string(length=length)
self.email_verification_token = token
self.email_verification_sent_at = timezone.now()
if save:
self.save(update_fields=["email_verification_token", "email_verification_sent_at"])
return token
def verify_email_token(self, token: str, max_age_hours: int = 48, save: bool = True) -> bool:
if not token or not self.email_verification_token:
return False
# optional expiry check
if self.email_verification_sent_at:
age = timezone.now() - self.email_verification_sent_at
if age > timedelta(hours=max_age_hours):
return False
if token != self.email_verification_token:
return False
if not self.email_verified:
self.email_verified = True
# clear token after success
self.email_verification_token = None
self.email_verification_sent_at = None
if save:
self.save(update_fields=["email_verified", "email_verification_token", "email_verification_sent_at"])
return True

View File

@@ -10,76 +10,115 @@ from .models import CustomUser
logger = get_task_logger(__name__)
@shared_task
def send_password_reset_email_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
error_msg = f"Task send_password_reset_email has failed. Invalid User ID was sent."
logger.error(error_msg)
raise Exception(error_msg)
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = password_reset_token.make_token(user)
reset_url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
html_message = render_to_string(
'emails/password_reset.html',
{'reset_url': reset_url}
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message, "\nKONEC OBSAHU")
send_email_with_context(
recipients=user.email,
subject="Obnova hesla",
message=None,
html_message=html_message
)
def send_email_with_context(recipients, subject, message=None, template_name=None, html_template_name=None, context=None):
"""
General function to send emails with a specific context.
Supports rendering plain text and HTML templates.
Converts `user` in context to a plain dict to avoid template access to the model.
"""
if isinstance(recipients, str):
recipients = [recipients]
# Only email verification for user registration
html_message = None
if template_name or html_template_name:
# Best effort to resolve both templates if only one provided
if not template_name and html_template_name:
template_name = html_template_name.replace(".html", ".txt")
if not html_template_name and template_name:
html_template_name = template_name.replace(".txt", ".html")
ctx = dict(context or {})
# Sanitize user if someone passes the model by mistake
if "user" in ctx and not isinstance(ctx["user"], dict):
try:
ctx["user"] = _build_user_template_ctx(ctx["user"])
except Exception:
ctx["user"] = {}
message = render_to_string(template_name, ctx)
html_message = render_to_string(html_template_name, ctx)
try:
send_mail(
subject=subject,
message=message or "",
from_email=None,
recipient_list=recipients,
fail_silently=False,
html_message=html_message,
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug(f"\nEMAIL OBSAH:\n{message}\nKONEC OBSAHU")
return True
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
return False
def _build_user_template_ctx(user: CustomUser) -> dict:
"""
Return a plain dict for templates instead of passing the DB model.
Provides aliases to avoid template errors (firstname vs first_name).
Adds a backward-compatible key 'get_full_name' for templates using `user.get_full_name`.
"""
first_name = getattr(user, "first_name", "") or ""
last_name = getattr(user, "last_name", "") or ""
full_name = f"{first_name} {last_name}".strip()
return {
"id": user.pk,
"email": getattr(user, "email", "") or "",
"first_name": first_name,
"firstname": first_name, # alias for templates using `firstname`
"last_name": last_name,
"lastname": last_name, # alias for templates using `lastname`
"full_name": full_name,
"get_full_name": full_name, # compatibility for templates using method-style access
}
#----------------------------------------------------------------------------------------------------
# This function sends an email to the user for email verification after registration.
@shared_task
def send_email_verification_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
error_msg = f"Task send_email_verification_task has failed. Invalid User ID was sent."
logger.error(error_msg)
raise Exception(error_msg)
logger.info(f"Task send_email_verification has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = account_activation_token.make_token(user)
verification_url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
html_message = render_to_string(
'emails/email_verification.html',
{'verification_url': verification_url}
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message, "\nKONEC OBSAHU")
# {changed} generate and store a per-user token
token = user.generate_email_verification_token()
verify_url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
context = {
"user": _build_user_template_ctx(user),
"action_url": verify_url,
"frontend_url": settings.FRONTEND_URL,
"cta_label": "Ověřit email",
}
send_email_with_context(
recipients=user.email,
subject="Ověření e-mailu",
message=None,
html_message=html_message
subject="Ověření emailu",
template_name="email/email_verification.txt",
html_template_name="email/email_verification.html",
context=context,
)
def send_email_with_context(recipients, subject, message=None, html_message=None):
"""
General function to send emails with a specific context.
"""
if isinstance(recipients, str):
recipients = [recipients]
try:
send_mail(
subject=subject,
message=message if message else '',
from_email=None,
recipient_list=recipients,
fail_silently=False,
html_message=html_message
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug("\nEMAIL OBSAH:\n", html_message if html_message else message, "\nKONEC OBSAHU")
return True
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
return False
@shared_task
def send_email_test_task(email):
context = {
"action_url": settings.FRONTEND_URL,
"frontend_url": settings.FRONTEND_URL,
"cta_label": "Otevřít aplikaci",
}
send_email_with_context(
recipients=email,
subject="Testovací email",
template_name="email/test.txt",
html_template_name="email/test.html",
context=context,
)

View File

@@ -1,128 +0,0 @@
<table style="background-color:#031D44; font-family:'Exo', Arial, sans-serif; width:100%;" align="center" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding:20px;">
<table border="0" cellspacing="20" cellpadding="0" style="max-width:600px; width:100%;" align="center">
<!-- Nadpis -->
<tr>
<td align="center" style="padding:20px; color:#ffffff; font-size:30px; font-weight:bold; border-radius:8px; text-decoration:underline;">
Nabídka tvorby webových stránek
</td>
</tr>
<tr>
<td style="color:#CAF0F8; border-radius:8px; font-size:18px; line-height:1.6;">
<p style="margin:0;">
Jsme <strong>malý tým</strong>, který se snaží prorazit a přinášet moderní řešení za férové ceny.
Nabízíme také <strong>levný hosting</strong> a <strong>SSL zabezpečení zdarma</strong>.
</p>
<p style="margin:10px 0 0;">
Dbáme na <strong>bezpečnost</strong>, používáme <strong>moderní frameworky</strong>
a rozhodně nejsme součástí „gerontosaurů“ <strong>PHP nepoužíváme</strong>.
</p>
</td>
</tr>
<!-- Balíčky Nadpis -->
<tr>
<td align="center" style="padding-top:30px; color:#ffffff; font-size:28px; font-weight:bold; text-decoration:underline;">
Balíčky
</td>
</tr>
<!-- Balíčky (jednotlivé) -->
<tr>
<td style="padding:20px; background:#3a8bb7; color:#CAF0F8; border-radius:15px; line-height:1.6; font-size:16px; width:100%;">
<h2 style="margin:0; color:#CAF0F8;">BASIC</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Jednoduchá prezentační webová stránka</li>
<li>Moderní a responzivní design (PC, tablety, mobily)</li>
<li>Max. počet stránek: 5</li>
<li>Seřízení vlastní domény, a k tomu<span style="text-decoration: underline;">SSL certifikát zdarma</span></li>
</ul>
<p style="font-size:16px; background-color:#24719f; padding:12px; color:#ffffff; font-weight:bold; margin:0; border-radius:8px;">
Cena: 5 000 Kč (jednorázově) + 100 Kč / měsíc
</p>
</td>
</tr>
<tr>
<td style="padding:20px; background:#70A288; color:#ffffff; border-radius:15px; line-height:1.6; font-size:16px; width:100%;">
<h2 style="margin:0; color:#ffffff;">STANDARD</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Vše z balíčku BASIC</li>
<li>Kontaktní formulář (přijde vám poptávka na e-mail)</li>
<li>Priorita při vývoji (cca 2 týdny)</li>
<li>Základní SEO</li>
<li>Max. počet stránek: 10</li>
</ul>
<p style="font-size:16px; background-color:#508845; padding:12px; color:#ffffff; font-weight:bold; margin:0; border-radius:8px;">
Cena: 7 500 Kč (jednorázově) + 250 Kč / měsíc
</p>
</td>
</tr>
<tr>
<td style="padding:20px; background:#87a9da; color:#031D44; border-radius:15px; line-height:1.6; font-size:16px; width:100%;">
<h2 style="margin:0; color:#031D44;">PREMIUM</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Vše z balíčku STANDARD</li>
<li>Vaše firma na Google Maps díky plně nastavenému Google Business Profile</li>
<li>Pokročilé SEO</li>
<li>Měsíční report návštěvnosti</li>
<li>Možnost drobných úprav</li>
<li>Neomezený počet stránek</li>
</ul>
<p style="font-size:16px; background-color:#4c7bbd; padding:12px; color:#ffffff; font-weight:bold; margin:0; border-radius:8px;">
Cena: od 9 500 Kč (jednorázově) + 400 Kč / měsíc
</p>
</td>
</tr>
<tr>
<td style="padding:20px; background:#04395E; color:#CAF0F8; border-radius:15px; line-height:1.6; font-size:16px; width:100%;">
<h2 style="margin:0; color:#CAF0F8;">CUSTOM</h2>
<ul style="padding-left:20px; margin:10px 0;">
<li>Kompletně na míru</li>
<li>Možnost e-shopu a rezervačních systémů</li>
<li>Integrace API a platební brány</li>
<li>Pokročilé SEO a marketing</li>
</ul>
<p style="font-size:16px; background-color:#216085; padding:12px; color:#ffffff; font-weight:bold; margin:0; border-radius:8px;">
Cena: dohodou
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Footer -->
<table style="width:100%; background-color:#031D44; font-family:'Exo', Arial, sans-serif;" align="center" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding:20px; color:#CAF0F8;">
<p style="margin:0; font-size:20px; font-weight:bold;">Máte zájem o některý z balíčků?</p>
<p>Stačí odpovědět na tento e-mail nebo mě kontaktovat:</p>
<p>
<a href="mailto:brunovontor@gmail.com" style="color:#CAF0F8; text-decoration:underline;">brunovontor@gmail.com</a><br>
<a href="tel:+420605512624" style="color:#CAF0F8; text-decoration:underline;">+420 605 512 624</a><br>
<a href="https://vontor.cz" style="color:#CAF0F8; text-decoration:underline;">vontor.cz</a>
</p>
</td>
</tr>
</table>
<!-- Responsivní CSS -->
<style>
@media only screen and (max-width: 600px) {
table[class="responsive-table"] {
width: 100% !important;
}
td {
font-size: 16px !important;
padding: 10px !important;
}
h2 {
font-size: 20px !important;
}
}
</style>

View File

@@ -1,19 +1,46 @@
<!DOCTYPE html>
<!doctype html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Ověření e-mailu</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-body">
<h2 class="card-title">Ověření e-mailu</h2>
<p class="card-text">Ověřte svůj e-mail kliknutím na odkaz níže:</p>
<a href="{{ verification_url }}" class="btn btn-success">Ověřit e-mail</a>
</div>
</div>
</div>
</body>
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Ověření emailu
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}
<p style="margin:0 0 12px 0;">Dobrý den{% if name %} {{ name }}{% endif %},</p>
{% endwith %}
<p style="margin:0 0 16px 0;">Děkujeme za registraci. Prosíme, ověřte svou emailovou adresu kliknutím na tlačítko níže.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,7 @@
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}Dobrý den{% if name %} {{ name }}{% endif %},{% endwith %}
Děkujeme za registraci. Prosíme, ověřte svou emailovou adresu kliknutím na následující odkaz:
{{ action_url }}
Pokud jste účet nevytvořili vy, tento email ignorujte.

View File

@@ -1,19 +1,46 @@
<!DOCTYPE html>
<!doctype html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Obnova hesla</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-body">
<h2 class="card-title">Obnova hesla</h2>
<p class="card-text">Pro obnovu hesla klikněte na následující odkaz:</p>
<a href="{{ reset_url }}" class="btn btn-primary">Obnovit heslo</a>
</div>
</div>
</div>
</body>
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Obnova hesla
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}
<p style="margin:0 0 12px 0;">Dobrý den{% if name %} {{ name }}{% endif %},</p>
{% endwith %}
<p style="margin:0 0 12px 0;">Obdrželi jste tento email, protože byla požádána obnova hesla k vašemu účtu. Pokud jste o změnu nepožádali, tento email ignorujte.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,7 @@
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}Dobrý den{% if name %} {{ name }}{% endif %},{% endwith %}
Obdrželi jste tento email, protože byla požádána obnova hesla k vašemu účtu.
Pokud jste o změnu nepožádali, tento email ignorujte.
Pro nastavení nového hesla použijte tento odkaz:
{{ action_url }}

View File

@@ -0,0 +1,44 @@
<!doctype html>
<html lang="cs">
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Testovací email
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
<p style="margin:0 0 12px 0;">Dobrý den,</p>
<p style="margin:0 0 16px 0;">Toto je testovací email z aplikace etržnice.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,6 @@
Dobrý den,
Toto je testovací email z aplikace etržnice.
Odkaz na aplikaci:
{{ action_url }}