Skip to content

Commit

Permalink
#653 Bulk SMS - enable email if user has no phone number (#165)
Browse files Browse the repository at this point in the history
* send email if user has no phone number

* tox fix

* removed debugging code

* tox migration fix

* black migration

* black migration
  • Loading branch information
KlemenSpruk authored Nov 6, 2023
1 parent ddba8a6 commit 851d5d3
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 24 deletions.
2 changes: 2 additions & 0 deletions django_project_base/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@
INVITE_NOTIFICATION_TEXT = "invite-notification-link-text"

NOTIFY_NEW_USER_SETTING_NAME = "notify-new-user-via-email-account-created"

USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER = "notify-user-via-email-if-no-phone-number"
2 changes: 1 addition & 1 deletion django_project_base/licensing/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def log(
for used_channel in list(set(list(items.keys()))):
used += chl_prices.get(used_channel, 0) * items.get(used_channel, 0)

if not kwargs.get("is_system_notification") and used >= 0: # janez medja
if not kwargs.get("is_system_notification") and used >= 0:
raise PermissionDenied(gettext("Your license is consumed. Please contact support."))

if on_sucess:
Expand Down
63 changes: 59 additions & 4 deletions django_project_base/notifications/base/channels/channel.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import logging
import uuid
from abc import ABC, abstractmethod
Expand All @@ -9,7 +10,11 @@

from django_project_base.notifications.base.channels.integrations.provider_integration import ProviderIntegration
from django_project_base.notifications.base.phone_number_parser import PhoneNumberParser
from django_project_base.notifications.models import DeliveryReport, DjangoProjectBaseNotification
from django_project_base.notifications.models import (
DeliveryReport,
DjangoProjectBaseMessage,
DjangoProjectBaseNotification,
)
from django_project_base.utils import get_pk_name


Expand Down Expand Up @@ -88,14 +93,21 @@ def clean_recipients(self, recipients: List[Recipient]) -> List[Recipient]:
return list(set(recipients))

def create_delivery_report(
self, notification: DjangoProjectBaseNotification, recipient: Recipient, pk: str
self,
notification: DjangoProjectBaseNotification,
recipient: Recipient,
pk: str,
channel: Optional[str] = None,
provider: Optional[str] = None,
auxiliary_notification: Optional[uuid.UUID] = None,
) -> DeliveryReport:
return DeliveryReport.objects.create(
notification=notification,
user_id=recipient.identifier,
channel=f"{self.__module__}.{self.__class__.__name__}",
provider=f"{self.provider.__module__}.{self.provider.__class__.__name__}",
channel=f"{self.__module__}.{self.__class__.__name__}" if not channel else channel,
provider=f"{self.provider.__module__}.{self.provider.__class__.__name__}" if not provider else provider,
pk=pk,
auxiliary_notification=auxiliary_notification,
)

@abstractmethod
Expand Down Expand Up @@ -151,9 +163,52 @@ def make_send(notification_obj, rec_obj, message_str, dlr_pk) -> Optional[Delive
logger.exception(de)
return dlr_obj

from django_project_base.notifications.base.channels.mail_channel import MailChannel

mail_fallback = (
MailChannel.name not in (notification.required_channels or "").split(",")
and notification.email_fallback
)

from django_project_base.notifications.email_notification import EMailNotification

for recipient in recipients: # noqa: E203
dlr__uuid = str(uuid.uuid4())
try:
if (
self.provider.is_sms_provider
and not recipient.phone_number
and mail_fallback
and not notification.send_notification_sms
):
try:
a_notification = EMailNotification(
message=DjangoProjectBaseMessage(
subject=notification.message.subject,
body=notification.message.body,
footer=notification.message.footer,
content_type=notification.message.content_type,
),
raw_recipents=[
recipient.identifier,
],
project=notification.project_slug if notification.project_slug else None,
recipients=[
recipient.identifier,
],
a_sender=notification.sender,
a_extra_data=extra_data,
a_recipients_list=notification.recipients_list,
delay=int(datetime.datetime.now().timestamp()),
).send()
self.create_delivery_report(
notification, recipient, dlr__uuid, auxiliary_notification=a_notification.pk
)
continue
except Exception as e:
logger.exception(e)
continue

while dlr := not make_send(
notification_obj=notification, message_str=message, rec_obj=recipient, dlr_pk=dlr__uuid
):
Expand Down
57 changes: 42 additions & 15 deletions django_project_base/notifications/base/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from django.conf import settings
from django.contrib.auth import get_user_model

from django_project_base.constants import EMAIL_SENDER_ID_SETTING_NAME, SMS_SENDER_ID_SETTING_NAME
from django_project_base.constants import (
EMAIL_SENDER_ID_SETTING_NAME,
SMS_SENDER_ID_SETTING_NAME,
USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER,
)
from django_project_base.notifications.base.channels.channel import Channel
from django_project_base.notifications.base.duplicate_notification_mixin import DuplicateNotificationMixin
from django_project_base.notifications.base.enums import ChannelIdentifier, NotificationLevel, NotificationType
Expand Down Expand Up @@ -94,6 +98,14 @@ def resend(notification: DjangoProjectBaseNotification, user_pk: str):
notification.recipients = ",".join(map(str, recipients)) if recipients else None
notification.recipients_original_payload_search = None
notification.sender = Notification._get_sender_config(notification.project_slug)
mail_fallback: bool = (
swapper.load_model("django_project_base", "ProjectSettings")
.objects.get(name=USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER, project__slug=notification.project_slug)
.python_value
if notification.project_slug
else False
)
notification.email_fallback = mail_fallback
notification.save(update_fields=["recipients", "recipients_original_payload_search"])
SendNotificationMixin().make_send(notification, {})

Expand Down Expand Up @@ -176,17 +188,19 @@ def send(self) -> DjangoProjectBaseNotification:
if not self.persist:
raise Exception("Delayed notification must be persisted")
self._set_db()
rec_list = []
for usr in self._recipients:
rec_list.append(
{
k: v
for k, v in get_user_model().objects.get(pk=usr).userprofile.__dict__.items()
if not k.startswith("_")
}
)

rec_list = self._extra_data.get("a_recipients_list") or []
if len(rec_list) == 0:
for usr in self._recipients:
rec_list.append(
{
k: v
for k, v in get_user_model().objects.get(pk=usr).userprofile.__dict__.items()
if not k.startswith("_")
}
)
notification.recipients_list = rec_list
self.enqueue_notification(notification, self._extra_data)
self.enqueue_notification(notification, self._extra_data.get("a_extra_data") or self._extra_data)
return notification

notification = self.make_send(notification, self._extra_data)
Expand All @@ -210,23 +224,36 @@ def _ensure_channels(
) -> DjangoProjectBaseNotification:
from django_project_base.notifications.base.channels.mail_channel import MailChannel

extra_data = self._extra_data.get("a_extra_data") or self._extra_data

for channel_name in channels:
# ensure dlr user and check providers
channel = ChannelIdentifier.channel(channel_name, extra_data=self._extra_data, project_slug=self._project)
channel = ChannelIdentifier.channel(channel_name, extra_data=extra_data, project_slug=self._project)

if not channel and self._extra_data.get("is_system_notification"):
if not channel and extra_data.get("is_system_notification"):
continue

assert channel

if self.send_notification_sms and channel.name == MailChannel.name:
notification.send_notification_sms_text = channel.provider.get_send_notification_sms_text(
notification=notification, host_url=self._extra_data.get("host_url", "") # noqa: E126
notification=notification, host_url=extra_data.get("host_url", "") # noqa: E126
)

notification.user = self._user

notification.sender = Notification._get_sender_config(self._project)
notification.sender = self._extra_data.get("a_sender") or Notification._get_sender_config(self._project)

mail_fallback = False
if not self._extra_data.get("a_sender"):
mail_fallback: bool = (
swapper.load_model("django_project_base", "ProjectSettings")
.objects.get(name=USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER, project__slug=notification.project_slug)
.python_value
if notification.project_slug
else False
)
notification.email_fallback = mail_fallback

if self._extra_data.get("is_system_notification"):
notification.sender[MailChannel.name] = getattr(settings, "SYSTEM_EMAIL_SENDER_ID", "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.4 on 2023-10-26 05:55
from gettext import gettext

import swapper
from django.db import migrations

from django_project_base.constants import USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER


def forwards_func(apps, schema_editor):
project_sett = swapper.load_model("django_project_base", "ProjectSettings")
for project in swapper.load_model("django_project_base", "Project").objects.all():
project_sett.objects.get_or_create(
project=project,
name=USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER,
defaults=dict(
description=gettext("Send notification via EMail if user has no phone number"),
value=False,
value_type="bool",
),
)


def reverse_func(apps, schema_editor):
pass


class Migration(migrations.Migration):
dependencies = [
("notifications", "0006_djangoprojectbasenotification_send_notification_sms"),
]

operations = [
migrations.RunPython(forwards_func, reverse_func),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.4 on 2023-10-26 06:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("notifications", "0007_auto_20231026_0555"),
]

operations = [
migrations.AddField(
model_name="deliveryreport",
name="auxiliary_notification",
field=models.UUIDField(null=True, verbose_name="Auxiliary notification"),
),
]
10 changes: 10 additions & 0 deletions django_project_base/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class DjangoProjectBaseNotification(AbstractDjangoProjectBaseNotification):
_user = None
_sender = {}
_email_list = []
_email_fallback = False

def _get_recipients(self):
return self._recipients_list
Expand All @@ -117,6 +118,12 @@ def _get_email_list(self):
def _set_email_list(self, val):
self._email_list = val

def _get_email_fallback(self):
return self._email_fallback

def _set_email_fallback(self, val):
self._email_fallback = val

recipients_list = property(_get_recipients, _set_recipents)

user = property(_get_user, _set_user)
Expand All @@ -125,6 +132,8 @@ def _set_email_list(self, val):

email_list = property(_get_email_list, _set_email_list)

email_fallback = property(_get_email_fallback, _set_email_fallback)


class SearchItemObject:
label = ""
Expand Down Expand Up @@ -245,3 +254,4 @@ class Status(IntDescribedEnum):
status = models.IntegerField(
default=Status.PENDING_DELIVERY, choices=Status.get_choices_tuple(), db_index=True, null=False
)
auxiliary_notification = models.UUIDField(verbose_name=_("Auxiliary notification"), null=True, blank=False)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_send_mail(self):
raw_recipents=[
self.test_user.pk,
],
project=swapper.load_model("django_project_base", "Project").objects.first(),
project=swapper.load_model("django_project_base", "Project").objects.first().slug,
recipients=[
self.test_user.pk,
],
Expand All @@ -32,7 +32,7 @@ def test_send_mail(self):
raw_recipents=[
self.test_user.pk,
],
project=swapper.load_model("django_project_base", "Project").objects.first(),
project=swapper.load_model("django_project_base", "Project").objects.first().slug,
recipients=[
self.test_user.pk,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

from django_project_base.constants import USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER
from django_project_base.notifications.base.channels.channel import Channel
from django_project_base.notifications.base.channels.mail_channel import MailChannel
from django_project_base.notifications.base.notification import Notification
Expand All @@ -33,6 +34,16 @@ def setUp(self) -> None:
name="test", slug="test", owner=self.test_user.userprofile
)
self.api_client = APIClient()
for p in swapper.load_model("django_project_base", "Project").objects.all():
swapper.load_model("django_project_base", "ProjectSettings").objects.get_or_create(
name=USE_EMAIL_IF_RECIPIENT_HAS_NO_PHONE_NUBER,
project=p,
defaults=dict(
value=False,
value_type="char",
description="Tests value",
),
)

def _login_to_api_client_with_test_user(self):
user_token, token_created = Token.objects.get_or_create(user=self.test_user)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Generated by Django 4.2.4 on 2023-10-10 06:50

from django.db import migrations, models


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-10-26 06:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("demo_django_base", "0009_projectsettings_pending_value"),
]

operations = [
migrations.AlterField(
model_name="projectsettings",
name="action_required",
field=models.BooleanField(blank=True, default=False, null=True),
),
migrations.AlterField(
model_name="projectsettings",
name="pending_value",
field=models.CharField(blank=True, max_length=320, null=True, verbose_name="Pending value"),
),
]

0 comments on commit 851d5d3

Please sign in to comment.