Send welcome email to new Cloud subscribers

This commit is contained in:
Sybren A. Stüvel 2017-12-21 15:26:23 +01:00
parent 0b34c5c1c6
commit c2518e9ae1
12 changed files with 345 additions and 10 deletions

View File

@ -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():

33
cloud/email.py Normal file
View File

@ -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)

39
cloud/eve_hooks.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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()));
});

View File

@ -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 %}

View File

@ -0,0 +1 @@
{% block body %}{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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."""