From c2518e9ae1f57413efee8a879d419dd759e76360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 21 Dec 2017 15:26:23 +0100 Subject: [PATCH] Send welcome email to new Cloud subscribers --- cloud/__init__.py | 4 +- cloud/email.py | 33 ++++++ cloud/eve_hooks.py | 39 +++++++ cloud/routes.py | 27 ++++- cloud/webhooks.py | 19 ++-- gulpfile.js | 6 ++ src/templates/emails/layout.pug | 97 ++++++++++++++++++ src/templates/emails/layout.txt | 1 + src/templates/emails/welcome.pug | 72 +++++++++++++ src/templates/emails/welcome.txt | 53 ++++++++++ .../img/email/background_caminandes_3_03.jpg | Bin 0 -> 16839 bytes tests/test_webhooks.py | 4 +- 12 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 cloud/email.py create mode 100644 cloud/eve_hooks.py create mode 100644 src/templates/emails/layout.pug create mode 100644 src/templates/emails/layout.txt create mode 100644 src/templates/emails/welcome.pug create mode 100644 src/templates/emails/welcome.txt create mode 100644 static/assets/img/email/background_caminandes_3_03.jpg diff --git a/cloud/__init__.py b/cloud/__init__.py index de05e83..af55054 100644 --- a/cloud/__init__.py +++ b/cloud/__init__.py @@ -85,10 +85,12 @@ class CloudExtension(PillarExtension): } def setup_app(self, app): - from . import routes, webhooks + from . import routes, webhooks, eve_hooks, email routes.setup_app(app) app.register_api_blueprint(webhooks.blueprint, '/webhooks') + eve_hooks.setup_app(app) + email.setup_app(app) def _get_current_cloud(): diff --git a/cloud/email.py b/cloud/email.py new file mode 100644 index 0000000..a997e67 --- /dev/null +++ b/cloud/email.py @@ -0,0 +1,33 @@ +import functools +import logging + +import flask + +from pillar.auth import UserClass + +log = logging.getLogger(__name__) + + +def queue_welcome_mail(user: UserClass): + """Queue a welcome email for execution by Celery.""" + assert user.email + log.info('queueing welcome email to %s', user.email) + + subject = 'Welcome to Blender Cloud' + render = functools.partial(flask.render_template, subject=subject, user=user) + text = render('emails/welcome.txt') + html = render('emails/welcome.html') + + from pillar.celery import email_tasks + email_tasks.send_email.delay(user.full_name, user.email, subject, text, html) + + +def user_subscription_changed(user: UserClass, *, grant_roles: set, revoke_roles: set): + if user.has_cap('subscriber') and 'has_subscription' in grant_roles: + log.info('user %s just got a new subscription', user.email) + queue_welcome_mail(user) + + +def setup_app(app): + from pillar.api.blender_cloud import subscription + subscription.user_subscription_updated.connect(user_subscription_changed) diff --git a/cloud/eve_hooks.py b/cloud/eve_hooks.py new file mode 100644 index 0000000..30b48bf --- /dev/null +++ b/cloud/eve_hooks.py @@ -0,0 +1,39 @@ +import logging +import typing + +from pillar.auth import UserClass + +from . import email + +log = logging.getLogger(__name__) + + +def welcome_new_user(user_doc: dict): + """Sends a welcome email to a new user.""" + + user_email = user_doc.get('email') + if not user_email: + log.warning('user %s has no email address', user_doc.get('_id', '-no-id-')) + return + + # Only send mail to new users when they actually are subscribers. + user = UserClass.construct('', user_doc) + if not (user.has_cap('subscriber') or user.has_cap('can-renew-subscription')): + log.debug('user %s is new, but not a subscriber, so no email for them.', user_email) + return + + email.queue_welcome_mail(user) + + +def welcome_new_users(user_docs: typing.List[dict]): + """Sends a welcome email to new users.""" + + for user_doc in user_docs: + try: + welcome_new_user(user_doc) + except Exception: + log.exception('error sending welcome mail to user %s', user_doc) + + +def setup_app(app): + app.on_inserted_users += welcome_new_users diff --git a/cloud/routes.py b/cloud/routes.py index 3c5d705..db1cdd5 100644 --- a/cloud/routes.py +++ b/cloud/routes.py @@ -1,10 +1,10 @@ import functools -import itertools import json import logging import typing from flask_login import current_user, login_required +import flask from flask import Blueprint, render_template, redirect, session, url_for, abort, flash from pillarsdk import Node, Project, User, exceptions as sdk_exceptions, Group from pillarsdk.exceptions import ResourceNotFound @@ -372,6 +372,31 @@ def privacy(): return render_template('privacy.html') +@blueprint.route('/emails/welcome.send') +@login_required +def emails_welcome_send(): + from cloud import email + email.queue_welcome_mail(current_user) + return f'queued mail to {current_user.email}' + + +@blueprint.route('/emails/welcome.html') +@login_required +def emails_welcome_html(): + return render_template('emails/welcome.html', + subject='Welcome to Blender Cloud', + user=current_user) + + +@blueprint.route('/emails/welcome.txt') +@login_required +def emails_welcome_txt(): + txt = render_template('emails/welcome.txt', + subject='Welcome to Blender Cloud', + user=current_user) + return flask.Response(txt, content_type='text/plain; charset=utf-8') + + def setup_app(app): global _homepage_context cached = app.cache.cached(timeout=300) diff --git a/cloud/webhooks.py b/cloud/webhooks.py index 861c3da..2eeb5a8 100644 --- a/cloud/webhooks.py +++ b/cloud/webhooks.py @@ -126,11 +126,14 @@ def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]: provider='blender-id', full_name=wh_payload['full_name']) - user_doc['roles'] = [subscription.ROLES_BID_TO_PILLAR[r] - for r in wh_payload.get('roles', []) - if r in subscription.ROLES_BID_TO_PILLAR] - + # Figure out the user's eventual roles. These aren't stored in the document yet, + # because that's handled by the badger service. + eventual_roles = [subscription.ROLES_BID_TO_PILLAR[r] + for r in wh_payload.get('roles', []) + if r in subscription.ROLES_BID_TO_PILLAR] user_ob = UserClass.construct('', user_doc) + user_ob.roles = eventual_roles + user_ob.collect_capabilities() create = (user_ob.has_cap('subscriber') or user_ob.has_cap('can-renew-subscription') or current_app.org_manager.user_is_unknown_member(email)) @@ -179,19 +182,23 @@ def user_modified(): return '', 204 # Use direct database updates to change the email and full name. + # Also updates the db_user dict so that local_user below will have + # the updated information. updates = {} if db_user['email'] != payload['email']: my_log.info('User changed email from %s to %s', payload['old_email'], payload['email']) updates['email'] = payload['email'] + db_user['email'] = payload['email'] - if payload['full_name'] != db_user['full_name']: + if db_user['full_name'] != payload['full_name']: my_log.info('User changed full name from %r to %r', - payload['full_name'], db_user['full_name']) + db_user['full_name'], payload['full_name']) if payload['full_name']: updates['full_name'] = payload['full_name'] else: # Fall back to the username when the full name was erased. updates['full_name'] = db_user['username'] + db_user['full_name'] = updates['full_name'] if updates: users_coll = current_app.db('users') diff --git a/gulpfile.js b/gulpfile.js index fbb434b..0245b31 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -55,6 +55,12 @@ gulp.task('templates', function() { })) .pipe(gulp.dest(destination.pug)) .pipe(gulpif(argv.livereload, livereload())); + // TODO(venomgfx): please check why 'gulp watch' doesn't pick up on .txt changes. + gulp.src('src/templates/**/*.txt') + .pipe(gulpif(enabled.failCheck, plumber())) + .pipe(gulpif(enabled.cachify, cache('templating'))) + .pipe(gulp.dest(destination.pug)) + .pipe(gulpif(argv.livereload, livereload())); }); diff --git a/src/templates/emails/layout.pug b/src/templates/emails/layout.pug new file mode 100644 index 0000000..c532471 --- /dev/null +++ b/src/templates/emails/layout.pug @@ -0,0 +1,97 @@ +doctype html +html + head + title {% block title %}{{ subject }}{% endblock %} + style. + @import url('https://fonts.googleapis.com/css?family=Roboto'); + html, body { + font-family: 'Roboto', 'Noto Sans', sans-serif; + font-size: 11pt; + background-color: #eaebec; + color: black; + } + + section, h1 { + max-width: 100%; + width: 522px; + margin: 15px auto; + padding: 10px 25px; + box-shadow: rgba(0,0,0,0.298039) 0px 1px 4px -1px; + } + a:link { + color: #2e99b8; + } + a:link, a:visited { + text-decoration: none; + } + + section { + background-color: white; + border-radius: 2px; + } + section.about, p.ps { + color: #888; + font-size: smaller; + } + section.about a:link, p.ps a:link { + color: #7297ab; + } + + h1 { + text-shadow: 1px 1px 1px rgba(0,0,0,0.5), 0 0 25px rgba(0,0,0,0.6); + color: white; + font-size: 30px; + background: #d5d0cb url('{{ abs_url("static", filename="assets/img/email/background_caminandes_3_03.jpg") }}') no-repeat top center; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + min-height: 120px; + } + h2 { + font-weight: 300; + color: #eb5e28; + position: relative; + } + p { + line-height: 150%; + text-align: justify; + } + p.closing { + margin-top: 2.5em; + } + p.closing a.cloudlogo { + float: right; + } + p.buttons { + text-align: center; + margin: 5ex; + } + + a.button { + text-align: center; + max-width: 30ex; + padding: 5px 30px; + text-decoration: none; + + border-radius: 3px; + border: thin solid #2e99b8; + color: #2e99b8; + background-color: transparent; + text-shadow: none; + transition: color 350ms ease-out, border 150ms ease-in-out, opacity 150ms ease-in-out, background-color 150ms ease-in-out; + } + a.button:hover { + color: white; + background-color: #2e99b8; + } + img#blendercloud { + height: 6em; + width: auto; + margin-right: 0; + margin-top: -0.5em; + margin-bottom: -0.2em; + vertical-align: middle; + } + body + | {% block body %}{% endblock %} diff --git a/src/templates/emails/layout.txt b/src/templates/emails/layout.txt new file mode 100644 index 0000000..3ddfb62 --- /dev/null +++ b/src/templates/emails/layout.txt @@ -0,0 +1 @@ +{% block body %}{% endblock %} diff --git a/src/templates/emails/welcome.pug b/src/templates/emails/welcome.pug new file mode 100644 index 0000000..fa58044 --- /dev/null +++ b/src/templates/emails/welcome.pug @@ -0,0 +1,72 @@ +| {% extends "emails/layout.html" %} +| {% block body %} +| {% set blender_cloud = abs_url('cloud.homepage') %} +h1 Welcome to Blender Cloud +section + h2 Hi {{ user.full_name or user.email }} + + p. + Sybren here. I'm one of the Blender Cloud developers, and I'm very happy to welcome you. The + Blender Animation Studio projects are made possible thanks to you! + #[strong In this email I'll explain where to go and what to see.] + If you want to dive in now, just hit this button: + + p.buttons + a.button(href="{{ blender_cloud }}", target='_blank') Log in and Explore! + + p. + To get the most out of Blender Cloud, be sure to check out + #[a(href="{{ abs_url('cloud.services') }}", target='_blank') the services] we offer, like + accessing our texture library from within Blender using the Cloud add-on, Blender Sync, + Image Sharing, and more: + + p + ul + li #[a(href="{{ abs_url('cloud.courses') }}", target='_blank') Courses] & #[a(href="{{ abs_url('cloud.workshops') }}", target='_blank') Workshops] + li #[a(href="{{ abs_url('projects.view', project_url='characters') }}", target='_blank') Characters] & #[a(href="{{ abs_url('cloud.open_projects') }}", target='_blank') Open Movies] + li #[a(href="{{ abs_url('projects.view', project_url='textures') }}", target='_blank') Textures] & #[a(href="{{ abs_url('projects.view', project_url='hdri') }}", target='_blank') HDRIs] + li #[a(href="{{ abs_url('projects.view', project_url='gallery') }}", target='_blank') Art Gallery] + li #[a(href="{{ abs_url('projects.index') }}", target='_blank') Your own Projects] + + p. + If you have any questions, remarks, ideas, or complaints, you can contact us at + #[a(href='mailto:cloudsupport@blender.org') cloudsupport@blender.org]. We're always + happy to help, and on working days we'll get back to you within a day. + + p. + To update your personal details, such as your email address, password, or full name, please + visit #[a(href="https://www.blender.org/id/") Blender ID]. + + p. + For renewing your subscription and other monetary things, there is the + #[a(href='https://store.blender.org/my-account/subscriptions/') Subscription Overview at the Blender Store]. + + p.closing + a.cloudlogo(href="{{ blender_cloud }}") + img#blendercloud(src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MDMuNSAzODkuMyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTk0LjM3IC0xLjU2MykiPjxwYXRoIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Im0yMjguMSAzMzQuNWMtMzguNzQtOC4zOS03MS4xLTQxLjY0LTc4LjY4LTgwLjg1LTQuNjI2LTIzLjk1IDAuNjcxMS01My42IDEzLjE5LTczLjg1bDUuNTIxLTguOTI1LTkuMy02LjY4MmMtNS4xMTUtMy42NzUtOS4zLTcuMzg1LTkuMy04LjI0NCAwLTEuNTA3IDcuMzcxLTMuMjc1IDMwLjU3LTcuMzM1IDEwLjQyLTEuODI1IDE4LTAuNDY4MiAxOC4wOSAzLjIzNSAwLjAxODMgMC43MzUyIDEuNjA1IDEwLjAzIDMuNTMxIDIwLjY1IDQuMTExIDIyLjY4IDMuMDY1IDI0LjM3LTkuNDU1IDE1LjNsLTguMTAzLTUuODctNC4yMzkgNS4yNjhjLTkuMTU5IDExLjM4LTEyLjU2IDI0LjE5LTEyLjU4IDQ3LjM1LTAuMDEyNyAyMC42OCAwLjIyODYgMjIuMDIgNi4zNTYgMzQuNiAxMS43OCAyNC4yMSAzNS41NyA0My4wNiA2MC4yMyA0Ny43NCA1LjUyOSAxLjA0NyA1My4yMiAxLjgyNSAxMTIgMS44MjUgNTguNzcgMCAxMDYuNS0wLjc3OCAxMTItMS44MjUgMjQuNzUtNC42OTUgNDguNjEtMjMuNjQgNjAuMDgtNDcuNzEgNS4zNTYtMTEuMjQgNi4zNDMtMTUuNTYgNy4wNi0zMC44NiAwLjY5NDYtMTQuODUgMC4xNDk0LTE5Ljc5LTMuMjU1LTI5Ljg1LTExLjE0LTMyLjg1LTM4LjYzLTU1LjU2LTY5LjcyLTU3LjYtMTYuNjEtMS4wOTItMjEuMTMtMy42NTgtMjIuODYtMTMtMy41MjItMTguOTUtMjMuODgtNDUuNS00MS45OS01NC43My0yMi40OS0xMS40Ny01MS4zLTEyLjQ2LTczLjExLTIuNTE1LTM0LjMyIDE1LjY1LTU1LjAxIDUzLjU4LTQ4Ljc3IDg5LjQxIDcuMDQxIDQwLjQzIDQxLjg4IDcwLjE0IDgyLjI3IDcwLjE0IDEwLjQ1IDAgMjkuMDgtMy42OTQgMzQuMTktNi43ODEgMC43MDUzLTAuNDI3My0wLjc4MjMtNS40NDUtMy4zMDYtMTEuMTUtMi41MjQtNS43MDgtNC4xMzItMTEuMTItMy41NzItMTIuMDIgMC41NjAyLTAuOTA0MSA4Ljc4MSAxLjE4MiAxOC4yNyA0LjY0MSAyMy45NSA4LjcyNSAyMy4zOSA4LjQ2NyAyNC41MyAxMS40MyAxLjIyNSAzLjE4OS0xMi42NiA0Mi44OC0xNSA0Mi44OC0wLjg5NzcgMC0zLjk3My00LjM2NS02LjgzMi05LjcwMWwtNS4xOTktOS43MDEtMTAuNyAzLjc4OGMtMTUuMTcgNS4zNjktMzkuMzggNi41NzQtNTQuNjYgMi43MTktMzguNDUtOS43MDEtNjguMDUtMzguOTctNzYuODQtNzUuOTUtOC4wODUtMzQuMDUgMi4wMzMtNjkuMjUgMjcuMi05NC42NSAxOS45My0yMC4xIDQ0Ljc3LTMwLjY4IDcyLjA4LTMwLjY4IDQxLjYzIDAgODIuNDIgMjguMjcgOTUuMjUgNjYuMDJsMy4yNCA5LjUzMSAxMy43NCAyLjMxNWMzOC43MyA2LjUyNyA3MS4zNCAzNy4xNSA4MS4zMyA3Ni4zNyAxNC4wOSA1NS4zNC0yMi4zNSAxMTMuNS03OC41MyAxMjUuNC0xOC42OSAzLjk0MS0yMTYuNSAzLjg1My0yMzQuNy0wLjEwNjd6IiBzdHJva2U9IiM5MmM2ZDQiIHN0cm9rZS13aWR0aD0iNi43OTYiIGZpbGw9IiM5MmM2ZDQiLz48L2c+PC9zdmc+Cg==", + alt="Blender Cloud Logo") + | Warm regards, + p. + Sybren A. Stüvel #[br] and the entire Blender Cloud team + + p.ps. + PS: If you do not want to receive other emails from us, + #[a(href="{{ abs_url('settings.emails') }}") we've got you covered]. + +section.about + h2 About Blender Cloud + + p. + Blender Cloud is the creative hub for your projects, powered by Free and Open Source + software. + + p. + On Blender Cloud you can create and share personal projects, access our texture and HDRI + library, keep track of your production, manage your renders, and much more! + + p. + The Blender Animation Studio projects are made possible thanks to subscriptions to the + #[a(href='{{ blender_cloud }}') Blender Cloud]. Thanks for joining! + + +| {% endblock %} diff --git a/src/templates/emails/welcome.txt b/src/templates/emails/welcome.txt new file mode 100644 index 0000000..10ade8d --- /dev/null +++ b/src/templates/emails/welcome.txt @@ -0,0 +1,53 @@ +{% extends "emails/layout.txt" %} +{% set blender_cloud = abs_url('cloud.homepage') %} +{% block body %}Welcome to Blender Cloud, {{ user.full_name or user.nickname }}! + +Sybren here. I'm one of the Blender Cloud developers, and I'm very +happy to welcome you. The Blender Animation Studio projects are +made possible thanks to you! In this email I'll explain where to +go and what to see. If you want to dive in now, just log in and +explore at: + + {{ blender_cloud }} + +To get the most out of Blender Cloud, be sure to check out the +services we offer at, like accessing our texture library from +within Blender using the Cloud add-on, Blender Sync, and Image +Sharing: + {{ abs_url('cloud.services') }} + + +If you have any questions, remarks, ideas, or complaints, you can +contact us at cloudsupport@blender.org. We're always happy to +help, and on working days we'll get back to you within a day. + +To update your personal details, such as your email address, +password, or full name, please visit Blender ID at: + https://www.blender.org/id/ + +For renewing your subscription and other monetary things, there is +the Subscription Overview at the Blender Store: + https://store.blender.org/my-account/subscriptions/ + +Warm regards, + +Sybren A. Stüvel +and the entire Blender Cloud team + +PS: If you do not want to receive other emails from us, we've got +you covered: {{ abs_url('settings.emails') }} + + + +About Blender Cloud + +Blender Cloud is the creative hub for your projects, powered by +Free and Open Source software. + +On Blender Cloud you can create and share personal projects, +access our texture and HDRI library, keep track of your +production, manage your renders, and much more! + +The Blender Animation Studio projects are made possible thanks to +subscriptions to the Blender Cloud. Thanks for joining! +{% endblock %} diff --git a/static/assets/img/email/background_caminandes_3_03.jpg b/static/assets/img/email/background_caminandes_3_03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c42e589aae3b0452c2782c522b45fbd2d16f123 GIT binary patch literal 16839 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 literal 0 HcmV?d00001 diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 5d07ac5..0598009 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -371,7 +371,7 @@ class UserModifiedUserCreationTest(AbstractWebhookTest): self.assertIsNotNone(new_user) self.assertEqual('new', new_user['username']) self.assertEqual('ကြယ်ဆွတ်', new_user['full_name']) - self.assertEqual(['subscriber', 'has_subscription'], new_user['roles']) + self.assertEqual({'subscriber', 'has_subscription'}, set(new_user['roles'])) def test_create_renewable(self): payload = {'id': 1112333, @@ -423,7 +423,7 @@ class UserModifiedUserCreationTest(AbstractWebhookTest): self.assertIsNotNone(new_user) self.assertEqual('new', new_user['username']) self.assertEqual('new', new_user['full_name']) # defaults to username - self.assertEqual(['subscriber', 'has_subscription'], new_user['roles']) + self.assertEqual({'subscriber', 'has_subscription'}, set(new_user['roles'])) def test_no_create_when_not_subscriber(self): """Don't create local users when they are not subscriber."""