diff --git a/backend/civil_society_vote/common/__init__.py b/backend/civil_society_vote/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/civil_society_vote/common/messaging.py b/backend/civil_society_vote/common/messaging.py new file mode 100644 index 00000000..1432914e --- /dev/null +++ b/backend/civil_society_vote/common/messaging.py @@ -0,0 +1,74 @@ +import logging +from typing import Dict, List, Optional + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template, render_to_string +from django.utils.translation import gettext_lazy as _ +from django_q.tasks import async_task + +logger = logging.getLogger(__name__) + + +def send_email( + subject: str, + to_emails: List[str], + text_template: str, + html_template: str, + context: Dict, + from_email: Optional[str] = None, +): + if settings.EMAIL_SEND_METHOD == "async": + async_send_email(subject, to_emails, text_template, html_template, context, from_email) + elif settings.EMAIL_SEND_METHOD == "sync": + send_emails(to_emails, subject, text_template, html_template, context, from_email) + else: + raise ValueError(_("Invalid email send method. Must be 'async' or 'sync'.")) + + +def async_send_email( + subject: str, + to_emails: List[str], + text_template: str, + html_template: str, + html_context: Dict, + from_email: Optional[str] = None, +): + logger.info(f"Asynchronously sending {len(to_emails)} emails with subject: {subject}.") + + async_task( + send_emails, + to_emails, + subject, + text_template, + html_template, + html_context, + from_email, + ) + + +def send_emails( + user_emails: List[str], + subject: str, + text_template: str, + html_template: str, + html_context: Dict, + from_email: Optional[str] = None, +): + logger.info(f"Sending emails to {len(user_emails)} users.") + + for email in user_emails: + text_body = render_to_string(text_template, context=html_context) + + html = get_template(html_template) + html_content = html.render(html_context) + + if not from_email: + from_email = ( + settings.DEFAULT_FROM_EMAIL if hasattr(settings, "DEFAULT_FROM_EMAIL") else settings.NO_REPLY_EMAIL + ) + + msg = EmailMultiAlternatives(subject, text_body, from_email, [email]) + msg.attach_alternative(html_content, "text/html") + + msg.send(fail_silently=False) diff --git a/backend/civil_society_vote/settings.py b/backend/civil_society_vote/settings.py index 0451d5de..651be935 100644 --- a/backend/civil_society_vote/settings.py +++ b/backend/civil_society_vote/settings.py @@ -168,6 +168,7 @@ "crispy_forms", "django_crispy_bulma", "storages", + "django_q", "django_recaptcha", "file_resubmit", "rangefilter", diff --git a/backend/hub/admin.py b/backend/hub/admin.py index 49038c29..f9c1b05a 100644 --- a/backend/hub/admin.py +++ b/backend/hub/admin.py @@ -1,7 +1,6 @@ import csv import io -from accounts.models import User from django.contrib import admin, messages from django.contrib.admin.filters import AllValuesFieldListFilter from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME @@ -9,26 +8,25 @@ from django.contrib.auth.models import Group from django.contrib.sites.shortcuts import get_current_site from django.shortcuts import redirect, render -from django.template import Context from django.urls import path, reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from impersonate.admin import UserAdminImpersonateMixin -from hub import utils +from accounts.models import User +from civil_society_vote.common.messaging import send_email from hub.forms import ImportCitiesForm from hub.models import ( + BlogPost, COMMITTEE_GROUP, COUNTIES, COUNTY_RESIDENCE, - BlogPost, Candidate, CandidateConfirmation, CandidateSupporter, CandidateVote, City, Domain, - EmailTemplate, FLAG_CHOICES, FeatureFlag, Organization, @@ -78,7 +76,7 @@ def get_groups(self, obj=None): get_groups.short_description = _("groups") -# NOTE: This is needed in order for impersonate to work +# NOTE: This is needed in order for impersonation to work # admin.site.unregister(User) admin.site.register(User, ImpersonableUserAdmin) @@ -223,17 +221,16 @@ def send_confirm_email_to_committee(request, candidate, to_email): confirmation_link_path = reverse("candidate-status-confirm", args=(candidate.pk,)) confirmation_link = f"{protocol}://{current_site.domain}{confirmation_link_path}" - utils.send_email( - template="confirmation", - context=Context( - { - "candidate": candidate.name, - "status": Candidate.STATUS[candidate.status], - "confirmation_link": confirmation_link, - } - ), + send_email( subject=f"[VOTONG] Confirmare candidatura: {candidate.name}", - to=to_email, + context={ + "candidate": candidate.name, + "status": Candidate.STATUS[candidate.status], + "confirmation_link": confirmation_link, + }, + to_emails=[to_email], + text_template="emails/05_confirmation.txt", + html_template="emails/05_confirmation.html", ) @@ -563,23 +560,6 @@ def has_change_permission(self, request, obj=None): return False -@admin.register(EmailTemplate) -class EmailTemplateAdmin(admin.ModelAdmin): - list_display = ["template"] - readonly_fields = ["template"] - - def has_add_permission(self, request): - return False - - def has_delete_permission(self, request, obj=None): - return False - - def has_change_permission(self, request, obj=None): - if request.user.is_superuser: - return True - return False - - @admin.register(BlogPost) class BlogPostAdmin(admin.ModelAdmin): list_display = ["title", "slug", "author", "published_date", "is_visible"] diff --git a/backend/hub/forms.py b/backend/hub/forms.py index df4bf87f..5c0d9072 100644 --- a/backend/hub/forms.py +++ b/backend/hub/forms.py @@ -1,16 +1,16 @@ -from django_recaptcha.fields import ReCaptchaField from django import forms from django.conf import settings from django.core.exceptions import PermissionDenied, ValidationError -from django.core.mail import send_mail from django.urls import reverse_lazy from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ +from django_recaptcha.fields import ReCaptchaField -# from django_crispy_bulma.widgets import EmailInput - +from civil_society_vote.common.messaging import send_email from hub.models import Candidate, City, Domain, FeatureFlag, Organization +# from django_crispy_bulma.widgets import EmailInput + ORG_FIELD_ORDER = [ "name", "county", @@ -276,10 +276,16 @@ def __init__(self, *args, **kwargs): if not settings.RECAPTCHA_PUBLIC_KEY: del self.fields["captcha"] - def send_email(self): - send_mail( - f"Contact {self.cleaned_data.get('name')}", - f"{self.cleaned_data.get('email')}: {self.cleaned_data.get('message')}", - settings.NO_REPLY_EMAIL, - (settings.DEFAULT_FROM_EMAIL,), + def send_form_email(self): + send_email( + subject=f"[VotONG] Contact {self.cleaned_data.get('name')}", + to_emails=[settings.DEFAULT_FROM_EMAIL], + text_template="emails/06_contact.txt", + html_template="emails/06_contact.html", + context={ + "name": self.cleaned_data.get("name"), + "email": self.cleaned_data.get("email"), + "message": self.cleaned_data.get("message"), + }, + from_email=settings.NO_REPLY_EMAIL, ) diff --git a/backend/hub/management/commands/init.py b/backend/hub/management/commands/init.py index 05dc5eaa..b54a1c1b 100644 --- a/backend/hub/management/commands/init.py +++ b/backend/hub/management/commands/init.py @@ -2,47 +2,7 @@ from django.core.management.base import BaseCommand from guardian.shortcuts import assign_perm -from hub.models import COMMITTEE_GROUP, FLAG_CHOICES, NGO_GROUP, STAFF_GROUP, SUPPORT_GROUP, EmailTemplate, FeatureFlag - -PENDING_ORGS_DIGEST_TEXT = """ -Buna ziua, - -Un numar de {{ nr_pending_orgs }} organizatii asteapta aprobarea voastra pe platforma VotONG. - -Accesati adresa {{ committee_org_list_url }} pentru a vizualiza ultimele aplicatii. - -Va multumim! -Echipa VotONG -""" - -ORG_APPROVED_TEXT = """ -Buna {{ representative }}, - -Cererea de inscriere a organizatiei "{{ name }}" in platforma VotONG a fost aprobata. - -Pentru a va activa contul mergeti la adresa {{ reset_url }}, introduceti adresa de email folosita la inscriere si resetati parola contului. - -Va multumim! -Echipa VotONG -""" - -ORG_REJECTED_TEXT = """ -{{ rejection_message }} -""" - -VOTE_AUDIT_TEXT = """ -{{ org }} a votat cu {{ candidate }} la {{ timestamp }} - -Organizatie: {{ org_link }} - -Candidat: {{ candidate_link }} -""" - -CONFIRM_TEXT = """ -Administratorul site-ului votong.ro a schimbatul statusul candidatului "{{ candidate }}" în "{{ status }}". - -Urmează acest link pentru a confirma acțiunea: {{ confirmation_link }} -""" +from hub.models import COMMITTEE_GROUP, FLAG_CHOICES, FeatureFlag, NGO_GROUP, STAFF_GROUP, SUPPORT_GROUP class Command(BaseCommand): @@ -68,29 +28,4 @@ def handle(self, *args, **options): for flag in [x[0] for x in FLAG_CHOICES]: FeatureFlag.objects.get_or_create(flag=flag) - template, created = EmailTemplate.objects.get_or_create(template="pending_orgs_digest") - if created: - template.text_content = PENDING_ORGS_DIGEST_TEXT - template.save() - - template, created = EmailTemplate.objects.get_or_create(template="org_approved") - if created: - template.text_content = ORG_APPROVED_TEXT - template.save() - - template, created = EmailTemplate.objects.get_or_create(template="org_rejected") - if created: - template.text_content = ORG_REJECTED_TEXT - template.save() - - template, created = EmailTemplate.objects.get_or_create(template="vote_audit") - if created: - template.text_content = VOTE_AUDIT_TEXT - template.save() - - template, created = EmailTemplate.objects.get_or_create(template="confirmation") - if created: - template.text_content = CONFIRM_TEXT - template.save() - self.stdout.write(self.style.SUCCESS("Initialization done!")) diff --git a/backend/hub/migrations/0049_delete_emailtemplate_and_more.py b/backend/hub/migrations/0049_delete_emailtemplate_and_more.py new file mode 100644 index 00000000..ba882d5e --- /dev/null +++ b/backend/hub/migrations/0049_delete_emailtemplate_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2024-07-05 12:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("hub", "0048_alter_featureflag_flag"), + ] + + operations = [ + migrations.DeleteModel( + name="EmailTemplate", + ), + migrations.AlterModelOptions( + name="candidateconfirmation", + options={"verbose_name": "Candidate confirmation", "verbose_name_plural": "Candidate confirmations"}, + ), + migrations.AlterModelOptions( + name="candidatevote", + options={"verbose_name": "Candidate vote", "verbose_name_plural": "Candidate votes"}, + ), + ] diff --git a/backend/hub/models.py b/backend/hub/models.py index aea0c2e6..ba091bf0 100644 --- a/backend/hub/models.py +++ b/backend/hub/models.py @@ -1,7 +1,6 @@ import logging from ckeditor_uploader.fields import RichTextUploadingField -from ckeditor.fields import RichTextField from django.conf import settings from django.contrib.auth.models import Group from django.core.exceptions import ValidationError @@ -16,7 +15,6 @@ from accounts.models import User - # NOTE: If you change the group names here, make sure you also update the names in the live database before deployment STAFF_GROUP = "Code4Romania Staff" COMMITTEE_GROUP = "Comisie Electorala" @@ -99,14 +97,6 @@ def select_public_storage(): ("global_support_round", _("Enable global support (the support of at least 10 organizations is required)")), ) -EMAIL_TEMPLATE_CHOICES = Choices( - ("pending_orgs_digest", _("Pending organizations digest email")), - ("org_approved", _("Your organization was approved")), - ("org_rejected", _("Your organization was rejected")), - ("vote_audit", _("Vote audit log")), - ("confirmation", _("Confirmation email")), -) - def get_feature_flag(flag_choice: str) -> bool: if not flag_choice or flag_choice not in FLAG_CHOICES: @@ -166,19 +156,6 @@ def __str__(self): return self.title -class EmailTemplate(TimeStampedModel): - template = models.CharField(_("Template"), choices=EMAIL_TEMPLATE_CHOICES, max_length=254, unique=True) - text_content = models.TextField(_("Text content")) - html_content = RichTextField(_("HTML content"), blank=True) - - class Meta: - verbose_name = _("Email template") - verbose_name_plural = _("Email templates") - - def __str__(self): - return f"{EMAIL_TEMPLATE_CHOICES[self.template]}" - - class Domain(TimeStampedModel): name = models.CharField(_("Name"), max_length=254, unique=True) description = models.TextField(_("Description")) @@ -458,9 +435,7 @@ class Candidate(StatusModel, TimeStampedModel): ("rejected", _("Rejected")), ) - org = models.OneToOneField( - "Organization", on_delete=models.CASCADE, related_name="candidate", null=True, blank=True - ) + org = models.OneToOneField(Organization, on_delete=models.CASCADE, related_name="candidate", null=True, blank=True) initial_org = models.ForeignKey( "Organization", on_delete=models.CASCADE, @@ -545,6 +520,7 @@ def save(self, *args, **kwargs): # in the same time keeping the old candidate record and backwards compatibility with the one-to-one relations # that are used in the rest of the codebase. # TODO: Refactor this flow to make it less hacky and have a single relationship back to organization. + self.org: Organization if self.id and self.initial_org: self.org = None self.is_proposed = False @@ -564,7 +540,7 @@ class CandidateVote(TimeStampedModel): domain = models.ForeignKey("Domain", on_delete=models.PROTECT, related_name="votes") class Meta: - verbose_name_plural = _("Canditate votes") + verbose_name_plural = _("Candidate votes") verbose_name = _("Candidate vote") constraints = [ models.UniqueConstraint(fields=["user", "candidate"], name="unique_candidate_vote"), @@ -603,7 +579,7 @@ class CandidateConfirmation(TimeStampedModel): candidate = models.ForeignKey("Candidate", on_delete=models.CASCADE, related_name="confirmations") class Meta: - verbose_name_plural = _("Canditate confirmations") + verbose_name_plural = _("Candidate confirmations") verbose_name = _("Candidate confirmation") constraints = [ models.UniqueConstraint(fields=["user", "candidate"], name="unique_candidate_confirmation"), diff --git a/backend/hub/templates/emails/01_pending_orgs_digest.html b/backend/hub/templates/emails/01_pending_orgs_digest.html new file mode 100644 index 00000000..d6e9504c --- /dev/null +++ b/backend/hub/templates/emails/01_pending_orgs_digest.html @@ -0,0 +1,23 @@ +{% extends "emails/base_email.html" %} + +{% block content %} + +

Bună ziua,

+ +

+ Un număr de {{ nr_pending_orgs }} organizații așteaptă aprobarea voastră pe platforma VotONG. +

+ +

+ Accesați adresa + {{ committee_org_list_url }} + pentru a vizualiza ultimele aplicații. +

+ +

+ Vă mulțumim! +
+ Echipa VotONG +

+ +{% endblock %} diff --git a/backend/hub/templates/emails/01_pending_orgs_digest.txt b/backend/hub/templates/emails/01_pending_orgs_digest.txt new file mode 100644 index 00000000..a63ce90c --- /dev/null +++ b/backend/hub/templates/emails/01_pending_orgs_digest.txt @@ -0,0 +1,8 @@ +Bună ziua, + +Un număr de {{ nr_pending_orgs }} organizații așteaptă aprobarea voastră pe platforma VotONG. + +Accesați adresa {{ committee_org_list_url }} pentru a vizualiza ultimele aplicații. + +Vă mulțumim! +Echipa VotONG diff --git a/backend/hub/templates/emails/02_org_approved.html b/backend/hub/templates/emails/02_org_approved.html new file mode 100644 index 00000000..889993d6 --- /dev/null +++ b/backend/hub/templates/emails/02_org_approved.html @@ -0,0 +1,20 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

Bună {{ representative }},

+ +

+ Cererea de înscriere a organizației "{{ name }}" în platforma VotONG a fost aprobată. +

+ +

+ Pentru a vă loga mergeți la adresa + {{ site_url }}. +

+ +

+ Va mulțumim! +
+ Echipa VotONG +

+{% endblock %} diff --git a/backend/hub/templates/emails/02_org_approved.txt b/backend/hub/templates/emails/02_org_approved.txt new file mode 100644 index 00000000..d3e25623 --- /dev/null +++ b/backend/hub/templates/emails/02_org_approved.txt @@ -0,0 +1,8 @@ +Bună {{ representative }}, + +Cererea de înscriere a organizației "{{ name }}" în platforma VotONG a fost aprobată. + +Pentru a vă loga mergeți la adresa {{ site_url }}. + +Va mulțumim! +Echipa VotONG diff --git a/backend/hub/templates/emails/03_org_rejected.html b/backend/hub/templates/emails/03_org_rejected.html new file mode 100644 index 00000000..3eaf88c6 --- /dev/null +++ b/backend/hub/templates/emails/03_org_rejected.html @@ -0,0 +1,21 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

Bună {{ representative }},

+ +

+ Organizația "{{ name }}" a fost respinsă din platforma VotONG. +

+ + {% if rejection_message %} +

+ Motivul respingerii: "{{ rejection_message }}" +

+ {% endif %} + +

+ Va mulțumim! +
+ Echipa VotONG +

+{% endblock %} \ No newline at end of file diff --git a/backend/hub/templates/emails/03_org_rejected.txt b/backend/hub/templates/emails/03_org_rejected.txt new file mode 100644 index 00000000..2913a509 --- /dev/null +++ b/backend/hub/templates/emails/03_org_rejected.txt @@ -0,0 +1,10 @@ +Bună {{ representative }}, + +Organizația "{{ name }}" a fost respinsă din platforma VotONG. + +{% if rejection_message %} +Motivul respingerii: "{{ rejection_message }}" +{% endif %} + +Va mulțumim! +Echipa VotONG diff --git a/backend/hub/templates/emails/04_vote_audit.html b/backend/hub/templates/emails/04_vote_audit.html new file mode 100644 index 00000000..0dcb18bb --- /dev/null +++ b/backend/hub/templates/emails/04_vote_audit.html @@ -0,0 +1,11 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

{{ org }} a votat cu {{ candidate }} la {{ timestamp }}

+ +

+ Organizație: {{ org_link }} +
+ Candidat: {{ candidate_link }} +

+{% endblock %} diff --git a/backend/hub/templates/emails/04_vote_audit.txt b/backend/hub/templates/emails/04_vote_audit.txt new file mode 100644 index 00000000..cacc98df --- /dev/null +++ b/backend/hub/templates/emails/04_vote_audit.txt @@ -0,0 +1,5 @@ +{{ org }} a votat cu {{ candidate }} la {{ timestamp }} + +Organizație: {{ org_link }} + +Candidat: {{ candidate_link }} diff --git a/backend/hub/templates/emails/05_confirmation.html b/backend/hub/templates/emails/05_confirmation.html new file mode 100644 index 00000000..3a6a702a --- /dev/null +++ b/backend/hub/templates/emails/05_confirmation.html @@ -0,0 +1,10 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

Administratorul site-ului votong.ro a schimbatul statusul candidatului "{{ candidate }}" în "{{ status }}".

+ +

+ Urmează acest link pentru a le confirma acțiunea: + {{ confirmation_link }} +

+{% endblock %} diff --git a/backend/hub/templates/emails/05_confirmation.txt b/backend/hub/templates/emails/05_confirmation.txt new file mode 100644 index 00000000..5ffdea85 --- /dev/null +++ b/backend/hub/templates/emails/05_confirmation.txt @@ -0,0 +1,3 @@ +Administratorul site-ului votong.ro a schimbatul statusul candidatului "{{ candidate }}" în "{{ status }}". + +Urmează acest link pentru a le confirma acțiunea: {{ confirmation_link }} diff --git a/backend/hub/templates/emails/06_contact.html b/backend/hub/templates/emails/06_contact.html new file mode 100644 index 00000000..d191e075 --- /dev/null +++ b/backend/hub/templates/emails/06_contact.html @@ -0,0 +1,24 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

+ Un nou mesaj de contact în Vot ONG +

+ +

+ Nume: {{ name }} +

+ +

+ Email: {{ email }} +

+ +
+

+ Mesaj: +

+

+ {{ message }} +

+
+{% endblock %} diff --git a/backend/hub/templates/emails/06_contact.txt b/backend/hub/templates/emails/06_contact.txt new file mode 100644 index 00000000..1ef12274 --- /dev/null +++ b/backend/hub/templates/emails/06_contact.txt @@ -0,0 +1,7 @@ +Un nou mesaj de contact: + +Nume: {{ name }} +Email: {{ email }} + +Mesaj: +{{ message }} diff --git a/backend/hub/utils.py b/backend/hub/utils.py index baf42deb..c92e82e3 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -1,34 +1,3 @@ -from django.conf import settings -from django.core.mail import EmailMultiAlternatives -from django.template import Template - -from hub.models import EmailTemplate - - -def send_email(template, context, subject, to): - """ - Sends a single email - - :param template: One of the EMAIL_TEMPLATE_CHOICES from models - :param context: A dict containing the dynamic values of that template - :param subject: The subject of the email - :param to: Destination email address - :return: Message send result - """ - tpl = EmailTemplate.objects.get(template=template) - - text_content = Template(tpl.text_content).render(context) - msg = EmailMultiAlternatives( - subject, text_content, settings.NO_REPLY_EMAIL, [to], headers={"X-SES-CONFIGURATION-SET": "votong"} - ) - - if tpl.html_content: - html_content = Template(tpl.html_content).render(context) - msg.attach_alternative(html_content, "text/html") - - return msg.send(fail_silently=True) - - def build_full_url(request, obj): """ :param request: django Request object diff --git a/backend/hub/views.py b/backend/hub/views.py index dbe7ec96..212254cf 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -11,7 +11,6 @@ from django.db.utils import IntegrityError from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect -from django.template import Context from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ @@ -20,7 +19,7 @@ from guardian.decorators import permission_required_or_403 from guardian.mixins import LoginRequiredMixin, PermissionRequiredMixin -from hub import utils +from civil_society_vote.common.messaging import send_email from hub.forms import ( CandidateRegisterForm, CandidateUpdateForm, @@ -29,11 +28,8 @@ OrganizationUpdateForm, ) from hub.models import ( - COMMITTEE_GROUP, - NGO_GROUP, - STAFF_GROUP, - SUPPORT_GROUP, BlogPost, + COMMITTEE_GROUP, Candidate, CandidateConfirmation, CandidateSupporter, @@ -41,7 +37,10 @@ City, Domain, FeatureFlag, + NGO_GROUP, Organization, + STAFF_GROUP, + SUPPORT_GROUP, ) @@ -66,7 +65,7 @@ class HomeView(MenuMixin, SuccessMessageMixin, FormView): success_message = _("Thank you! We'll get in touch soon!") def form_valid(self, form): - form.send_email() + form.send_form_email() return super().form_valid(form) @@ -302,17 +301,16 @@ def organization_vote(request, pk, action): if org.rejection_message: org.save() - utils.send_email( - template="org_rejected", - context=Context( - { - "representative": org.legal_representative_name, - "name": org.name, - "rejection_message": org.rejection_message, - } - ), - subject="Cerere de inscriere respinsa", - to=org.email, + send_email( + subject="Cerere de înscriere respinsă", + to_emails=[org.email], + text_template="emails/03_org_rejected.txt", + html_template="emails/03_org_rejected.html", + context={ + "representative": org.legal_representative_name, + "name": org.name, + "rejection_message": org.rejection_message, + }, ) else: messages.error(request, _("You must write a rejection message.")) @@ -326,17 +324,16 @@ def organization_vote(request, pk, action): current_site = get_current_site(request) protocol = "https" if request.is_secure() else "http" - utils.send_email( - template="org_approved", - context=Context( - { - "representative": org.legal_representative_name, - "name": org.name, - "reset_url": f"{protocol}://{current_site.domain}{reverse('password_reset')}", - } - ), - subject="Cerere de inscriere aprobata", - to=org.email, + send_email( + subject="Cerere de înscriere aprobată", + to_emails=[org.email], + text_template="emails/02_org_approved.txt", + html_template="emails/02_org_approved.html", + context={ + "representative": org.legal_representative_name, + "name": org.name, + "site_url": f"{protocol}://{current_site.domain}", + }, ) finally: return redirect("ngo-detail", pk=pk) @@ -478,19 +475,18 @@ def candidate_vote(request, pk): if settings.VOTE_AUDIT_EMAIL: current_site = get_current_site(request) protocol = "https" if request.is_secure() else "http" - utils.send_email( - template="vote_audit", - context=Context( - { - "org": vote.user.orgs.first().name, - "candidate": vote.candidate.name, - "timestamp": timezone.localtime(vote.created).strftime("%H:%M:%S (%d/%m/%Y)"), - "org_link": f"{protocol}://{current_site.domain}{vote.user.orgs.first().get_absolute_url()}", - "candidate_link": f"{protocol}://{current_site.domain}{vote.candidate.get_absolute_url()}", - } - ), - subject=f"[VOTONG] Vot candidatura: {vote.candidate.name}", - to=settings.VOTE_AUDIT_EMAIL, + send_email( + subject=f"[VOTONG] Vot candidatură: {vote.candidate.name}", + to_emails=[settings.VOTE_AUDIT_EMAIL], + text_template="emails/04_vote_audit.txt", + html_template="emails/04_vote_audit.html", + context={ + "org": vote.user.orgs.first().name, + "candidate": vote.candidate.name, + "timestamp": timezone.localtime(vote.created).strftime("%H:%M:%S (%d/%m/%Y)"), + "org_link": f"{protocol}://{current_site.domain}{vote.user.orgs.first().get_absolute_url()}", + "candidate_link": f"{protocol}://{current_site.domain}{vote.candidate.get_absolute_url()}", + }, ) return redirect("candidate-detail", pk=pk) diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 9bcaaaba..1ad8def7 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -751,7 +751,7 @@ msgid "Cannot update candidate after votes have been cast." msgstr "" #: backend/hub/models.py:557 -msgid "Canditate votes" +msgid "Candidate votes" msgstr "" #: backend/hub/models.py:558 @@ -759,7 +759,7 @@ msgid "Candidate vote" msgstr "" #: backend/hub/models.py:581 -msgid "Canditate supporters" +msgid "Candidate supporters" msgstr "" #: backend/hub/models.py:582 @@ -767,7 +767,7 @@ msgid "Candidate supporter" msgstr "" #: backend/hub/models.py:596 -msgid "Canditate confirmations" +msgid "Candidate confirmations" msgstr "" #: backend/hub/models.py:597 diff --git a/backend/locale/ro/LC_MESSAGES/django.po b/backend/locale/ro/LC_MESSAGES/django.po index 11ae56d7..2dbb9da2 100644 --- a/backend/locale/ro/LC_MESSAGES/django.po +++ b/backend/locale/ro/LC_MESSAGES/django.po @@ -756,7 +756,7 @@ msgid "Cannot update candidate after votes have been cast." msgstr "Nu se poate modifica candidatura dupa ce s-au inregistrat voturi." #: backend/hub/models.py:557 -msgid "Canditate votes" +msgid "Candidate votes" msgstr "Voturi candidatură" #: backend/hub/models.py:558 @@ -764,7 +764,7 @@ msgid "Candidate vote" msgstr "Vot candidatură" #: backend/hub/models.py:581 -msgid "Canditate supporters" +msgid "Candidate supporters" msgstr "Susținători candidatură" #: backend/hub/models.py:582 @@ -772,7 +772,7 @@ msgid "Candidate supporter" msgstr "Susținător candidatură" #: backend/hub/models.py:596 -msgid "Canditate confirmations" +msgid "Candidate confirmations" msgstr "Confirmări candidatură" #: backend/hub/models.py:597 diff --git a/backend/templates/emails/base_email.html b/backend/templates/emails/base_email.html new file mode 100644 index 00000000..d9a556c0 --- /dev/null +++ b/backend/templates/emails/base_email.html @@ -0,0 +1,19 @@ + + + + + + + + + {% block header %}{% endblock %} + +
+ {% block content %}{% endblock %} +
+ + {% block footer %}{% endblock %} + + + +