Skip to content

Commit 6f6a06f

Browse files
committed
newsletter: add initial support and backends
1 parent 6849ec7 commit 6f6a06f

File tree

14 files changed

+231
-1
lines changed

14 files changed

+231
-1
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Version 8.13 (Unreleased)
1414
- Fixed bug where workflow notification subject may not include a custom email prefix.
1515
- Added configurable subject templates for individual alert emails (`mail:subject_template` option).
1616
- Added data migration to populate ReleaseProject.new_groups
17+
- Added support for managing newsletter subscriptions with Sentry.io
1718

1819
Schema Changes
1920
~~~~~~~~~~~~~~

src/sentry/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def get_instance(attribute, options, dangerous=()):
5151
nodestore = get_instance('SENTRY_NODESTORE', settings.SENTRY_NODESTORE_OPTIONS)
5252
ratelimiter = get_instance('SENTRY_RATELIMITER', settings.SENTRY_RATELIMITER_OPTIONS)
5353
search = get_instance('SENTRY_SEARCH', settings.SENTRY_SEARCH_OPTIONS)
54+
newsletter = get_instance('SENTRY_NEWSLETTER', settings.SENTRY_NEWSLETTER_OPTIONS)
5455

5556
from sentry.tsdb.dummy import DummyTSDB
5657
tsdb = get_instance('SENTRY_TSDB', settings.SENTRY_TSDB_OPTIONS, (DummyTSDB,))

src/sentry/conf/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,9 @@ def create_partitioned_queues(name):
890890
SENTRY_TSDB = 'sentry.tsdb.dummy.DummyTSDB'
891891
SENTRY_TSDB_OPTIONS = {}
892892

893+
SENTRY_NEWSLETTER = 'sentry.newsletter.base.Newsletter'
894+
SENTRY_NEWSLETTER_OPTIONS = {}
895+
893896
# rollups must be ordered from highest granularity to lowest
894897
SENTRY_TSDB_ROLLUPS = (
895898
# (time in seconds, samples to keep)

src/sentry/models/useremail.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def set_hash(self):
4141
def hash_is_valid(self):
4242
return self.validation_hash and self.date_hash_added > timezone.now() - timedelta(hours=48)
4343

44+
def is_primary(self):
45+
return self.user.email == self.email
46+
4447
@classmethod
4548
def get_primary_email(self, user):
4649
return UserEmail.objects.get_or_create(

src/sentry/newsletter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from __future__ import absolute_import

src/sentry/newsletter/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import absolute_import
2+
3+
4+
class Newsletter(object):
5+
DEFAULT_LIST_ID = 1
6+
7+
enabled = False
8+
9+
def is_enabled(self):
10+
return self.enabled
11+
12+
def get_subscriptions(self, user):
13+
return None
14+
15+
def update_subscription(self, user, **kwargs):
16+
return None
17+
18+
def create_or_update_subscription(self, user, **kwargs):
19+
kwargs['create'] = True
20+
return self.update_subscription(user, **kwargs)

src/sentry/newsletter/remote.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import absolute_import
2+
3+
import logging
4+
from uuid import uuid4
5+
from hashlib import sha1
6+
7+
from sentry.http import safe_urlopen
8+
from sentry.newsletter.base import Newsletter
9+
from sentry.utils import json
10+
11+
logger = logging.getLogger('sentry.newsletter')
12+
13+
14+
def get_install_id():
15+
from sentry import options
16+
install_id = options.get('sentry:install-id')
17+
if not install_id:
18+
install_id = sha1(uuid4().bytes).hexdigest()
19+
options.set('sentry:install-id', install_id)
20+
return install_id
21+
22+
23+
class RemoteNewsletter(Newsletter):
24+
enabled = True
25+
endpoint = 'https://sentry.io/remote/newsletter/subscription/'
26+
27+
def __init__(self, endpoint=None):
28+
if endpoint is not None:
29+
self.endpoint = endpoint
30+
31+
def update_subscription(self, user, **kwargs):
32+
kwargs['user_id'] = user.id
33+
kwargs['email'] = user.email
34+
kwargs['referral'] = 'onpremise'
35+
kwargs['install_id'] = get_install_id()
36+
try:
37+
response = safe_urlopen(
38+
self.endpoint,
39+
method='POST',
40+
headers={'Content-Type': 'application/json'},
41+
data=json.dumps(kwargs),
42+
)
43+
response.raise_for_status()
44+
return response.json()
45+
except Exception:
46+
logger.exception('update.failed')
47+
return None
48+
49+
def get_subscriptions(self, user):
50+
try:
51+
response = safe_urlopen(
52+
self.endpoint,
53+
method='GET',
54+
params={
55+
'user_id': user.id,
56+
'install_id': get_install_id(),
57+
},
58+
)
59+
response.raise_for_status()
60+
return response.json()
61+
except Exception:
62+
logger.exception('fetch.failed')
63+
return None

src/sentry/receivers/useremail.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from django.db import IntegrityError
44
from django.db.models.signals import post_save
5+
from django.utils import timezone
56

67
from sentry.models import User, UserEmail
8+
from sentry.signals import email_verified
79

810

911
def create_user_email(instance, created, **kwargs):
@@ -19,3 +21,20 @@ def create_user_email(instance, created, **kwargs):
1921
dispatch_uid="create_user_email",
2022
weak=False
2123
)
24+
25+
26+
@email_verified.connect(weak=False)
27+
def verify_newsletter_subscription(sender, **kwargs):
28+
from sentry.app import newsletter
29+
30+
if not newsletter.enabled:
31+
return
32+
33+
if not sender.is_primary():
34+
return
35+
36+
newsletter.update_subscription(
37+
sender.user,
38+
verified=True,
39+
verified_date=timezone.now(),
40+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{% extends "sentry/bases/account.html" %}
2+
3+
{% load crispy_forms_tags %}
4+
{% load i18n %}
5+
{% load sentry_auth %}
6+
{% load sentry_helpers %}
7+
8+
{% block title %}{% trans "Subscriptions" %} | {{ block.super }}{% endblock %}
9+
10+
{% block main %}
11+
{% if not email.is_verified %}
12+
<div class="alert alert-warning alert-block">
13+
{% trans "Your email address has not been verified. " %}
14+
<form action="{% url 'sentry-account-confirm-email-send' %}" method="post" class="email-alert-button">
15+
{% csrf_token %}
16+
<input type="hidden" name="email" value="{{ email.email }}">
17+
<button type="submit" name="primary-email" class="btn-link">{% trans "Resend Verification Email." %}</button>
18+
</form>
19+
</div>
20+
{% endif %}
21+
22+
<legend class="m-t-0">Subscriptions</legend>
23+
{% for subscription in subscriptions.subscriptions %}
24+
<div class="row">
25+
<div class="col-md-9">
26+
<h5>{{ subscription.list_name }}</h5>
27+
</div>
28+
<div class="col-md-3 align-right" style="padding-right: 25px">
29+
<div data-list-id="{{ subscription.list_id }}" class="switch switch-lg{% if subscription.subscribed %} switch-on{% endif %}" role="checkbox">
30+
<span class="switch-toggle"></span>
31+
</div>
32+
</div>
33+
</div>
34+
{% endfor %}
35+
36+
<script>
37+
$('div.switch').click(function(){
38+
var $e = $(this);
39+
$e.toggleClass('switch-on');
40+
$.post('{% url 'sentry-account-settings-subscriptions' %}', {
41+
'subscribed': $e.hasClass('switch-on') ? '1' : '0',
42+
'listId': $e.data('list-id')
43+
});
44+
});
45+
</script>
46+
{% endblock %}

src/sentry/templates/sentry/bases/account.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ <h4 class="m-b-1">My Settings</h4>
4141
<li{% if page == 'identities' %} class="active"{% endif %}><a href="{% url 'sentry-account-settings-identities' %}">{% trans "Identities" %}</a></li>
4242
{% endif %}
4343
<li{% if page == 'security' %} class="active"{% endif %}><a href="{% url 'sentry-account-security' %}">{% trans "Security" %}</a></li>
44+
{% if has_newsletters %}
45+
<li{% if page == 'subscriptions' %} class="active"{% endif %}><a href="{% url 'sentry-account-settings-subscriptions' %}">{% trans "Subscriptions" %}</a></li>
46+
{% endif %}
4447
</ul>
4548
{% endblock %}
4649

0 commit comments

Comments
 (0)