From 93c9806990bf40e87beae05690eb9a12bba92eb6 Mon Sep 17 00:00:00 2001 From: adriant Date: Tue, 28 Jul 2015 12:33:17 +1200 Subject: [PATCH] reworking of email sending Emails now send at 3 different stages. Now we have two different types of email templates, one html, one plaintext. The templates, and reply email can be set actionview, per stage. Change-Id: I3a37959c547232c8f0df60a69953cc4ac7441d8e --- MANIFEST.in | 2 +- conf/conf.yaml | 56 +++++- setup.py | 2 +- stacktask/api_v1/migrations/0001_initial.py | 1 + stacktask/api_v1/models.py | 3 + stacktask/api_v1/templates/completed.txt | 11 ++ stacktask/api_v1/templates/initial.txt | 13 ++ stacktask/api_v1/views.py | 209 ++++++++++++++++---- stacktask/base/models.py | 10 +- stacktask/base/user_store.py | 2 +- stacktask/settings.py | 3 +- 11 files changed, 256 insertions(+), 56 deletions(-) create mode 100644 stacktask/api_v1/templates/completed.txt create mode 100644 stacktask/api_v1/templates/initial.txt diff --git a/MANIFEST.in b/MANIFEST.in index 47aa52b..51d607e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.md -graft stacktask/*/templates +include stacktask/*/templates/* diff --git a/conf/conf.yaml b/conf/conf.yaml index 51b5354..22ed3a6 100644 --- a/conf/conf.yaml +++ b/conf/conf.yaml @@ -48,19 +48,65 @@ DEFAULT_REGION: RegionOne # Additonal actions for views: # - The order of the actions matters. These will run after the default action, # in the given order. -API_ACTIONS: +ACTIONVIEW_SETTINGS: CreateProject: - - AddAdminToProject - - DefaultProjectResources + actions: + - AddAdminToProject + - DefaultProjectResources + emails: + initial: + subject: Initial Confirmation + reply: no-reply@example.com + template: initial.txt + html_template: initial.txt + # If the related actions 'can' send a token, + # this field should here. + token: + subject: Your Token + reply: no-reply@example.com + template: token.txt + html_template: token.txt + completed: + subject: Registration completed + reply: no-reply@example.com + template: completed.txt + html_template: completed.txt + AttachUser: + emails: + # To not send this email, set the value to null, + # or don't have the field there at all. + initial: null + token: + subject: Your Token + reply: no-reply@example.com + template: token.txt + html_template: token.txt + completed: + subject: Registration completed + reply: no-reply@example.com + template: completed.txt + html_template: completed.txt + ResetPassword: + emails: + token: + subject: Your Token + reply: no-reply@example.com + template: token.txt + html_template: token.txt + completed: + subject: Registration completed + reply: no-reply@example.com + template: completed.txt + html_template: completed.txt # Action settings: ACTION_SETTINGS: DefaultProjectResources: - "RegionOne": + RegionOne: network_name: somenetwork subnet_name: somesubnet router_name: somerouter - public_network: 83559fa7-0a67-4716-94b9-10596e3ed1e6 + public_network: 3cb50d61-5bce-4c03-96e6-8e262e12bb35 DNS_NAMESERVERS: - 193.168.1.2 - 193.168.1.3 diff --git a/setup.py b/setup.py index 194becd..85c6b62 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name='stacktask', - version='0.1.0a3', + version='0.1.0a4', description='A user registration service for openstack.', long_description=( 'A registration service to sit alongside keystone and ' + diff --git a/stacktask/api_v1/migrations/0001_initial.py b/stacktask/api_v1/migrations/0001_initial.py index 9b287bd..35658a0 100644 --- a/stacktask/api_v1/migrations/0001_initial.py +++ b/stacktask/api_v1/migrations/0001_initial.py @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ('uuid', models.CharField(default=stacktask.api_v1.models.hex_uuid, max_length=200, serialize=False, primary_key=True)), ('reg_ip', models.GenericIPAddressField()), ('keystone_user', jsonfield.fields.JSONField(default={})), + ('action_view', models.CharField(max_length=200)), ('action_notes', jsonfield.fields.JSONField(default={})), ('approved', models.BooleanField(default=False)), ('completed', models.BooleanField(default=False)), diff --git a/stacktask/api_v1/models.py b/stacktask/api_v1/models.py index a671ded..281d537 100644 --- a/stacktask/api_v1/models.py +++ b/stacktask/api_v1/models.py @@ -34,6 +34,9 @@ class Registration(models.Model): reg_ip = models.GenericIPAddressField() keystone_user = JSONField(default={}) + # which ActionView initiated this + action_view = models.CharField(max_length=200) + # Effectively a log of what the actions are doing. action_notes = JSONField(default={}) diff --git a/stacktask/api_v1/templates/completed.txt b/stacktask/api_v1/templates/completed.txt new file mode 100644 index 0000000..ac68fb8 --- /dev/null +++ b/stacktask/api_v1/templates/completed.txt @@ -0,0 +1,11 @@ +Hello, + +Your registration has been completed. + +The actions you had requested are: +{% for action in actions %} +- {{ action }} +{% endfor %} + +Thank you for using our service. + diff --git a/stacktask/api_v1/templates/initial.txt b/stacktask/api_v1/templates/initial.txt new file mode 100644 index 0000000..ea5e625 --- /dev/null +++ b/stacktask/api_v1/templates/initial.txt @@ -0,0 +1,13 @@ +Hello, + +Your registration is in our system and now waiting approval. + +The actions you had requested are: +{% for action in actions %} +- {{ action }} +{% endfor %} + +Once someone has approved your registration you will be emailed an update. + +Thank you for using our service. + diff --git a/stacktask/api_v1/views.py b/stacktask/api_v1/views.py index 21294eb..bf63097 100644 --- a/stacktask/api_v1/views.py +++ b/stacktask/api_v1/views.py @@ -80,44 +80,52 @@ def create_token(registration): return token -def email_token(registration, token): +def send_email(registration, email_conf, token=None): + if email_conf: + template = loader.get_template(email_conf['template']) + html_template = loader.get_template(email_conf['html_template']) - emails = set() - actions = [] - for action in registration.actions: - act = action.get_action() - if act.need_token: - emails.add(act.token_email()) - actions.append(unicode(act)) + emails = set() + actions = [] + for action in registration.actions: + act = action.get_action() + email = act.get_email() + if email: + emails.add(email) + actions.append(unicode(act)) - if len(emails) > 1: - notes = { - 'notes': - (("Error: Unable to send token, More than one email for" + - " registration: %s") % registration.uuid) - } - create_notification(registration, notes) - # TODO(adriant): raise some error? - # and surround calls to this function with try/except + if len(emails) > 1: + notes = { + 'notes': + (("Error: Unable to send token, More than one email for" + + " registration: %s") % registration.uuid) + } + create_notification(registration, notes) + return + # TODO(adriant): raise some error? + # and surround calls to this function with try/except - context = {'actions': actions, 'token': token.token} + if token: + context = {'registration': registration, 'actions': actions, + 'token': token.token} + else: + context = {'registration': registration, 'actions': actions} - email_template = loader.get_template("token.txt") - - try: - message = email_template.render(Context(context)) - send_mail( - 'Your token', message, 'no-reply@example.com', - [emails.pop()], fail_silently=False) - except SMTPException as e: - notes = { - 'notes': - ("Error: '%s' while emailing token for registration: %s" % - (e, registration.uuid)) - } - create_notification(registration, notes) - # TODO(adriant): raise some error? - # and surround calls to this function with try/except + try: + message = template.render(Context(context)) + html_message = html_template.render(Context(context)) + send_mail( + email_conf['subject'], message, email_conf['reply'], + [emails.pop()], fail_silently=False, html_message=html_message) + except SMTPException as e: + notes = { + 'notes': + ("Error: '%s' while emailing token for registration: %s" % + (e, registration.uuid)) + } + create_notification(registration, notes) + # TODO(adriant): raise some error? + # and surround calls to this function with try/except def create_notification(registration, notes): @@ -379,8 +387,38 @@ class RegistrationDetail(APIViewWithLogger): registration.save() if need_token: token = create_token(registration) - email_token(registration, token) - return Response({'notes': ['created token']}, status=200) + try: + class_conf = settings.ACTIONVIEW_SETTINGS[ + registration.action_view] + + # will throw a key error if the token template has not + # been specified + email_conf = class_conf['emails']['token'] + send_email(registration, email_conf, token) + return Response({'notes': ['created token']}, + status=200) + except KeyError as e: + notes = { + 'errors': + [("Error: '%s' while sending " + + "token. See registration " + + "itself for details.") % e], + 'registration': registration.uuid + } + create_notification(registration, notes) + + import traceback + trace = traceback.format_exc() + self.logger.critical(("(%s) - Exception escaped!" + + " %s\n Trace: \n%s") % + (timezone.now(), e, trace)) + + response_dict = { + 'errors': + ["Error: Something went wrong on the " + + "server. It will be looked into shortly."] + } + return Response(response_dict, status=500) else: for action in actions: try: @@ -406,6 +444,14 @@ class RegistrationDetail(APIViewWithLogger): registration.completed = True registration.completed_on = timezone.now() registration.save() + + # Sending confirmation email: + class_conf = settings.ACTIONVIEW_SETTINGS.get( + registration.action_view, {}) + email_conf = class_conf.get( + 'emails', {}).get('completed', None) + send_email(registration, email_conf) + return Response( {'notes': "Registration completed successfully."}, status=200) @@ -458,7 +504,36 @@ class TokenList(APIViewWithLogger): token.delete() token = create_token(registration) - email_token(registration, token) + try: + class_conf = settings.ACTIONVIEW_SETTINGS[ + registration.action_view] + + # will throw a key error if the token template has not + # been specified + email_conf = class_conf['emails']['token'] + send_email(registration, email_conf, token) + except KeyError as e: + notes = { + 'errors': + [("Error: '%s' while sending " + + "token. See registration " + + "itself for details.") % e], + 'registration': registration.uuid + } + create_notification(registration, notes) + + import traceback + trace = traceback.format_exc() + self.logger.critical(("(%s) - Exception escaped!" + + " %s\n Trace: \n%s") % + (timezone.now(), e, trace)) + + response_dict = { + 'errors': + ["Error: Something went wrong on the " + + "server. It will be looked into shortly."] + } + return Response(response_dict, status=500) return Response( {'notes': ['Token reissued.']}, status=200) @@ -584,6 +659,13 @@ class TokenDetail(APIViewWithLogger): token.registration.save() token.delete() + # Sending confirmation email: + class_conf = settings.ACTIONVIEW_SETTINGS.get( + token.registration.action_view, {}) + email_conf = class_conf.get( + 'emails', {}).get('completed', None) + send_email(token.registration, email_conf) + return Response( {'notes': "Token submitted successfully."}, status=200) @@ -630,9 +712,12 @@ class ActionView(APIViewWithLogger): function on all the actions. """ + class_conf = settings.ACTIONVIEW_SETTINGS.get(self.__class__.__name__, + {}) + actions = [self.default_action, ] - actions += settings.API_ACTIONS.get(self.__class__.__name__, []) + actions += class_conf.get('actions', []) act_list = [] @@ -658,7 +743,8 @@ class ActionView(APIViewWithLogger): keystone_user = request.keystone_user registration = Registration.objects.create( - reg_ip=ip_addr, keystone_user=keystone_user) + reg_ip=ip_addr, keystone_user=keystone_user, + action_view=self.__class__.__name__) registration.save() for i, act in enumerate(act_list): @@ -695,6 +781,10 @@ class ActionView(APIViewWithLogger): } return response_dict + # send initial conformation email: + email_conf = class_conf.get('emails', {}).get('initial', None) + send_email(registration, email_conf) + return {'registration': registration} else: errors = {} @@ -759,8 +849,38 @@ class ActionView(APIViewWithLogger): if valid: if need_token: token = create_token(registration) - email_token(registration, token) - return Response({'notes': ['created token']}, status=200) + try: + class_conf = settings.ACTIONVIEW_SETTINGS[ + self.__class__.__name__] + + # will throw a key error if the token template has not + # been specified + email_conf = class_conf['emails']['token'] + send_email(registration, email_conf, token) + return Response({'notes': ['created token']}, + status=200) + except KeyError as e: + notes = { + 'errors': + [("Error: '%s' while sending " + + "token. See registration " + + "itself for details.") % e], + 'registration': registration.uuid + } + create_notification(registration, notes) + + import traceback + trace = traceback.format_exc() + self.logger.critical(("(%s) - Exception escaped!" + + " %s\n Trace: \n%s") % + (timezone.now(), e, trace)) + + response_dict = { + 'errors': + ["Error: Something went wrong on the " + + "server. It will be looked into shortly."] + } + return Response(response_dict, status=500) else: for action in actions: try: @@ -791,6 +911,13 @@ class ActionView(APIViewWithLogger): registration.completed = True registration.completed_on = timezone.now() registration.save() + + # Sending confirmation email: + class_conf = settings.ACTIONVIEW_SETTINGS.get( + self.__class__.__name__, {}) + email_conf = class_conf.get( + 'emails', {}).get('completed', None) + send_email(registration, email_conf) return Response( {'notes': "Registration completed successfully."}, status=200) diff --git a/stacktask/base/models.py b/stacktask/base/models.py index 99584dd..ab99afe 100644 --- a/stacktask/base/models.py +++ b/stacktask/base/models.py @@ -128,11 +128,11 @@ class BaseAction(object): def need_token(self): return self.action.need_token - def token_email(self): - return self._token_email() + def get_email(self): + return self._get_email() - def _token_email(self): - raise NotImplementedError + def _get_email(self): + return None def get_cache(self, key): return self.action.cache.get(key, None) @@ -191,7 +191,7 @@ class UserAction(BaseAction): else: super(UserAction, self).__init__(*args, **kwargs) - def _token_email(self): + def _get_email(self): return self.email diff --git a/stacktask/base/user_store.py b/stacktask/base/user_store.py index 20b5fa8..5974845 100644 --- a/stacktask/base/user_store.py +++ b/stacktask/base/user_store.py @@ -59,7 +59,7 @@ class IdentityManager(object): return role def get_roles(self, user, project): - return self.ks.roles.roles_for_user(user, tenant=project) + return self.ks_client.roles.roles_for_user(user, tenant=project) def add_user_role(self, user, role, project_id): self.ks_client.roles.add_user_role(user, role, project_id) diff --git a/stacktask/settings.py b/stacktask/settings.py index f9dd3a2..31a5d9b 100644 --- a/stacktask/settings.py +++ b/stacktask/settings.py @@ -118,8 +118,7 @@ DEFAULT_REGION = CONFIG['DEFAULT_REGION'] # Additonal actions for views: # - The order of the actions matters. These will run after the default action, # in the given order. -API_ACTIONS = {'CreateProject': ['AddAdminToProject', - 'DefaultProjectResources']} +ACTIONVIEW_SETTINGS = CONFIG['ACTIONVIEW_SETTINGS'] ACTION_SETTINGS = CONFIG['ACTION_SETTINGS']