Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 70 additions & 16 deletions api/subscriptions/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db.models import Value, When, Case, Q, OuterRef, Subquery
from django.db.models import Value, When, Case, OuterRef, Subquery
from django.db.models.fields import CharField, IntegerField
from django.db.models.functions import Concat, Cast
from django.contrib.contenttypes.models import ContentType
Expand All @@ -24,6 +24,7 @@
RegistrationProvider,
AbstractProvider,
AbstractNode,
Guid,
)
from osf.models.notification_type import NotificationType
from osf.models.notification_subscription import NotificationSubscription
Expand All @@ -44,47 +45,72 @@ class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):

def get_queryset(self):
user_guid = self.request.user._id
provider_ct = ContentType.objects.get(app_label='osf', model='abstractprovider')

node_subquery = AbstractNode.objects.filter(
id=Cast(OuterRef('object_id'), IntegerField()),
).values('guids___id')[:1]

_global_file_updated = [
NotificationType.Type.USER_FILE_UPDATED.value,
NotificationType.Type.FILE_ADDED.value,
NotificationType.Type.FILE_REMOVED.value,
NotificationType.Type.ADDON_FILE_COPIED.value,
NotificationType.Type.ADDON_FILE_RENAMED.value,
NotificationType.Type.ADDON_FILE_MOVED.value,
NotificationType.Type.ADDON_FILE_REMOVED.value,
NotificationType.Type.FOLDER_CREATED.value,
]
_global_reviews = [
NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.value,
NotificationType.Type.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION.value,
NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
NotificationType.Type.REVIEWS_SUBMISSION_STATUS.value,
]
_node_file_updated = [
NotificationType.Type.NODE_FILE_UPDATED.value,
NotificationType.Type.FILE_ADDED.value,
NotificationType.Type.FILE_REMOVED.value,
NotificationType.Type.ADDON_FILE_COPIED.value,
NotificationType.Type.ADDON_FILE_RENAMED.value,
NotificationType.Type.ADDON_FILE_MOVED.value,
NotificationType.Type.ADDON_FILE_REMOVED.value,
NotificationType.Type.FOLDER_CREATED.value,
]

qs = NotificationSubscription.objects.filter(
notification_type__in=[
NotificationType.Type.USER_FILE_UPDATED.instance,
NotificationType.Type.NODE_FILE_UPDATED.instance,
NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance,
],
notification_type__name__in=[
NotificationType.Type.USER_FILE_UPDATED.value,
NotificationType.Type.NODE_FILE_UPDATED.value,
NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
] + _global_reviews + _global_file_updated + _node_file_updated,
user=self.request.user,
).annotate(
event_name=Case(
When(
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
notification_type__name__in=_node_file_updated,
then=Value('files_updated'),
),
When(
notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
notification_type__name__in=_global_file_updated,
then=Value('global_file_updated'),
),
When(
Q(notification_type=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance) &
Q(content_type=provider_ct),
notification_type__name__in=_global_reviews,
then=Value('global_reviews'),
),
),
legacy_id=Case(
When(
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
notification_type__name__in=_node_file_updated,
then=Concat(Subquery(node_subquery), Value('_file_updated')),
),
When(
notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
notification_type__name__in=_global_file_updated,
then=Value(f'{user_guid}_global_file_updated'),
),
When(
Q(notification_type=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance) &
Q(content_type=provider_ct),
notification_type__name__in=_global_reviews,
then=Value(f'{user_guid}_global_reviews'),
),
),
Expand Down Expand Up @@ -182,7 +208,7 @@ def update(self, request, *args, **kwargs):
NotificationType.Type.ADDON_FILE_REMOVED.value,
NotificationType.Type.FOLDER_CREATED.value,
],
)
).exclude(content_type=ContentType.objects.get_for_model(AbstractNode))
if not qs.exists():
raise PermissionDenied

Expand Down Expand Up @@ -211,6 +237,34 @@ def update(self, request, *args, **kwargs):
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
elif '_files_updated' in self.kwargs['subscription_id']:
# Copy _files_updated subscription changes to all node file subscriptions
node_id = Guid.load(self.kwargs['subscription_id'].split('_files_updated')[0]).object_id

qs = NotificationSubscription.objects.filter(
user=self.request.user,
content_type=ContentType.objects.get_for_model(AbstractNode),
object_id=node_id,
notification_type__name__in=[
NotificationType.Type.NODE_FILE_UPDATED.value,
NotificationType.Type.FILE_ADDED.value,
NotificationType.Type.FILE_REMOVED.value,
NotificationType.Type.ADDON_FILE_COPIED.value,
NotificationType.Type.ADDON_FILE_RENAMED.value,
NotificationType.Type.ADDON_FILE_MOVED.value,
NotificationType.Type.ADDON_FILE_REMOVED.value,
NotificationType.Type.FOLDER_CREATED.value,
],
)
if not qs.exists():
raise PermissionDenied

for instance in qs:
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)

else:
return super().update(request, *args, **kwargs)

Expand Down
36 changes: 21 additions & 15 deletions api_tests/notifications/test_notification_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ def add_notification_subscription(user, notification_type, frequency, subscribed
Create a NotificationSubscription for a user.
If the notification type corresponds to a subscribed_object, set subscribed_object to get the provider.
"""
from osf.models import NotificationSubscription
from osf.models import NotificationSubscription, AbstractProvider
kwargs = {
'user': user,
'notification_type': NotificationType.objects.get(name=notification_type),
'message_frequency': frequency,
}
if subscribed_object is not None:
kwargs['object_id'] = subscribed_object.id
kwargs['content_type'] = ContentType.objects.get_for_model(subscribed_object)
if isinstance(subscribed_object, AbstractProvider):
kwargs['content_type'] = ContentType.objects.get_for_model(subscribed_object, for_concrete_model=False) if subscribed_object else None
else:
kwargs['content_type'] = ContentType.objects.get_for_model(subscribed_object) if subscribed_object else None
if subscription is not None:
kwargs['object_id'] = subscription.id
kwargs['content_type'] = ContentType.objects.get_for_model(subscription)
Expand Down Expand Up @@ -113,16 +116,17 @@ def test_send_user_email_task_no_notifications(self):
def test_send_moderator_email_task_registration_provider_admin(self):
user = AuthUserFactory(fullname='Admin User')
reg_provider = RegistrationProviderFactory(_id='abc123')
reg = RegistrationFactory(provider=reg_provider)
admin_group = reg_provider.get_group('admin')
admin_group.user_set.add(user)
reg_provider_content_type = ContentType.objects.get_for_model(reg_provider)
RegistrationFactory(provider=reg_provider)
moderator_group = reg_provider.get_group('moderator')
moderator_group.user_set.add(user)
notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
notification = Notification.objects.create(
subscription=add_notification_subscription(
user,
notification_type,
'daily',
subscribed_object=reg
subscribed_object=reg_provider
),
event_context={
'profile_image_url': 'http://example.com/profile.png',
Expand All @@ -137,7 +141,7 @@ def test_send_moderator_email_task_registration_provider_admin(self):
)
notification_ids = [notification.id]
with capture_notifications() as notifications:
send_moderator_email_task.apply(args=(user._id, notification_ids)).get()
send_moderator_email_task.apply(args=(user._id, notification_ids, reg_provider_content_type.id, reg_provider.id)).get()
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['type'] == NotificationType.Type.DIGEST_REVIEWS_MODERATORS
assert notifications['emits'][0]['kwargs']['user'] == user
Expand All @@ -150,30 +154,30 @@ def test_send_moderator_email_task_registration_provider_admin(self):
def test_send_moderator_email_task_no_notifications(self):
user = AuthUserFactory(fullname='Admin User')
provider = RegistrationProviderFactory()
reg = RegistrationFactory(provider=provider)
reg_provider_content_type = ContentType.objects.get_for_model(provider)
RegistrationFactory(provider=provider)

notification_ids = []
notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
add_notification_subscription(
user,
notification_type,
'daily',
subscribed_object=reg
subscribed_object=provider
)

send_moderator_email_task.apply(args=(user._id, notification_ids)).get()
send_moderator_email_task.apply(args=(user._id, notification_ids, reg_provider_content_type.id, provider.id)).get()
email_task = EmailTask.objects.filter(user_id=user.id).first()
assert email_task.status == 'SUCCESS'

def test_send_moderator_email_task_user_not_found(self):
send_moderator_email_task.apply(args=('nouser', [])).get()
send_moderator_email_task.apply(args=('nouser', [], 1, 1)).get()
email_task = EmailTask.objects.filter()
assert email_task.exists()
assert email_task.first().status == 'NO_USER_FOUND'

def test_get_users_emails(self):
user = AuthUserFactory()
notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_DIGEST)
notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
notification1 = Notification.objects.create(
subscription=add_notification_subscription(user, notification_type, 'daily'),
sent=None,
Expand Down Expand Up @@ -249,10 +253,12 @@ def test_send_users_digest_email_end_to_end(self):
def test_send_moderators_digest_email_end_to_end(self):
user = AuthUserFactory()
provider = RegistrationProviderFactory()
reg = RegistrationFactory(provider=provider)
RegistrationFactory(provider=provider)
moderator_group = provider.get_group('moderator')
moderator_group.user_set.add(user)
notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
Notification.objects.create(
subscription=add_notification_subscription(user, notification_type, 'daily', subscribed_object=reg),
subscription=add_notification_subscription(user, notification_type, 'daily', subscribed_object=provider),
sent=None,
event_context={
'submitter_fullname': 'submitter_fullname',
Expand Down
7 changes: 7 additions & 0 deletions notifications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,10 @@ notification_types:
object_content_type_model_name: abstractnode
template: 'website/templates/file_updated.html.mako'
tests: ['tests/test_events.py']

- name: node_withdrawal_request_rejected
subject: 'Your withdrawal request has been declined'
__docs__: ...
object_content_type_model_name: abstractnode
template: 'website/templates/withdrawal_request_declined.html.mako'
tests: []
1 change: 1 addition & 0 deletions notifications/file_event_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def perform(self):
name=self.action
).emit(
user=self.user,
subscribed_object=self.node,
event_context={
'user_fullname': self.user.fullname,
'profile_image_url': self.profile_image_url,
Expand Down
58 changes: 30 additions & 28 deletions notifications/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@project_created.connect
def subscribe_creator(resource):
from osf.models import NotificationSubscription, NotificationType
from osf.models import NotificationSubscription, NotificationType, Preprint

from django.contrib.contenttypes.models import ContentType

Expand All @@ -30,24 +30,25 @@ def subscribe_creator(resource):
)
except NotificationSubscription.MultipleObjectsReturned:
pass
try:
NotificationSubscription.objects.get_or_create(
user=user,
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
defaults={
'message_frequency': 'instantly',
}
)
except NotificationSubscription.MultipleObjectsReturned:
pass
if not isinstance(resource, Preprint):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why only exclude Preprints?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior to the NR, we excluded the preprint. This piece was simply lost during the NR.

subscribe_user_to_notifications(node, node.creator)

if isinstance(node, Preprint):

try:
NotificationSubscription.objects.get_or_create(
user=user,
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
defaults={
'message_frequency': 'instantly',
}
)
except NotificationSubscription.MultipleObjectsReturned:
pass

@contributor_added.connect
def subscribe_contributor(resource, contributor, auth=None, *args, **kwargs):
from django.contrib.contenttypes.models import ContentType
from osf.models import NotificationSubscription, NotificationType
from osf.models import NotificationSubscription, NotificationType, Preprint

from osf.models import Node
if isinstance(resource, Node):
Expand All @@ -67,19 +68,20 @@ def subscribe_contributor(resource, contributor, auth=None, *args, **kwargs):
)
except NotificationSubscription.MultipleObjectsReturned:
pass
try:
NotificationSubscription.objects.get_or_create(
user=contributor,
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
defaults={
'message_frequency': 'instantly',
}
)
except NotificationSubscription.MultipleObjectsReturned:
pass
if not isinstance(resource, Preprint):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

try:
NotificationSubscription.objects.get_or_create(
user=contributor,
notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
defaults={
'message_frequency': 'instantly',
}
)
except NotificationSubscription.MultipleObjectsReturned:
pass


# Handle email notifications to notify moderators of new submissions.
Expand Down
Loading
Loading