Skip to content

Commit b2e5ae1

Browse files
Johnetordoffbodintsov
authored andcommitted
add new notificationsubscription class to views
1 parent 25fce4d commit b2e5ae1

7 files changed

Lines changed: 144 additions & 126 deletions

File tree

api/subscriptions/fields.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from rest_framework import serializers as ser
2+
from osf.models import NotificationSubscription
3+
4+
class FrequencyField(ser.ChoiceField):
5+
def __init__(self, **kwargs):
6+
super().__init__(choices=['none', 'instantly', 'daily', 'weekly', 'monthly'], **kwargs)
7+
8+
def to_representation(self, obj: NotificationSubscription):
9+
return obj.message_frequency
10+
11+
def to_internal_value(self, freq):
12+
return super().to_internal_value(freq)

api/subscriptions/permissions.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from rest_framework import permissions
22

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

55

66
class IsSubscriptionOwner(permissions.BasePermission):
77

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

api/subscriptions/serializers.py

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,43 @@
1+
from django.contrib.contenttypes.models import ContentType
12
from rest_framework import serializers as ser
2-
from rest_framework.exceptions import ValidationError
33
from api.nodes.serializers import RegistrationProviderRelationshipField
44
from api.collections_providers.fields import CollectionProviderRelationshipField
55
from api.preprints.serializers import PreprintProviderRelationshipField
6+
from osf.models import Node
67
from website.util import api_v2_url
78

89

9-
from api.base.serializers import JSONAPISerializer, LinksField
10-
11-
NOTIFICATION_TYPES = {
12-
'none': 'none',
13-
'instant': 'email_transactional',
14-
'daily': 'email_digest',
15-
}
16-
17-
18-
class FrequencyField(ser.Field):
19-
def to_representation(self, obj):
20-
user_id = self.context['request'].user.id
21-
if obj.email_transactional.filter(id=user_id).exists():
22-
return 'instant'
23-
if obj.email_digest.filter(id=user_id).exists():
24-
return 'daily'
25-
return 'none'
26-
27-
def to_internal_value(self, frequency):
28-
notification_type = NOTIFICATION_TYPES.get(frequency)
29-
if notification_type:
30-
return {'notification_type': notification_type}
31-
raise ValidationError(f'Invalid frequency "{frequency}"')
10+
from api.base.serializers import JSONAPISerializer
11+
from .fields import FrequencyField
3212

3313
class SubscriptionSerializer(JSONAPISerializer):
3414
filterable_fields = frozenset([
3515
'id',
3616
'event_name',
17+
'frequency',
3718
])
3819

39-
id = ser.CharField(source='_id', read_only=True)
20+
id = ser.CharField(read_only=True)
4021
event_name = ser.CharField(read_only=True)
4122
frequency = FrequencyField(source='*', required=True)
42-
links = LinksField({
43-
'self': 'get_absolute_url',
44-
})
4523

4624
class Meta:
4725
type_ = 'subscription'
4826

49-
def get_absolute_url(self, obj):
50-
return obj.absolute_api_v2_url
51-
5227
def update(self, instance, validated_data):
5328
user = self.context['request'].user
54-
notification_type = validated_data.get('notification_type')
55-
instance.add_user_to_subscription(user, notification_type, save=True)
29+
frequency = validated_data.get('frequency')
30+
31+
if frequency != 'none' and instance.content_type == ContentType.objects.get_for_model(Node):
32+
node = Node.objects.get(
33+
id=instance.id,
34+
content_type=instance.content_type,
35+
)
36+
user_subs = node.parent_node.child_node_subscriptions
37+
if node._id not in user_subs.setdefault(user._id, []):
38+
user_subs[user._id].append(node._id)
39+
node.parent_node.save()
40+
5641
return instance
5742

5843

api/subscriptions/views.py

Lines changed: 13 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
from pyasn1_modules.rfc5126 import ContentType
12
from rest_framework import generics
23
from rest_framework import permissions as drf_permissions
34
from rest_framework.exceptions import NotFound
45
from django.core.exceptions import ObjectDoesNotExist
5-
from django.db.models import Q
66

77
from framework.auth.oauth_scopes import CoreScopes
88
from api.base.views import JSONAPIBaseView
@@ -16,13 +16,12 @@
1616
)
1717
from api.subscriptions.permissions import IsSubscriptionOwner
1818
from osf.models import (
19-
NotificationSubscription,
2019
CollectionProvider,
2120
PreprintProvider,
2221
RegistrationProvider,
2322
AbstractProvider,
2423
)
25-
from osf.models.notifications import NotificationSubscriptionLegacy
24+
from osf.models.notification import NotificationSubscription
2625

2726

2827
class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
@@ -38,32 +37,20 @@ class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
3837
required_read_scopes = [CoreScopes.SUBSCRIPTIONS_READ]
3938
required_write_scopes = [CoreScopes.NULL]
4039

41-
def get_default_queryset(self):
42-
user = self.request.user
43-
return NotificationSubscriptionLegacy.objects.filter(
44-
Q(none=user) |
45-
Q(email_digest=user) |
46-
Q(
47-
email_transactional=user,
48-
),
49-
).distinct()
50-
5140
def get_queryset(self):
52-
return self.get_queryset_from_request()
41+
return NotificationSubscription.objects.filter(
42+
user=self.request.user,
43+
)
5344

5445

5546
class AbstractProviderSubscriptionList(SubscriptionList):
56-
def get_default_queryset(self):
57-
user = self.request.user
58-
return NotificationSubscriptionLegacy.objects.filter(
59-
provider___id=self.kwargs['provider_id'],
60-
provider__type=self.provider_class._typedmodels_type,
61-
).filter(
62-
Q(none=user) |
63-
Q(email_digest=user) |
64-
Q(email_transactional=user),
65-
).distinct()
66-
47+
def get_queryset(self):
48+
provider = AbstractProvider.objects.get(_id=self.kwargs['provider_id'])
49+
return NotificationSubscription.objects.filter(
50+
object_id=provider,
51+
provider__type=ContentType.objects.get_for_model(provider.__class__),
52+
user=self.request.user,
53+
)
6754

6855
class SubscriptionDetail(JSONAPIBaseView, generics.RetrieveUpdateAPIView):
6956
view_name = 'notification-subscription-detail'
@@ -81,7 +68,7 @@ class SubscriptionDetail(JSONAPIBaseView, generics.RetrieveUpdateAPIView):
8168
def get_object(self):
8269
subscription_id = self.kwargs['subscription_id']
8370
try:
84-
obj = NotificationSubscriptionLegacy.objects.get(_id=subscription_id)
71+
obj = NotificationSubscription.objects.get(id=subscription_id)
8572
except ObjectDoesNotExist:
8673
raise NotFound
8774
self.check_object_permissions(self.request, obj)
@@ -101,33 +88,6 @@ class AbstractProviderSubscriptionDetail(SubscriptionDetail):
10188
required_write_scopes = [CoreScopes.SUBSCRIPTIONS_WRITE]
10289
provider_class = None
10390

104-
def __init__(self, *args, **kwargs):
105-
assert issubclass(self.provider_class, AbstractProvider), 'Class must be subclass of AbstractProvider'
106-
super().__init__(*args, **kwargs)
107-
108-
def get_object(self):
109-
subscription_id = self.kwargs['subscription_id']
110-
if self.kwargs.get('provider_id'):
111-
provider = self.provider_class.objects.get(_id=self.kwargs.get('provider_id'))
112-
try:
113-
obj = NotificationSubscriptionLegacy.objects.get(
114-
_id=subscription_id,
115-
provider_id=provider.id,
116-
)
117-
except ObjectDoesNotExist:
118-
raise NotFound
119-
else:
120-
try:
121-
obj = NotificationSubscriptionLegacy.objects.get(
122-
_id=subscription_id,
123-
provider__type=self.provider_class._typedmodels_type,
124-
)
125-
except ObjectDoesNotExist:
126-
raise NotFound
127-
self.check_object_permissions(self.request, obj)
128-
return obj
129-
130-
13191
class CollectionProviderSubscriptionDetail(AbstractProviderSubscriptionDetail):
13292
provider_class = CollectionProvider
13393
serializer_class = CollectionSubscriptionSerializer
Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import pytest
22

33
from api.base.settings.defaults import API_BASE
4-
from osf_tests.factories import AuthUserFactory, NotificationSubscriptionLegacyFactory
5-
4+
from osf_tests.factories import (
5+
AuthUserFactory,
6+
NotificationSubscriptionFactory
7+
)
68

79
@pytest.mark.django_db
810
class TestSubscriptionDetail:
@@ -16,18 +18,18 @@ def user_no_auth(self):
1618
return AuthUserFactory()
1719

1820
@pytest.fixture()
19-
def global_user_notification(self, user):
20-
notification = NotificationSubscriptionLegacyFactory(_id=f'{user._id}_global', user=user, event_name='global')
21-
notification.add_user_to_subscription(user, 'email_transactional')
22-
return notification
21+
def notification(self, user):
22+
return NotificationSubscriptionFactory(
23+
user=user,
24+
)
2325

2426
@pytest.fixture()
25-
def url(self, global_user_notification):
26-
return f'/{API_BASE}subscriptions/{global_user_notification._id}/'
27+
def url(self, notification):
28+
return f'/{API_BASE}subscriptions/{notification.id}/'
2729

2830
@pytest.fixture()
2931
def url_invalid(self):
30-
return '/{}subscriptions/{}/'.format(API_BASE, 'invalid-notification-id')
32+
return f'/{API_BASE}subscriptions/invalid-notification-id/'
3133

3234
@pytest.fixture()
3335
def payload(self):
@@ -51,56 +53,101 @@ def payload_invalid(self):
5153
}
5254
}
5355

54-
def test_subscription_detail(self, app, user, user_no_auth, global_user_notification, url, url_invalid, payload, payload_invalid):
55-
# GET with valid notification_id
56-
# Invalid user
57-
res = app.get(url, auth=user_no_auth.auth, expect_errors=True)
56+
def test_subscription_detail_invalid_user(
57+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
58+
):
59+
res = app.get(
60+
url,
61+
auth=user_no_auth.auth,
62+
expect_errors=True
63+
)
5864
assert res.status_code == 403
59-
# No user
60-
res = app.get(url, expect_errors=True)
65+
66+
def test_subscription_detail_no_user(
67+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
68+
):
69+
res = app.get(
70+
url,
71+
expect_errors=True
72+
)
6173
assert res.status_code == 401
62-
# Valid user
74+
75+
def test_subscription_detail_valid_user(
76+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
77+
):
78+
6379
res = app.get(url, auth=user.auth)
6480
notification_id = res.json['data']['id']
6581
assert res.status_code == 200
66-
assert notification_id == f'{user._id}_global'
82+
assert notification_id == str(notification.id)
6783

68-
# GET with invalid notification_id
69-
# No user
84+
def test_subscription_detail_invalid_notification_id_no_user(
85+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
86+
):
7087
res = app.get(url_invalid, expect_errors=True)
7188
assert res.status_code == 404
72-
# Existing user
73-
res = app.get(url_invalid, auth=user.auth, expect_errors=True)
89+
90+
def test_subscription_detail_invalid_notification_id_existing_user(
91+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
92+
):
93+
res = app.get(
94+
url_invalid,
95+
auth=user.auth,
96+
expect_errors=True
97+
)
7498
assert res.status_code == 404
7599

76-
# PATCH with valid notification_id and invalid data
77-
# Invalid user
100+
def test_subscription_detail_invalid_payload_403(
101+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
102+
):
78103
res = app.patch_json_api(url, payload_invalid, auth=user_no_auth.auth, expect_errors=True)
79104
assert res.status_code == 403
80-
# No user
105+
106+
def test_subscription_detail_invalid_payload_401(
107+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
108+
):
81109
res = app.patch_json_api(url, payload_invalid, expect_errors=True)
82110
assert res.status_code == 401
83-
# Valid user
84-
res = app.patch_json_api(url, payload_invalid, auth=user.auth, expect_errors=True)
111+
112+
def test_subscription_detail_invalid_payload_400(
113+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
114+
):
115+
res = app.patch_json_api(
116+
url,
117+
payload_invalid,
118+
auth=user.auth,
119+
expect_errors=True
120+
)
85121
assert res.status_code == 400
86-
assert res.json['errors'][0]['detail'] == 'Invalid frequency "invalid-frequency"'
122+
assert res.json['errors'][0]['detail'] == '"invalid-frequency" is not a valid choice.'
87123

88-
# PATCH with invalid notification_id
89-
# No user
124+
def test_subscription_detail_patch_invalid_notification_id_no_user(
125+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
126+
):
90127
res = app.patch_json_api(url_invalid, payload, expect_errors=True)
91128
assert res.status_code == 404
92-
# Existing user
129+
130+
def test_subscription_detail_patch_invalid_notification_id_existing_user(
131+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
132+
):
93133
res = app.patch_json_api(url_invalid, payload, auth=user.auth, expect_errors=True)
94134
assert res.status_code == 404
95135

96-
# PATCH with valid notification_id and valid data
97-
# Invalid user
136+
def test_subscription_detail_patch_invalid_user(
137+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
138+
):
98139
res = app.patch_json_api(url, payload, auth=user_no_auth.auth, expect_errors=True)
99140
assert res.status_code == 403
100-
# No user
141+
142+
def test_subscription_detail_patch_no_user(
143+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
144+
):
101145
res = app.patch_json_api(url, payload, expect_errors=True)
102146
assert res.status_code == 401
103-
# Valid user
147+
148+
def test_subscription_detail_patch(
149+
self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
150+
):
104151
res = app.patch_json_api(url, payload, auth=user.auth)
105152
assert res.status_code == 200
106153
assert res.json['data']['attributes']['frequency'] == 'none'

osf/models/notification.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ def emit(self, user, subscribed_object=None, event_context=None):
296296
event_context=event_context
297297
)
298298

299+
@property
300+
def absolute_api_v2_url(self):
301+
from api.base.utils import absolute_reverse
302+
return absolute_reverse('institutions:institution-detail', kwargs={'institution_id': self._id, 'version': 'v2'})
303+
304+
299305
class Notification(models.Model):
300306
subscription = models.ForeignKey(
301307
NotificationSubscription,

0 commit comments

Comments
 (0)