Skip to content

Commit 57b0bd1

Browse files
committed
add new data model for notifications
1 parent 883b530 commit 57b0bd1

26 files changed

+715
-170
lines changed

admin/notifications/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
from osf.models.notifications import NotificationSubscription
1+
from osf.models.notifications import NotificationSubscriptionLegacy
22
from django.db.models import Count
33

44
def delete_selected_notifications(selected_ids):
5-
NotificationSubscription.objects.filter(id__in=selected_ids).delete()
5+
NotificationSubscriptionLegacy.objects.filter(id__in=selected_ids).delete()
66

77
def detect_duplicate_notifications(node_id=None):
8-
query = NotificationSubscription.objects.values('_id').annotate(count=Count('_id')).filter(count__gt=1)
8+
query = NotificationSubscriptionLegacy.objects.values('_id').annotate(count=Count('_id')).filter(count__gt=1)
99
if node_id:
1010
query = query.filter(node_id=node_id)
1111

1212
detailed_duplicates = []
1313
for dup in query:
14-
notifications = NotificationSubscription.objects.filter(
14+
notifications = NotificationSubscriptionLegacy.objects.filter(
1515
_id=dup['_id']
1616
).order_by('created')
1717

admin_tests/notifications/test_views.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import pytest
22
from django.test import RequestFactory
3-
from osf.models import OSFUser, NotificationSubscription, Node
3+
from osf.models import OSFUser, Node
44
from admin.notifications.views import (
55
delete_selected_notifications,
66
detect_duplicate_notifications,
77
)
8+
from osf.models.notifications import NotificationSubscriptionLegacy
89
from tests.base import AdminTestCase
910

1011
pytestmark = pytest.mark.django_db
@@ -18,19 +19,19 @@ def setUp(self):
1819
self.request_factory = RequestFactory()
1920

2021
def test_delete_selected_notifications(self):
21-
notification1 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1')
22-
notification2 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event2')
23-
notification3 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event3')
22+
notification1 = NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event1')
23+
notification2 = NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event2')
24+
notification3 = NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event3')
2425

2526
delete_selected_notifications([notification1.id, notification2.id])
2627

27-
assert not NotificationSubscription.objects.filter(id__in=[notification1.id, notification2.id]).exists()
28-
assert NotificationSubscription.objects.filter(id=notification3.id).exists()
28+
assert not NotificationSubscriptionLegacy.objects.filter(id__in=[notification1.id, notification2.id]).exists()
29+
assert NotificationSubscriptionLegacy.objects.filter(id=notification3.id).exists()
2930

3031
def test_detect_duplicate_notifications(self):
31-
NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1')
32-
NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1')
33-
NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event2')
32+
NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event1')
33+
NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event1')
34+
NotificationSubscriptionLegacy.objects.create(user=self.user, node=self.node, event_name='event2')
3435

3536
duplicates = detect_duplicate_notifications()
3637

api/subscriptions/permissions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from rest_framework import permissions
22

3-
from osf.models.notifications import NotificationSubscription
3+
from osf.models.notifications import NotificationSubscriptionLegacy
44

55

66
class IsSubscriptionOwner(permissions.BasePermission):
77

88
def has_object_permission(self, request, view, obj):
9-
assert isinstance(obj, NotificationSubscription), f'obj must be a NotificationSubscription; got {obj}'
9+
assert isinstance(obj, NotificationSubscriptionLegacy), f'obj must be a NotificationSubscriptionLegacy; got {obj}'
1010
user_id = request.user.id
1111
return obj.none.filter(id=user_id).exists() \
1212
or obj.email_transactional.filter(id=user_id).exists() \

api/subscriptions/views.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
RegistrationProvider,
2323
AbstractProvider,
2424
)
25+
from osf.models.notifications import NotificationSubscriptionLegacy
2526

2627

2728
class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
@@ -39,7 +40,7 @@ class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
3940

4041
def get_default_queryset(self):
4142
user = self.request.user
42-
return NotificationSubscription.objects.filter(
43+
return NotificationSubscriptionLegacy.objects.filter(
4344
Q(none=user) |
4445
Q(email_digest=user) |
4546
Q(
@@ -54,7 +55,7 @@ def get_queryset(self):
5455
class AbstractProviderSubscriptionList(SubscriptionList):
5556
def get_default_queryset(self):
5657
user = self.request.user
57-
return NotificationSubscription.objects.filter(
58+
return NotificationSubscriptionLegacy.objects.filter(
5859
provider___id=self.kwargs['provider_id'],
5960
provider__type=self.provider_class._typedmodels_type,
6061
).filter(
@@ -80,7 +81,7 @@ class SubscriptionDetail(JSONAPIBaseView, generics.RetrieveUpdateAPIView):
8081
def get_object(self):
8182
subscription_id = self.kwargs['subscription_id']
8283
try:
83-
obj = NotificationSubscription.objects.get(_id=subscription_id)
84+
obj = NotificationSubscriptionLegacy.objects.get(_id=subscription_id)
8485
except ObjectDoesNotExist:
8586
raise NotFound
8687
self.check_object_permissions(self.request, obj)
@@ -109,15 +110,15 @@ def get_object(self):
109110
if self.kwargs.get('provider_id'):
110111
provider = self.provider_class.objects.get(_id=self.kwargs.get('provider_id'))
111112
try:
112-
obj = NotificationSubscription.objects.get(
113+
obj = NotificationSubscriptionLegacy.objects.get(
113114
_id=subscription_id,
114115
provider_id=provider.id,
115116
)
116117
except ObjectDoesNotExist:
117118
raise NotFound
118119
else:
119120
try:
120-
obj = NotificationSubscription.objects.get(
121+
obj = NotificationSubscriptionLegacy.objects.get(
121122
_id=subscription_id,
122123
provider__type=self.provider_class._typedmodels_type,
123124
)

api_tests/subscriptions/views/test_subscriptions_detail.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from api.base.settings.defaults import API_BASE
4-
from osf_tests.factories import AuthUserFactory, NotificationSubscriptionFactory
4+
from osf_tests.factories import AuthUserFactory, NotificationSubscriptionLegacyFactory
55

66

77
@pytest.mark.django_db
@@ -17,7 +17,7 @@ def user_no_auth(self):
1717

1818
@pytest.fixture()
1919
def global_user_notification(self, user):
20-
notification = NotificationSubscriptionFactory(_id=f'{user._id}_global', user=user, event_name='global')
20+
notification = NotificationSubscriptionLegacyFactory(_id=f'{user._id}_global', user=user, event_name='global')
2121
notification.add_user_to_subscription(user, 'email_transactional')
2222
return notification
2323

api_tests/subscriptions/views/test_subscriptions_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from api.base.settings.defaults import API_BASE
4-
from osf_tests.factories import AuthUserFactory, PreprintProviderFactory, ProjectFactory, NotificationSubscriptionFactory
4+
from osf_tests.factories import AuthUserFactory, PreprintProviderFactory, ProjectFactory, NotificationSubscriptionLegacyFactory
55

66

77
@pytest.mark.django_db
@@ -23,7 +23,7 @@ def node(self, user):
2323

2424
@pytest.fixture()
2525
def global_user_notification(self, user):
26-
notification = NotificationSubscriptionFactory(_id=f'{user._id}_global', user=user, event_name='global')
26+
notification = NotificationSubscriptionLegacyFactory(_id=f'{user._id}_global', user=user, event_name='global')
2727
notification.add_user_to_subscription(user, 'email_transactional')
2828
return notification
2929

osf/email/__init__.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import logging
2+
import smtplib
3+
from email.mime.text import MIMEText
4+
from sendgrid import SendGridAPIClient
5+
from sendgrid.helpers.mail import Mail
6+
from website import settings
7+
8+
def send_email_over_smtp(to_addr, notification_type, context):
9+
"""Send an email notification using SMTP. This is typically not used in productions as other 3rd party mail services
10+
are preferred. This is to be used for tests and on staging environments and special situations.
11+
12+
Args:
13+
to_addr (str): The recipient's email address.
14+
notification_type (str): The subject of the notification.
15+
context (dict): The email content context.
16+
"""
17+
if not settings.MAIL_SERVER:
18+
raise NotImplementedError('MAIL_SERVER is not set')
19+
if not settings.MAIL_USERNAME and settings.MAIL_PASSWORD:
20+
raise NotImplementedError('MAIL_USERNAME and MAIL_PASSWORD are required for STMP')
21+
22+
msg = MIMEText(
23+
notification_type.template.format(context),
24+
'html',
25+
_charset='utf-8'
26+
)
27+
msg['Subject'] = notification_type.email_subject_line_template.format(context=context)
28+
29+
with smtplib.SMTP(settings.MAIL_SERVER) as server:
30+
server.ehlo()
31+
server.starttls()
32+
server.ehlo()
33+
server.login(settings.MAIL_USERNAME, settings.MAIL_PASSWORD)
34+
server.sendmail(
35+
settings.FROM_EMAIL,
36+
[to_addr],
37+
msg.as_string()
38+
)
39+
40+
41+
def send_email_with_send_grid(to_addr, notification_type, context):
42+
"""Send an email notification using SendGrid.
43+
44+
Args:
45+
to_addr (str): The recipient's email address.
46+
notification_type (str): The subject of the notification.
47+
context (dict): The email content context.
48+
"""
49+
if not settings.SENDGRID_API_KEY:
50+
raise NotImplementedError('SENDGRID_API_KEY is required for sendgrid notifications.')
51+
52+
message = Mail(
53+
from_email=settings.FROM_EMAIL,
54+
to_emails=to_addr,
55+
subject=notification_type,
56+
html_content=context.get('message', '')
57+
)
58+
59+
try:
60+
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
61+
response = sg.send(message)
62+
if response.status_code not in (200, 201, 202):
63+
logging.error(f'SendGrid response error: {response.status_code}, body: {response.body}')
64+
response.raise_for_status()
65+
logging.info(f'Notification email sent to {to_addr} for {notification_type}.')
66+
except Exception as exc:
67+
logging.error(f'Failed to send email notification to {to_addr}: {exc}')
68+
raise exc

osf/management/commands/add_notification_subscription.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66

77
import django
8+
89
django.setup()
910

1011
from django.core.management.base import BaseCommand
@@ -20,9 +21,9 @@
2021
def add_reviews_notification_setting(notification_type, state=None):
2122
if state:
2223
OSFUser = state.get_model('osf', 'OSFUser')
23-
NotificationSubscription = state.get_model('osf', 'NotificationSubscription')
24+
NotificationSubscriptionLegacy = state.get_model('osf', 'NotificationSubscriptionLegacy')
2425
else:
25-
from osf.models import OSFUser, NotificationSubscription
26+
from osf.models import OSFUser, NotificationSubscriptionLegacy
2627

2728
active_users = OSFUser.objects.filter(date_confirmed__isnull=False).exclude(date_disabled__isnull=False).exclude(is_active=False).order_by('id')
2829
total_active_users = active_users.count()
@@ -33,10 +34,10 @@ def add_reviews_notification_setting(notification_type, state=None):
3334
for user in active_users.iterator():
3435
user_subscription_id = to_subscription_key(user._id, notification_type)
3536

36-
subscription = NotificationSubscription.load(user_subscription_id)
37+
subscription = NotificationSubscriptionLegacy.load(user_subscription_id)
3738
if not subscription:
3839
logger.info(f'No {notification_type} subscription found for user {user._id}. Subscribing...')
39-
subscription = NotificationSubscription(_id=user_subscription_id, owner=user, event_name=notification_type)
40+
subscription = NotificationSubscriptionLegacy(_id=user_subscription_id, owner=user, event_name=notification_type)
4041
subscription.save() # Need to save in order to access m2m fields
4142
subscription.add_user_to_subscription(user, 'email_transactional')
4243
else:

osf/management/commands/populate_collection_provider_notification_subscriptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
from django.core.management.base import BaseCommand
4-
from osf.models import NotificationSubscription, CollectionProvider
4+
from osf.models import NotificationSubscriptionLegacy, CollectionProvider
55

66
logger = logging.getLogger(__file__)
77

@@ -12,7 +12,7 @@ def populate_collection_provider_notification_subscriptions():
1212
provider_moderators = provider.get_group('moderator').user_set.all()
1313

1414
for subscription in provider.DEFAULT_SUBSCRIPTIONS:
15-
instance, created = NotificationSubscription.objects.get_or_create(
15+
instance, created = NotificationSubscriptionLegacy.objects.get_or_create(
1616
_id=f'{provider._id}_{subscription}',
1717
event_name=subscription,
1818
provider=provider

osf/management/commands/populate_registration_provider_notification_subscriptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django.contrib.auth.models import Group
44
from django.core.management.base import BaseCommand
5-
from osf.models import NotificationSubscription, RegistrationProvider
5+
from osf.models import RegistrationProvider, NotificationSubscriptionLegacy
66

77
logger = logging.getLogger(__file__)
88

@@ -17,7 +17,7 @@ def populate_registration_provider_notification_subscriptions():
1717
continue
1818

1919
for subscription in provider.DEFAULT_SUBSCRIPTIONS:
20-
instance, created = NotificationSubscription.objects.get_or_create(
20+
instance, created = NotificationSubscriptionLegacy.objects.get_or_create(
2121
_id=f'{provider._id}_{subscription}',
2222
event_name=subscription,
2323
provider=provider

0 commit comments

Comments
 (0)