Skip to content

Commit

Permalink
Add several functions and maps for notifications logic (#324)
Browse files Browse the repository at this point in the history
* Add several functions and maps for notifications logic, not working

* Finish notification sending function

* Remove redundant check for valid notification channel for notification type

* Send notifications when notification events occur

* Recommend unused function

* Add throttle classes back

* Fix file extensions

* Fix notification template prefix

* Add message field to notification model to support notifications that mention project names

* Properly create migrations and use char field

* Use name instead of label for django integer choice class

* Change notification link attribute to be a char field

* Fix email templates for notifications
  • Loading branch information
AndrewLester authored and ajain1921 committed Apr 27, 2022
1 parent e1a091f commit 8d87b41
Show file tree
Hide file tree
Showing 35 changed files with 314 additions and 57 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
2 changes: 1 addition & 1 deletion zubhub_backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ destroy: ## Remove stopped containers, networks not currently in use, images no
.PHONY: destroy

.env: ## Generate a .env file for local development
bash ./compose/make_backend_env.sh ./.env
bash ./compose/make_backend_env.sh ./.env
1 change: 1 addition & 0 deletions zubhub_backend/compose/celery/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ vine>=1.3.0
watchdog>=0.10.2
wcwidth>=0.2.5
whitenoise>=4.1.4
django-extensions>=1.0.0
47 changes: 24 additions & 23 deletions zubhub_backend/zubhub/creators/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,31 +151,32 @@ def send_group_invite_mail(self, group_invite_confirmation):
self.send_mail(
email_template, group_invite_confirmation.creator.email, ctx)

def send_mass_email(self, template_prefix, contexts):
if settings.ENVIRONMENT == "production" and not settings.DEBUG:
def send_mass_email(self, template_prefix, contexts, full_template=False):
if not full_template:
template_prefix = "projects/email/" + template_prefix
connection = get_connection(
username=None, password=None, fail_silently=False)
messages = []
for ctx in contexts:
msg = self.render_mail(template_prefix, ctx["email"], ctx)
messages.append(msg)

return connection.send_messages(messages)

def send_mass_text(self, template_prefix, contexts):
if settings.ENVIRONMENT == "production" and not settings.DEBUG:
connection = get_connection(
username=None, password=None, fail_silently=False)
messages = []
for ctx in contexts:
msg = self.render_mail(template_prefix, ctx["email"], ctx)
messages.append(msg)

return connection.send_messages(messages)

def send_mass_text(self, template_prefix, contexts, full_template=False):
template_name = template_prefix
if not full_template:
template_name = "projects/phone/" + template_prefix + "_message.txt"
client = Client(settings.TWILIO_ACCOUNT_SID,
settings.TWILIO_AUTH_TOKEN)
client = Client(settings.TWILIO_ACCOUNT_SID,
settings.TWILIO_AUTH_TOKEN)

rendered_text = self.render_text(
template_name, contexts[0]["phone"], contexts[0])
rendered_text = self.render_text(
template_name, contexts[0]["phone"], contexts[0])

bindings = list(map(lambda context: json.dumps(
{'binding_type': 'sms', 'address': context["phone"]}), contexts))
bindings = list(map(lambda context: json.dumps(
{'binding_type': 'sms', 'address': context["phone"]}), contexts))

client.notify.services(settings.TWILIO_NOTIFY_SERVICE_SID).notifications.create(
to_binding=bindings,
body=rendered_text["body"]
)
client.notify.services(settings.TWILIO_NOTIFY_SERVICE_SID).notifications.create(
to_binding=bindings,
body=rendered_text["body"]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2022-03-20 21:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('creators', '0004_auto_20220222_0254'),
]

operations = [
migrations.AlterField(
model_name='setting',
name='contact',
field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'WHATSAPP'), (2, 'EMAIL'), (3, 'SMS'), (4, 'WEB')], default=3, null=True),
),
]
32 changes: 32 additions & 0 deletions zubhub_backend/zubhub/creators/model_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.core.exceptions import FieldDoesNotExist

from django.contrib.auth import get_user_model


def user_field(user, field, *args):
"""
Gets or sets (optional) user model fields. No-op if fields do not exist.
"""
if not field:
return
User = get_user_model()
try:
field_meta = User._meta.get_field(field)
max_length = field_meta.max_length
except FieldDoesNotExist:
if not hasattr(user, field):
return
max_length = None
if args:
# Setter
v = args[0]
if v:
v = v[0:max_length]
setattr(user, field, v)
else:
# Getter
return getattr(user, field)


def user_phone(user, *args):
return user_field(user, "phone", *args)
6 changes: 4 additions & 2 deletions zubhub_backend/zubhub/creators/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.contrib.postgres.indexes import GinIndex

from .managers import PhoneNumberManager
from .utils import user_phone
from .model_utils import user_phone

try:
from allauth.account import app_settings as allauth_settings
Expand Down Expand Up @@ -83,11 +83,13 @@ class Setting(models.Model):
WHATSAPP = 1
EMAIL = 2
SMS = 3
WEB = 4

CONTACT_CHOICES = (
(WHATSAPP, 'WHATSAPP'),
(EMAIL, 'EMAIL'),
(SMS, 'SMS')
(SMS, 'SMS'),
(WEB, 'WEB')
)

creator = models.OneToOneField(
Expand Down
8 changes: 4 additions & 4 deletions zubhub_backend/zubhub/creators/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ def send_whatsapp(self, phone, template_name, ctx):


@shared_task(name="creators.tasks.send_mass_email", bind=True, acks_late=True, max_retries=10)
def send_mass_email(self, template_name, ctxs):
def send_mass_email(self, template_name, ctxs, full_template=False):
try:
get_adapter().send_mass_email(template_name, ctxs)
get_adapter().send_mass_email(template_name, ctxs, full_template)
except Exception as e:
raise self.retry(exc=e, countdown=int(
uniform(2, 4) ** self.request.retries))


@shared_task(name="creators.tasks.send_mass_text", bind=True, acks_late=True, max_retries=10)
def send_mass_text(self, template_name, ctxs):
def send_mass_text(self, template_name, ctxs, full_template=False):
try:
get_adapter().send_mass_text(template_name, ctxs)
get_adapter().send_mass_text(template_name, ctxs, full_template)
except Exception as e:
raise self.retry(exc=e, countdown=int(
uniform(2, 4) ** self.request.retries))
Expand Down
63 changes: 46 additions & 17 deletions zubhub_backend/zubhub/creators/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from datetime import timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank
from django.db.models import F
from typing import List, Set, Dict, cast
from django.core.exceptions import FieldDoesNotExist
from django.db import transaction
from django.utils import timezone
from django.contrib.auth import get_user_model
from projects.tasks import delete_file_task
from creators.tasks import upload_file_task, send_mass_email, send_mass_text, send_whatsapp
from creators.models import Setting
from notifications.models import Notification
from notifications.utils import push_notification
from creators.models import Creator
from django.template.loader import render_to_string

try:
from allauth.account.adapter import get_adapter
Expand Down Expand Up @@ -368,40 +374,63 @@ def enforce_creator__creator_tags_constraints(creator, tag):



def send_notification(users, contexts, template_name):
from .models import Setting
enabled_notification_settings: Dict[Notification.Type, Set[int]] = {
Notification.Type.BOOKMARK: {Setting.WEB},
Notification.Type.CLAP: {Setting.WEB},
Notification.Type.COMMENT: {Setting.WHATSAPP, Setting.EMAIL, Setting.SMS, Setting.WEB},
Notification.Type.FOLLOW: {Setting.WHATSAPP, Setting.EMAIL, Setting.SMS, Setting.WEB},
Notification.Type.FOLLOWING_PROJECT:
{Setting.WHATSAPP, Setting.EMAIL, Setting.SMS, Setting.WEB},
}


def is_valid_setting(setting: int, notification_type: Notification.Type) -> bool:
return setting in enabled_notification_settings[notification_type]


html_based_contacts = {Setting.WEB}
def get_notification_template_name(
contact_method: int, notification_type: Notification.Type) -> str:
file_extension = '.html' if contact_method in html_based_contacts else '.txt'
if contact_method == Setting.EMAIL:
file_extension = ''
return (f'notifications/{notification_type.name.lower()}'
f'/{Setting.CONTACT_CHOICES[cast(int, contact_method) - 1][1].lower()}{file_extension}')


def send_notification(users: List[Creator], source: Creator, contexts,
notification_type: Notification.Type, link: str) -> None:
email_contexts = []
sms_contexts = []

for user, context in zip(users, contexts):
user_setting = Setting.objects.get(creator=user)
context.update({'source': user.username})

if user.phone and user_setting.contact == Setting.WHATSAPP:
if user.phone and user_setting.contact == Setting.WHATSAPP and is_valid_setting(Setting.WHATSAPP, notification_type):
context.update({"phone": user.phone})
send_whatsapp(
phone=user.phone,
template_name=template_name,
ctx=context)
send_whatsapp.delay(phone=user.phone,
template_name=get_notification_template_name(Setting.WHATSAPP, notification_type),
ctx=context)

if user.email and user_setting.contact == Setting.EMAIL:
if user.email and user_setting.contact == Setting.EMAIL and is_valid_setting(Setting.EMAIL, notification_type):
context.update({"email": user.email})
email_contexts.append(context)

if user.phone and user_setting.contact == Setting.SMS:
if user.phone and user_setting.contact == Setting.SMS and is_valid_setting(Setting.SMS, notification_type):
context.update({"phone": user.phone})
sms_contexts.append(context)

message = render_to_string(
get_notification_template_name(Setting.WEB, notification_type),
context
).strip()
push_notification(user, source, notification_type, message, link)

if len(email_contexts) > 0:
send_mass_email.delay(
template_name=template_name,
ctxs=email_contexts
)
send_mass_email.delay(template_name=get_notification_template_name(Setting.EMAIL, notification_type), ctxs=email_contexts, full_template=True)
if len(sms_contexts) > 0:
send_mass_text.delay(
template_name=template_name,
ctxs=sms_contexts
)
send_mass_text.delay(template_name=get_notification_template_name(Setting.SMS, notification_type), ctxs=sms_contexts, full_template=True)


# def sync_user_email_addresses(user):
Expand Down
3 changes: 3 additions & 0 deletions zubhub_backend/zubhub/creators/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import csv
from io import StringIO
from django.utils.translation import ugettext_lazy as _
from notifications.models import Notification
from rest_framework import status
from django.http import Http404
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -361,6 +362,8 @@ def get_object(self):
else:
obj.followers.add(self.request.user)
obj.save()

send_notification([obj], self.request.user, [{}], Notification.Type.FOLLOW, f'/profile/{self.request.user.username}')
self.request.user.save()

return obj
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2022-03-20 21:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('notifications', '0004_notification_link'),
]

operations = [
migrations.AlterField(
model_name='notification',
name='type',
field=models.PositiveSmallIntegerField(choices=[(1, 'Bookmark'), (2, 'Clap'), (3, 'Comment'), (4, 'Follow'), (5, 'Following Project')]),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2022-03-20 21:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('notifications', '0005_alter_notification_type'),
]

operations = [
migrations.AddField(
model_name='notification',
name='message',
field=models.CharField(blank=True, max_length=255, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2022-03-22 01:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('notifications', '0006_notification_message'),
]

operations = [
migrations.AlterField(
model_name='notification',
name='link',
field=models.CharField(blank=True, max_length=1000, null=True),
),
]
10 changes: 6 additions & 4 deletions zubhub_backend/zubhub/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

class Notification(models.Model):
class Type(models.IntegerChoices):
CLAP = 1
COMMENT = 2
SAVE = 3
BOOKMARK = 1
CLAP = 2
COMMENT = 3
FOLLOW = 4
FOLLOWING_PROJECT = 5

id = models.UUIDField(
primary_key=True, default=uuid.uuid4, editable=False, unique=True)
Expand All @@ -21,6 +22,7 @@ class Type(models.IntegerChoices):
Creator, on_delete=models.CASCADE, null=True, related_name="notification_recipient", blank=True)
source = models.ForeignKey(
Creator, on_delete=models.CASCADE, null=True, related_name="notification_source", blank=True)
link = models.URLField(max_length=1000)
message = models.CharField(max_length=255, blank=True, null=True)
link = models.CharField(max_length=1000, blank=True, null=True)
viewed = models.BooleanField(default=False)
date = models.DateTimeField(default=timezone.now)
5 changes: 2 additions & 3 deletions zubhub_backend/zubhub/notifications/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from notifications.models import Notification
from django.utils import timezone


def push_notification(recipient, source, type, link):
return Notification.objects.create(recipient=recipient, source=source, type=type, link=link)
def push_notification(recipient, source, notification_type, message, link):
return Notification.objects.create(recipient=recipient, source=source, type=notification_type, message=message, link=link)
Loading

0 comments on commit 8d87b41

Please sign in to comment.