Skip to content

Commit

Permalink
adding notifications and foundations for branded emails
Browse files Browse the repository at this point in the history
  • Loading branch information
Sascha Dobbelaere committed Nov 28, 2023
1 parent 5016320 commit 27f2661
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 8 deletions.
58 changes: 58 additions & 0 deletions OneSila/notifications/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.conf import settings
from django.utils.html import strip_tags
from django.template.loader import render_to_string

from premailer import transform
from collections import namedtuple
from get_absolute_url.helpers import generate_absolute_url

import re


EmailAttachment = namedtuple('EmailAttachment', 'name, content')


def textify(html):
# Remove html tags and continuous whitespaces
text_only = re.sub('[ \t]+', ' ', strip_tags(html))
# Strip single spaces in the beginning of each line
return text_only.replace('\n ', '\n').strip()


def send_branded_mail(subject, html, to_email, from_email=None, fail_silently=True, attachment=None, **kwargs):
'''
Send a branded email with all signatures in place.
'''

if not from_email:
from_email = settings.DEFAULT_FROM_EMAIL

text = textify(html)
html = transform(html, base_url=generate_absolute_url())

mail = EmailMultiAlternatives(subject, text, settings.EMAIL_HOST_USER, [to_email])
mail.attach_alternative(html, "text/html")

if attachment:
if not isinstance(attachment, EmailAttachment):
raise ValueError(f'Attachment should be an EmailAttachment instance, not {type(attachment)}')

extension = attachment.name.split('.')[-1]

if extension in ['xlsx']:
content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
elif extension in ['pdf']:
content_type = 'application/pdf'
else:
raise ValueError('Unkown content type.')

mail.attach(attachment.name, attachment.content, content_type)

# Lets override fail_silently for dev-env
if settings.DEBUG:
fail_silently = False

return mail.send(fail_silently=fail_silently)

# return send_mail(subject, text, from_email, [to_email], html_message=html, fail_silently=fail_silently, **kwargs)
4 changes: 1 addition & 3 deletions OneSila/notifications/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
from django.db import models

# Create your models here.
from core import models
17 changes: 17 additions & 0 deletions OneSila/notifications/receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.dispatch import receiver
from core.models.multi_tenant import MultiTenantUser
from core.signals import registered, invite_sent, \
invite_accepted, disabled, enabled

from notifications.flows.email import send_welcome_email_flow, \
send_user_invite_email_flow


@receiver(registered, sender=MultiTenantUser)
def notifications__email__welcome(sender, instance, **kwargs):
send_welcome_email_flow(user=instance)


@receiver(invite_sent, sender=MultiTenantUser)
def notifications__email__invite(sender, instance, **kwargs):
send_user_invite_email_flow(user=instance)
18 changes: 14 additions & 4 deletions docs/conventions/structure_and_naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ Tasks are reserved to wrap action-classes and expose them to Huey, or any other
__Construction steps:__

```python
from .flows import SyncStockFlow
from .flows import sync_stock_flow

def shopware6_local__tasks__sync_stock(*, shopware_product):
SyncStockFlow(shopware_product=shopware_product).flow()
sync_stock_flow(shopware_product=shopware_product)

@db_task(cronjob(day='*'))
@db_task(cronjob(hour='4'))
def shopware6_local__tasks__sync_stock__cronjob(*, shopware_product):
SyncStockFlow(shopware_product=shopware_product).flow()
sync_stock_flow(shopware_product=shopware_product)
```

__Task Naming conventions__
Expand All @@ -98,7 +98,7 @@ and

## Flows

Flows are classes that decide the work. They are the decision makers and will trigger the factories according to a set of conditions decided internally in the Flow class.
Flows are classes or methods that decide the work. They are the decision makers and will trigger the factories according to a set of conditions decided internally in the Flow class.

All flows go In either:

Expand Down Expand Up @@ -127,6 +127,16 @@ class StockSyncFlow:
self.identify_product()
```

or in simple cases:

```python
def sync_stock_flow(*, shopware_product):
from .factories import SomeFactory
fac = SomeFactory(shopware_product)
fac.run()
```


__Naming convention__

We won’t be adding the app name in the class. it’s causing too much repetitive typing and causing long, hard to read names. So short, sweet and above all *clear*
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ mkdocs
mkdocs-material
coverage
daphne
premailer
django-dirtyfields
django-cors-headers

# Using fork to ensure we have better authentication error support
strawberry-graphql-django @ git+https://github.com/sdobbelaere/strawberry-graphql-django.git@not_authenticated_bug
django_get_absolute_url @ git+https://git@github.com/TweaveTech/django_get_absolute_url.git@v0.2
django_get_absolute_url @ git+https://git@github.com/TweaveTech/django_get_absolute_url.git@master


# Starlette is used for it CORSMiddleware as referenced
Expand Down

0 comments on commit 27f2661

Please sign in to comment.