diff --git a/CHANGELOG b/CHANGELOG index 3ed2e838286..8725b6dfc5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,13 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +26.1.4 (2026-01-06) +=================== + +- Follow-up fix for preprint/registration withdrawal request +- Added a script/task to populate default notification subscriptions that were missing before and after NR +- Reworked PR template + 26.1.3 (2026-01-05) =================== diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index ed765fca672..b4978d67c59 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,39 +1,55 @@ - +## Ticket + +[//]: # (Link to a JIRA ticket if available.) + +* [ENG-*****]() ## Purpose - +[//]: # (Briefly describe the purpose of this PR.) ## Changes - +[//]: # (Briefly describe or list your changes.) -## QA Notes +## Side Effects +[//]: # (Any possible side effects?) - +## QE Notes -## Documentation +[//]: # ( + * Any QA testing notes for QE? + * Make verification statements inspired by your code and what your code touches. + * What are the areas of risk? + * Any concerns/considerations/questions that development raised? + * If you have a JIRA ticket, make sure the ticket also contains the QE notes. +) - +## CE Notes -## Side Effects - - +[//]: # ( + * Any server configuration and deployment notes for CE? + * Is model migration required? Is data migration/backfill/population required? + * If so, is it reversible? Is there a roll-back plan? + * Does server settings needs to be updated? + * If so, have you checked with CE on existing settings for affected servers? + * Are there any deployment dependencies to other services? + * If you have a JIRA ticket, make sure the ticket also contains the CE notes. +) -## Ticket +## Documentation - +[//]: # ( + * Does any internal or external documentation need to be updated? + * If the API was versioned, update the developer.osf.io changelog. + * If changes were made to the API, link the developer.osf.io PR here. +) diff --git a/package.json b/package.json index 9d16c836db7..1525f296937 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "26.1.3", + "version": "26.1.4", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", diff --git a/scripts/populate_notification_subscriptions.py b/scripts/populate_notification_subscriptions.py new file mode 100644 index 00000000000..f9da312f6e4 --- /dev/null +++ b/scripts/populate_notification_subscriptions.py @@ -0,0 +1,111 @@ +import django +django.setup() + +from website.app import init_app +init_app(routes=False) + +from framework.celery_tasks import app as celery_app +from django.contrib.contenttypes.models import ContentType +from django.db.models import Count, F, OuterRef, Subquery, IntegerField, CharField +from django.db.models.functions import Cast +from osf.models import OSFUser, Node, NotificationSubscription, NotificationType + + +@celery_app.task(name='scripts.populate_notification_subscriptions') +def populate_notification_subscriptions(): + created = 0 + user_file_nt = NotificationType.Type.USER_FILE_UPDATED.instance + review_nt = NotificationType.Type.REVIEWS_SUBMISSION_STATUS.instance + node_file_nt = NotificationType.Type.NODE_FILE_UPDATED.instance + + user_ct = ContentType.objects.get_for_model(OSFUser) + node_ct = ContentType.objects.get_for_model(Node) + + reviews_qs = OSFUser.objects.exclude(subscriptions__notification_type__name=NotificationType.Type.REVIEWS_SUBMISSION_STATUS).distinct('id') + files_qs = OSFUser.objects.exclude(subscriptions__notification_type__name=NotificationType.Type.USER_FILE_UPDATED).distinct('id') + + node_notifications_sq = ( + NotificationSubscription.objects.filter( + content_type=node_ct, + notification_type=node_file_nt, + object_id=Cast(OuterRef('pk'), CharField()), + ).values( + 'object_id' + ).annotate( + cnt=Count('id') + ).values('cnt')[:1] + ) + + nodes_qs = ( + Node.objects + .annotate( + contributors_count=Count('_contributors', distinct=True), + notifications_count=Subquery( + node_notifications_sq, + output_field=IntegerField(), + ), + ).exclude(contributors_count=F('notifications_count')) + ) + + print(f"Creating REVIEWS_SUBMISSION_STATUS subscriptions for {reviews_qs.count()} users.") + for id, user in enumerate(reviews_qs, 1): + print(f"Processing user {id} / {reviews_qs.count()}") + try: + _, is_created = NotificationSubscription.objects.get_or_create( + notification_type=review_nt, + user=user, + content_type=user_ct, + object_id=user.id, + defaults={ + 'message_frequency': 'none', + }, + ) + if is_created: + created += 1 + except Exception as exeption: + print(exeption) + continue + + print(f"Creating USER_FILE_UPDATED subscriptions for {files_qs.count()} users.") + for id, user in enumerate(files_qs, 1): + print(f"Processing user {id} / {files_qs.count()}") + try: + _, is_created = NotificationSubscription.objects.get_or_create( + notification_type=user_file_nt, + user=user, + content_type=user_ct, + object_id=user.id, + defaults={ + 'message_frequency': 'none', + }, + ) + if is_created: + created += 1 + except Exception as exeption: + print(exeption) + continue + + print(f"Creating NODE_FILE_UPDATED subscriptions for {nodes_qs.count()} nodes.") + for id, node in enumerate(nodes_qs, 1): + print(f"Processing node {id} / {nodes_qs.count()}") + for contributor in node.contributors.all(): + try: + _, is_created = NotificationSubscription.objects.get_or_create( + notification_type=node_file_nt, + user=contributor, + content_type=node_ct, + object_id=node.id, + defaults={ + 'message_frequency': 'none', + }, + ) + if is_created: + created += 1 + except Exception as exeption: + print(exeption) + continue + + print(f"Created {created} subscriptions") + +if __name__ == '__main__': + populate_notification_subscriptions.delay() diff --git a/website/reviews/listeners.py b/website/reviews/listeners.py index 51baf5621c6..ef43684bbc0 100644 --- a/website/reviews/listeners.py +++ b/website/reviews/listeners.py @@ -25,8 +25,9 @@ def reviews_notification(self, creator, template, context, action): @reviews_signals.reviews_withdraw_requests_notification_moderators.connect def reviews_withdraw_requests_notification_moderators(self, timestamp, context, user, resource): - context['referrer_fullname'] = user.fullname + from website.profile.utils import get_profile_image_url context['requester_fullname'] = user.fullname + context['profile_image_url'] = get_profile_image_url(resource.creator) provider = resource.provider from osf.models import NotificationType @@ -38,6 +39,7 @@ def reviews_withdraw_requests_notification_moderators(self, timestamp, context, for recipient in provider.get_group(group_name).user_set.all(): context['user_fullname'] = recipient.fullname context['recipient_fullname'] = recipient.fullname + context['localized_timestamp'] = str(timestamp) NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit( user=recipient, @@ -48,7 +50,9 @@ def reviews_withdraw_requests_notification_moderators(self, timestamp, context, @reviews_signals.reviews_email_withdrawal_requests.connect def reviews_withdrawal_requests_notification(self, timestamp, context): + from website.profile.utils import get_profile_image_url preprint = context.pop('reviewable') + context['profile_image_url'] = get_profile_image_url(preprint.creator) context['reviewable_absolute_url'] = preprint.absolute_url context['reviewable_title'] = preprint.title context['reviewable__id'] = preprint._id @@ -63,6 +67,7 @@ def reviews_withdrawal_requests_notification(self, timestamp, context): for recipient in preprint.provider.get_group(group_name).user_set.all(): context['user_fullname'] = recipient.fullname context['recipient_fullname'] = recipient.fullname + context['localized_timestamp'] = str(timestamp) NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit( user=recipient, diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 2a82bb820ca..d09e583c181 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -450,6 +450,7 @@ class CeleryConfig: 'osf.management.commands.monthly_reporters_go', 'osf.management.commands.ingest_cedar_metadata_templates', 'osf.metrics.reporters', + 'scripts.populate_notification_subscriptions', } med_pri_modules = { @@ -578,6 +579,7 @@ class CeleryConfig: 'osf.management.commands.monthly_reporters_go', 'osf.external.spam.tasks', 'api.share.utils', + 'scripts.populate_notification_subscriptions', ) # Modules that need metrics and release requirements