1+ from django .contrib .contenttypes .models import ContentType
2+ from django .core .exceptions import PermissionDenied
13from django .db .models import Value , When , Case , OuterRef , Subquery , F
24from django .db .models .fields import CharField , IntegerField
35from django .db .models .functions import Concat , Cast
4- from django . contrib . contenttypes . models import ContentType
6+
57from rest_framework import generics
68from rest_framework import permissions as drf_permissions
7- from rest_framework .exceptions import NotFound
8- from django .core .exceptions import ObjectDoesNotExist , PermissionDenied
99from rest_framework .response import Response
10+
1011from framework .auth .oauth_scopes import CoreScopes
12+
1113from api .base .views import JSONAPIBaseView
1214from api .base .filters import ListFilterMixin
1315from api .base import permissions as base_permissions
1820 RegistrationSubscriptionSerializer ,
1921)
2022from api .subscriptions .permissions import IsSubscriptionOwner
23+ from api .subscriptions import utils
24+
2125from osf .models import (
2226 CollectionProvider ,
2327 PreprintProvider ,
2428 RegistrationProvider ,
2529 AbstractProvider ,
2630 AbstractNode ,
2731 Guid ,
32+ OSFUser ,
2833)
2934from osf .models .notification_type import NotificationType
3035from osf .models .notification_subscription import NotificationSubscription
@@ -44,11 +49,16 @@ class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
4449 required_write_scopes = [CoreScopes .NULL ]
4550
4651 def get_queryset (self ):
52+
53+ user = self .request .user
4754 user_guid = self .request .user ._id
4855
49- provider_ct = ContentType .objects .get_by_natural_key (app_label = 'osf' , model = 'abstractprovider' )
50- node_ct = ContentType .objects .get_by_natural_key (app_label = 'osf' , model = 'abstractnode' )
51- user_ct = ContentType .objects .get_by_natural_key (app_label = 'osf' , model = 'osfuser' )
56+ filter_id = self .request .query_params .get ('filter[id]' )
57+ filter_event_name = self .request .query_params .get ('filter[event_name]' )
58+
59+ provider_ct = ContentType .objects .get_for_model (AbstractProvider )
60+ node_ct = ContentType .objects .get_for_model (AbstractNode )
61+ user_ct = ContentType .objects .get_for_model (OSFUser )
5262
5363 node_subquery = AbstractNode .objects .filter (
5464 id = Cast (OuterRef ('object_id' ), IntegerField ()),
@@ -85,15 +95,16 @@ def get_queryset(self):
8595 NotificationType .Type .FILE_UPDATED .value ,
8696 ]
8797
88- qs = NotificationSubscription .objects .filter (
89- notification_type__name__in = _global_reviews_provider + _global_reviews_user + _global_file_updated + _node_file_updated ,
90- user = self .request .user ,
98+ full_set_of_types = _global_reviews_provider + _global_reviews_user + _global_file_updated + _node_file_updated
99+ annotated_qs = NotificationSubscription .objects .filter (
100+ notification_type__name__in = full_set_of_types ,
101+ user = user ,
91102 ).annotate (
92103 event_name = Case (
93104 When (
94105 notification_type__name__in = _node_file_updated ,
95106 content_type = node_ct ,
96- then = Value ('files_updated ' ),
107+ then = Value ('file_updated ' ),
97108 ),
98109 When (
99110 notification_type__name__in = _global_file_updated ,
@@ -135,17 +146,31 @@ def get_queryset(self):
135146 ),
136147 ).distinct ('legacy_id' )
137148
149+ return_qs = annotated_qs
150+
138151 # Apply manual filter for legacy_id if requested
139- filter_id = self .request .query_params .get ('filter[id]' )
140152 if filter_id :
141- qs = qs .filter (legacy_id = filter_id )
142- # convert to list comprehension because legacy_id is an annotation, not in DB
153+ return_qs = annotated_qs .filter (legacy_id = filter_id )
154+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
155+ # NOTE: `.exists()` errors for unknown reason, possibly due to complex annotation with `.distinct()`
156+ if return_qs .count () == 0 :
157+ missing_subscription_created = utils .create_missing_notification_from_legacy_id (filter_id , user )
158+ if missing_subscription_created :
159+ return_qs = annotated_qs .filter (legacy_id = filter_id )
160+ # `filter_id` takes priority over `filter_event_name`
161+ return return_qs
162+
143163 # Apply manual filter for event_name if requested
144- filter_event_name = self .request .query_params .get ('filter[event_name]' )
145164 if filter_event_name :
146- qs = qs .filter (event_name__in = filter_event_name .split (',' ))
165+ filter_event_names = filter_event_name .split (',' )
166+ return_qs = annotated_qs .filter (event_name__in = filter_event_names )
167+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
168+ # NOTE: `.exists()` errors for unknown reason, possibly due to complex annotation with `.distinct()`
169+ if return_qs .count () == 0 :
170+ utils .create_missing_notifications_from_event_name (filter_event_names , user )
171+
172+ return return_qs
147173
148- return qs
149174
150175class AbstractProviderSubscriptionList (SubscriptionList ):
151176 def get_queryset (self ):
@@ -171,52 +196,60 @@ class SubscriptionDetail(JSONAPIBaseView, generics.RetrieveUpdateAPIView):
171196
172197 def get_object (self ):
173198 subscription_id = self .kwargs ['subscription_id' ]
199+ user = self .request .user
174200 user_guid = self .request .user ._id
175-
176- provider_ct = ContentType .objects .get (app_label = 'osf' , model = 'abstractprovider' )
177- node_ct = ContentType .objects .get (app_label = 'osf' , model = 'abstractnode' )
201+ user_ct = ContentType .objects .get_for_model (OSFUser )
202+ node_ct = ContentType .objects .get_for_model (AbstractNode )
178203
179204 node_subquery = AbstractNode .objects .filter (
180205 id = Cast (OuterRef ('object_id' ), IntegerField ()),
181206 ).values ('guids___id' )[:1 ]
182207
183- try :
184- annotated_obj_qs = NotificationSubscription .objects .filter (user = self .request .user ).annotate (
185- legacy_id = Case (
186- When (
187- notification_type__name = NotificationType .Type .NODE_FILE_UPDATED .value ,
188- content_type = node_ct ,
189- then = Concat (Subquery (node_subquery ), Value ('_files_updated' )),
190- ),
191- When (
192- notification_type__name = NotificationType .Type .USER_FILE_UPDATED .value ,
193- then = Value (f'{ user_guid } _global_file_updated' ),
194- ),
195- When (
196- notification_type__name = NotificationType .Type .PROVIDER_NEW_PENDING_SUBMISSIONS .value ,
197- content_type = provider_ct ,
198- then = Value (f'{ user_guid } _global_reviews' ),
199- ),
200- default = Value (f'{ user_guid } _global' ),
201- output_field = CharField (),
208+ missing_subscription_created = None
209+ annotated_obj_qs = NotificationSubscription .objects .filter (user = user ).annotate (
210+ legacy_id = Case (
211+ When (
212+ notification_type__name = NotificationType .Type .NODE_FILE_UPDATED .value ,
213+ content_type = node_ct ,
214+ then = Concat (Subquery (node_subquery ), Value ('_file_updated' )),
202215 ),
203- )
204- obj = annotated_obj_qs .filter (legacy_id = subscription_id )
205-
206- except ObjectDoesNotExist :
207- raise NotFound
208-
209- obj = obj .filter (user = self .request .user ).first ()
210- if not obj :
216+ When (
217+ notification_type__name = NotificationType .Type .USER_FILE_UPDATED .value ,
218+ then = Value (f'{ user_guid } _global_file_updated' ),
219+ ),
220+ When (
221+ notification_type__name = NotificationType .Type .REVIEWS_SUBMISSION_STATUS .value ,
222+ content_type = user_ct ,
223+ then = Value (f'{ user_guid } _global_reviews' ),
224+ ),
225+ default = Value (f'{ user_guid } _global' ),
226+ output_field = CharField (),
227+ ),
228+ )
229+ existing_subscriptions = annotated_obj_qs .filter (legacy_id = subscription_id )
230+
231+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
232+ if not existing_subscriptions .exists ():
233+ missing_subscription_created = utils .create_missing_notification_from_legacy_id (subscription_id , user )
234+ if missing_subscription_created :
235+ # Note: must use `annotated_obj_qs` to insert `legacy_id` so that `SubscriptionSerializer` can build data
236+ # properly; in addition, there should be only one result
237+ subscription = annotated_obj_qs .get (legacy_id = subscription_id )
238+ else :
239+ # TODO: Use `get()` and fails/warns on multiple objects after fully de-duplicating the OSF DB
240+ subscription = existing_subscriptions .order_by ('id' ).last ()
241+ if not subscription :
211242 raise PermissionDenied
212243
213- self .check_object_permissions (self .request , obj )
214- return obj
244+ self .check_object_permissions (self .request , subscription )
245+ return subscription
215246
216247 def update (self , request , * args , ** kwargs ):
217248 """
218249 Update a notification subscription
219250 """
251+ self .get_object ()
252+
220253 if '_global_file_updated' in self .kwargs ['subscription_id' ]:
221254 # Copy _global_file_updated subscription changes to all file subscriptions
222255 qs = NotificationSubscription .objects .filter (
@@ -261,9 +294,9 @@ def update(self, request, *args, **kwargs):
261294 serializer .is_valid (raise_exception = True )
262295 self .perform_update (serializer )
263296 return Response (serializer .data )
264- elif '_files_updated ' in self .kwargs ['subscription_id' ]:
265- # Copy _files_updated subscription changes to all node file subscriptions
266- node_id = Guid .load (self .kwargs ['subscription_id' ].split ('_files_updated ' )[0 ]).object_id
297+ elif '_file_updated ' in self .kwargs ['subscription_id' ]:
298+ # Copy _file_updated subscription changes to all node file subscriptions
299+ node_id = Guid .load (self .kwargs ['subscription_id' ].split ('_file_updated ' )[0 ]).object_id
267300
268301 qs = NotificationSubscription .objects .filter (
269302 user = self .request .user ,
0 commit comments