Slide 1

Slide 1 text

Mailing and Notifications Building Blocks

Slide 2

Slide 2 text

PROBLEMS

Slide 3

Slide 3 text

Creating the email No way of seeing how an email looks like after being converted into a template. No way of testing how it will look like in different email clients.

Slide 4

Slide 4 text

Sending the email PM cannot test that emails are being sent. Developer’s email tools are poor.

Slide 5

Slide 5 text

“...whatever you do, don’t congratulate yourself too much, or berate yourself neither...”

Slide 6

Slide 6 text

A CLOSER LOOK AT send_notify

Slide 7

Slide 7 text

Code smells if language == 'es': notification_remove_msg = 'No...' else: notification_remove_msg = 'Don't...'

Slide 8

Slide 8 text

Code smells if language == 'es': notification_remove_msg = 'No...' elif language == 'de': notification_remove_msg = 'No...' elif language == 'pt': notification_remove_msg = 'No...' else: notification_remove_msg = "Don't..."

Slide 9

Slide 9 text

Inaccurate logs if not self.user.email: if log_send: logging.info('to email: ' + str(self.user.pk)) return

Slide 10

Slide 10 text

Hardcoding message = mail.EmailMessage( sender='App')

Slide 11

Slide 11 text

Not DRY if log_send: ... x3

Slide 12

Slide 12 text

DRY Don’t Repeat Yourself

Slide 13

Slide 13 text

Can’t be extended without changing or rewriting the entire code.

Slide 14

Slide 14 text

Dependencies send_notify() depends on: 1.global settings.DEBUG 2.recipient ending in ‘@app.com’ 3.parameter force_send 4.user is_active

Slide 15

Slide 15 text

Dependencies not documented def send_notify(self, notify, params, force_send=False, log_send=True): """Send a notification to user Args: notify: type of notification params: data to fill templates """

Slide 16

Slide 16 text

SOLUTIONS

Slide 17

Slide 17 text

Unix philosophy “...emphasizes building short, simple, clear, modular and extendable code...”

Slide 18

Slide 18 text

Single Responsibility Open-closed Liskov substitution Interface segregation Dependency inversion http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)

Slide 19

Slide 19 text

NOTIFICATIONS

Slide 20

Slide 20 text

Language Subject Plain Text body HTML body Subject, Plain Text body and HTML body are sort of templates.

Slide 21

Slide 21 text

Basic operations Load the notification template named name and with language lang from somewhere. Render each notification template (subject, text, html) with the actual data.

Slide 22

Slide 22 text

“Loader” object class Loader(object): def get_subject(self, name, lang): """Get the subject""" def get_html(self, name, lang): """Get the html""" def get_text(self, name, lang): """Get the plain text"""

Slide 23

Slide 23 text

“Template” object class Template(object): def __init__(self, source): pass def render(self, **context): """Put context in template"""

Slide 24

Slide 24 text

Now we need to combine Loader and Template objects in a way that can be configurable.

Slide 25

Slide 25 text

“Environment” object class Environment(object): def __init__(self, loader, template_class): self.loader = loader self.template_class = template_class def get_notification(self, name, lang=None, **context): subject = self.template_class( self.loader.get_subject(name, lang)).render(**context) html = self.template_class( self.loader.get_html(name, lang)).render(**context) text = self.template_class( self.loader.get_text(name, lang)).render(**context) return Notification(name, lang, subject, html, text)

Slide 26

Slide 26 text

Putting all together

Slide 27

Slide 27 text

notifications = Environment( loader=Loader(), template_class=Template)

Slide 28

Slide 28 text

But what if I want to keep my notifications in the database from now on?

Slide 29

Slide 29 text

Custom loaders can be created by extending the base loader.

Slide 30

Slide 30 text

class FileSystemLoader(Loader): SUBJECT_FILENAME = 'subject.txt' HTML_FILENAME = 'body.html' TEXT_FILENAME = 'body.txt' def __init__(self, path): self.path = path def _read(self, name, lang, filename): with open(os.path.join(self.path, name, lang, filename)) as f: return f.read() def get_subject(self, name, lang): return self._read(name, lang, self.SUBJECT_FILENAME) def get_html(self, name, lang): return self._read(name, lang, self.HTML_FILENAME) def get_text(self, name, lang): try: text = self._read(name, lang, self.TEXT_FILENAME) except IOError: text = super().get_text(name, lang) return text

Slide 31

Slide 31 text

class DjangoTemplateLoader(FileSystemLoader): def __init__(self, template_dirs=None): self.template_dirs = template_dirs def _read(self, name, lang, filename): template_name = '/'.join((name, lang, filename)) return load_template_source( template_name, self.template_dirs)[0]

Slide 32

Slide 32 text

No Problem notifications = Environment( loader=DatastoreTemplateLoader(), template_class=DjangoTemplate)

Slide 33

Slide 33 text

But what if I want to start using Django’s template system?

Slide 34

Slide 34 text

Custom templates can be created by extending the base template.

Slide 35

Slide 35 text

class DjangoTemplate(Template): def __init__(self, source): self._template = django.template.Template(source) def render(self, **context): return self._template.render( django.template.Context(context))

Slide 36

Slide 36 text

No Problem notifications = Environment( loader=DatastoreTemplateLoader(), template_class=DjangoTemplate)

Slide 37

Slide 37 text

from .environment import notifications get_notification = notifications.get_notification

Slide 38

Slide 38 text

subject = 'email/%s/%s/subject.txt' % (name, language) message.subject = loader.get_template(subject) .render(Context(params)) body = 'email/%s/%s/body.txt' % (name, language) message.body = loader.get_template(body).render(Context(params)) message.body += notification_remove_msg + ' ' + profile_notification_url html = 'email/%s/%s/body.html' % (name, language) message.html = loader.get_template(html).render(Context(params)) message.html += '
%s' % (profile_notification_url, notification_remove_msg) “Before”

Slide 39

Slide 39 text

notification = get_notification(name, language, **params) message.subject = notification.subject message.body = notification.text message.html = notification.html “After”

Slide 40

Slide 40 text

MAILING

Slide 41

Slide 41 text

I don’t like App Engine email API because it mixes the way email are sent with the way they are represented.

Slide 42

Slide 42 text

import smtplib from email.mime.text import MIMEText msg = MIMEText('Message') msg['Subject'] = 'The subject' msg['From'] = me msg['To'] = you s = smtplib.SMTP('localhost') s.sendmail(me, [you], msg.as_string()) s.quit() I prefer the approach of the standard library

Slide 43

Slide 43 text

But we are already using App Engine API in many places.

Slide 44

Slide 44 text

Let’s try to refactor... from google.appengine.api import mail message = mail.EmailMessage(sender='...') message.to = self.user.email message.subject = '...' message.body = '...' message.html = '...' message.send()

Slide 45

Slide 45 text

...in order to support Different kinds of messages: The actual that sends the email. One that logs the email. One that does nothing.

Slide 46

Slide 46 text

Refactor “...restructuring an existing body of code, altering its internal structure without changing its external behavior.”

Slide 47

Slide 47 text

EMAIL_MESSAGE_SETTING = 'EMAIL_MESSAGE_CLASS' def EmailMessage(message_class=None, **kwargs): if message_class is None: if not hasattr(settings, EMAIL_MESSAGE_SETTING): raise ImproperlyConfigured message_class = getattr(settings, EMAIL_MESSAGE_SETTING) if isinstance(message_class, basestring): ... return message_class(**kwargs)

Slide 48

Slide 48 text

class Message(MailerDelegator): pass class LoggingMessage(MailerDelegator): name = 'app.mail' logger = logging.getLogger(name) level = logging.DEBUG def send(self, *args, **kwargs): self.logger.log(self.level, self) Pick your “Message” class DummyMessage(MailerDelegator): def send(self, *args, **kwargs): pass

Slide 49

Slide 49 text

EMAIL_MESSAGE_CLASS = 'app.mail.LoggingMessage' Configuration

Slide 50

Slide 50 text

from app import mail msg = mail.EmailMessage() msg.send() # => Logs the message

Slide 51

Slide 51 text

class MailerDelegator(object): __slots__ = '_mailer' mailer_class = google.appengine.api.mail.EmailMessage def __init__(self, *args, **kwargs): object.__setattr__(self, '_mailer', self.mailer_class(*args, **kwargs)) def __setattr__(self, name, value): if name in self._mailer.PROPERTIES: setattr(self._mailer, name, value) else: super().__setattr__(name, value) def __getattr__(self, name): if not hasattr(self._mailer, name): raise AttributeError return getattr(self._mailer, name) def __str__(self): return self.to_mime_message().as_string()

Slide 52

Slide 52 text

Something else?...

Slide 53

Slide 53 text

Of course! Where are the docs dude!?

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

PROBLEMS SOLVED

Slide 57

Slide 57 text

Creating the email (unsolved) No way of seeing how an email looks like after being converted into a template. (unsolved) No way of testing how it will look like in different email clients.

Slide 58

Slide 58 text

Sending the email (unsolved) PM cannot test that emails are being sent. (solved) Developers email tools are poor.

Slide 59

Slide 59 text

75% still unsolved!

Slide 60

Slide 60 text

Having better tools for doing the job is more a middle/long term investment.

Slide 61

Slide 61 text

Choose Wisely

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

Recommended

Slide 64

Slide 64 text

raise SystemExit