Send welcome email to new Cloud subscribers
This commit is contained in:
parent
0b34c5c1c6
commit
c2518e9ae1
@ -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
33
cloud/email.py
Normal 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
39
cloud/eve_hooks.py
Normal 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
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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()));
|
||||
});
|
||||
|
||||
|
||||
|
97
src/templates/emails/layout.pug
Normal file
97
src/templates/emails/layout.pug
Normal 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 %}
|
1
src/templates/emails/layout.txt
Normal file
1
src/templates/emails/layout.txt
Normal file
@ -0,0 +1 @@
|
||||
{% block body %}{% endblock %}
|
72
src/templates/emails/welcome.pug
Normal file
72
src/templates/emails/welcome.pug
Normal 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 %}
|
53
src/templates/emails/welcome.txt
Normal file
53
src/templates/emails/welcome.txt
Normal 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 %}
|
BIN
static/assets/img/email/background_caminandes_3_03.jpg
Normal file
BIN
static/assets/img/email/background_caminandes_3_03.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -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."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user