diff --git a/addons/base/views.py b/addons/base/views.py
index 3e5403bc698..5ff3d6e7093 100644
--- a/addons/base/views.py
+++ b/addons/base/views.py
@@ -51,7 +51,7 @@
DraftRegistration,
Guid,
FileVersionUserMetadata,
- FileVersion, NotificationType
+ FileVersion, NotificationTypeEnum
)
from osf.metrics import PreprintView, PreprintDownload
from osf.utils import permissions
@@ -575,14 +575,13 @@ def create_waterbutler_log(payload, **kwargs):
if payload.get('email') or payload.get('errors'):
if payload.get('email'):
- notification_type = NotificationType.Type.USER_FILE_OPERATION_SUCCESS.instance
+ notification_type = NotificationTypeEnum.USER_FILE_OPERATION_SUCCESS.instance
if payload.get('errors'):
- notification_type = NotificationType.Type.USER_FILE_OPERATION_FAILED.instance
+ notification_type = NotificationTypeEnum.USER_FILE_OPERATION_FAILED.instance
notification_type.emit(
user=user,
subscribed_object=node,
event_context={
- 'user_fullname': user.fullname,
'action': payload['action'],
'source_node': source_node._id,
'source_node_title': source_node.title,
diff --git a/addons/boa/tasks.py b/addons/boa/tasks.py
index 283867d8489..afbb89f8961 100644
--- a/addons/boa/tasks.py
+++ b/addons/boa/tasks.py
@@ -14,7 +14,7 @@
from addons.boa.boa_error_code import BoaErrorCode
from framework import sentry
from framework.celery_tasks import app as celery_app
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationTypeEnum
from osf.utils.fields import ensure_str, ensure_bytes
from website import settings as osf_settings
@@ -183,18 +183,15 @@ async def submit_to_boa_async(host, username, password, user_guid, project_guid,
logger.info('Successfully uploaded query output to OSF.')
logger.debug('Task ends <<<<<<<<')
- NotificationType.Type.ADDONS_BOA_JOB_COMPLETE.instance.emit(
+ NotificationTypeEnum.ADDONS_BOA_JOB_COMPLETE.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
- 'query_file_name': query_file_name,
'query_file_full_path': file_full_path,
'output_file_name': output_file_name,
'job_id': boa_job.id,
'project_url': project_url,
'boa_job_list_url': boa_settings.BOA_JOB_LIST_URL,
- 'boa_support_email': boa_settings.BOA_SUPPORT_EMAIL,
- 'osf_support_email': osf_settings.OSF_SUPPORT_EMAIL,
}
)
return BoaErrorCode.NO_ERROR
@@ -209,12 +206,11 @@ def handle_boa_error(message, code, username, fullname, project_url, query_file_
sentry.log_message(message, skip_session=True)
except Exception:
pass
- NotificationType.Type.ADDONS_BOA_JOB_FAILURE.instance.emit(
+ NotificationTypeEnum.ADDONS_BOA_JOB_FAILURE.instance.emit(
destination_address=username,
event_context={
'user_fullname': fullname,
'code': code,
- 'query_file_name': query_file_name,
'file_size': file_size,
'message': message,
'max_file_size': boa_settings.MAX_SUBMISSION_SIZE,
diff --git a/addons/boa/tests/test_tasks.py b/addons/boa/tests/test_tasks.py
index 814dc3e2f57..38cc6eba11d 100644
--- a/addons/boa/tests/test_tasks.py
+++ b/addons/boa/tests/test_tasks.py
@@ -9,7 +9,7 @@
from addons.boa import settings as boa_settings
from addons.boa.boa_error_code import BoaErrorCode
from addons.boa.tasks import submit_to_boa, submit_to_boa_async, handle_boa_error
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import AuthUserFactory, ProjectFactory
from tests.base import OsfTestCase
from tests.utils import capture_notifications
@@ -66,7 +66,7 @@ def test_handle_boa_error(self):
job_id=self.job_id
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDONS_BOA_JOB_FAILURE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDONS_BOA_JOB_FAILURE
mock_sentry_log_message.assert_called_with(self.error_message, skip_session=True)
mock_logger_error.assert_called_with(self.error_message)
assert return_value == BoaErrorCode.UNKNOWN
diff --git a/addons/osfstorage/tests/test_models.py b/addons/osfstorage/tests/test_models.py
index 9fbae6a7fa8..e3972ef04af 100644
--- a/addons/osfstorage/tests/test_models.py
+++ b/addons/osfstorage/tests/test_models.py
@@ -9,7 +9,7 @@
from framework.auth import Auth
from addons.osfstorage.models import OsfStorageFile, OsfStorageFileNode, OsfStorageFolder
-from osf.models import BaseFileNode, NotificationType
+from osf.models import BaseFileNode, NotificationTypeEnum
from osf.exceptions import ValidationError
from osf.utils.permissions import WRITE, ADMIN
@@ -750,7 +750,7 @@ def test_after_fork_copies_versions(self, node, node_settings, auth_obj):
fork = node.fork_node(auth_obj)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
fork_node_settings = fork.get_addon('osfstorage')
fork_node_settings.reload()
diff --git a/admin/management/urls.py b/admin/management/urls.py
index 6b84a854598..d583deb2ce0 100644
--- a/admin/management/urls.py
+++ b/admin/management/urls.py
@@ -17,5 +17,7 @@
re_path(r'^ingest_cedar_metadata_templates', views.IngestCedarMetadataTemplates.as_view(), name='ingest_cedar_metadata_templates'),
re_path(r'^bulk_resync', views.BulkResync.as_view(), name='bulk-resync'),
re_path(r'^empty_metadata_dataarchive_registration_bulk_resync', views.EmptyMetadataDataarchiveRegistrationBulkResync.as_view(),
- name='empty-metadata-dataarchive-registration-bulk-resync')
+ name='empty-metadata-dataarchive-registration-bulk-resync'),
+ re_path(r'^sync_notification_templates', views.SyncNotificationTemplates.as_view(),
+ name='sync_notification_templates')
]
diff --git a/admin/management/views.py b/admin/management/views.py
index d97e4f4b894..36f3d893f24 100644
--- a/admin/management/views.py
+++ b/admin/management/views.py
@@ -11,6 +11,7 @@
from osf.management.commands.monthly_reporters_go import monthly_reporters_go
from osf.management.commands.fetch_cedar_metadata_templates import ingest_cedar_metadata_templates
from osf.management.commands.sync_doi_metadata import sync_doi_metadata, sync_doi_empty_metadata_dataarchive_registrations
+from osf.management.commands.populate_notification_types import populate_notification_types
from scripts.find_spammy_content import manage_spammy_content
from django.urls import reverse
from django.shortcuts import redirect
@@ -172,3 +173,11 @@ def post(self, request):
})
messages.success(request, 'Resyncing with DataCite! It will take some time.')
return redirect(reverse('management:commands'))
+
+
+class SyncNotificationTemplates(ManagementCommandPermissionView):
+
+ def post(self, request):
+ populate_notification_types()
+ messages.success(request, 'Notification templates have been successfully synced.')
+ return redirect(reverse('management:commands'))
diff --git a/admin/providers/views.py b/admin/providers/views.py
index 20f8383dc69..ae0d74a05f8 100644
--- a/admin/providers/views.py
+++ b/admin/providers/views.py
@@ -1,7 +1,7 @@
from django.shortcuts import redirect
from django.views.generic import TemplateView
from django.contrib import messages
-from osf.models import RegistrationProvider, OSFUser, CollectionProvider, NotificationType
+from osf.models import RegistrationProvider, OSFUser, CollectionProvider, NotificationTypeEnum
from website.settings import DOMAIN
@@ -63,7 +63,7 @@ def post(self, request, *args, **kwargs):
context['provider_url'] = f'{provider.domain or DOMAIN}{provider_type_word}/{(provider._id if not provider.domain else '').strip('/')}'
messages.success(request, f'The following {target_type} was successfully added: {target_user.fullname} ({target_user.username})')
- notification_type = NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ notification_type = NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
notification_type.instance.emit(
user=target_user,
event_context=context,
diff --git a/admin/templates/management/commands.html b/admin/templates/management/commands.html
index c596e876b1c..dd90affd5ff 100644
--- a/admin/templates/management/commands.html
+++ b/admin/templates/management/commands.html
@@ -152,6 +152,19 @@
Resync empty metadata dataarchive registrations with DataCite
+
+ Sync Notification Templates
+
+ Use this management command to sync notification templates. Warning: existing templates modifications via django admin will be overridden if they haven't been updated in code. In addition, templates are cached for 2 hours so changes won't be effective immediately.
+
+
+
{% endblock %}
diff --git a/admin/users/views.py b/admin/users/views.py
index 1584c78158e..fb0cabda15a 100644
--- a/admin/users/views.py
+++ b/admin/users/views.py
@@ -20,7 +20,7 @@
from osf.models.base import Guid
from osf.models.user import OSFUser
from osf.models.spam import SpamStatus
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from framework.auth import get_user
from framework.auth.core import generate_verification_key
@@ -184,12 +184,11 @@ def post(self, request, *args, **kwargs):
message=f'User account {user.pk} disabled',
action_flag=USER_REMOVED
)
- NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE.instance.emit(
+ NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
'contact_email': OSF_SUPPORT_EMAIL,
- 'can_change_preferences': False,
}
)
else:
diff --git a/admin_tests/preprints/test_views.py b/admin_tests/preprints/test_views.py
index 2cdcda136d1..efd571a5772 100644
--- a/admin_tests/preprints/test_views.py
+++ b/admin_tests/preprints/test_views.py
@@ -8,7 +8,7 @@
from django.contrib.messages.storage.fallback import FallbackStorage
from tests.base import AdminTestCase
-from osf.models import Preprint, PreprintLog, PreprintRequest, NotificationType
+from osf.models import Preprint, PreprintLog, PreprintRequest, NotificationTypeEnum
from framework.auth import Auth
from osf_tests.factories import (
AuthUserFactory,
@@ -719,7 +719,7 @@ def test_can_unwithdraw_preprint_without_moderation_workflow(self, withdrawal_re
machine_state=DefaultStates.INITIAL.value)
withdrawal_request.run_submit(admin)
- with assert_notification(type=NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_APPROVED):
+ with assert_notification(type=NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_APPROVED):
withdrawal_request.run_accept(admin, withdrawal_request.comment)
assert preprint.machine_state == 'withdrawn'
diff --git a/admin_tests/users/test_views.py b/admin_tests/users/test_views.py
index a8ccb5618a8..0e4b6325970 100644
--- a/admin_tests/users/test_views.py
+++ b/admin_tests/users/test_views.py
@@ -27,7 +27,7 @@
from admin.users.forms import UserSearchForm, MergeUserForm
from osf.models.admin_log_entry import AdminLogEntry
from tests.utils import assert_notification, capture_notifications
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
pytestmark = pytest.mark.django_db
@@ -105,7 +105,7 @@ def test_correct_view_permissions(self):
response = views.ResetPasswordView.as_view()(request, guid=guid)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORGOT_PASSWORD
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORGOT_PASSWORD
self.assertEqual(response.status_code, 302)
@@ -168,7 +168,7 @@ def setUp(self):
def test_disable_user(self):
settings.ENABLE_EMAIL_SUBSCRIPTIONS = False
count = AdminLogEntry.objects.count()
- with assert_notification(type=NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE, user=self.user):
+ with assert_notification(type=NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE, user=self.user):
self.view().post(self.request)
self.user.reload()
assert self.user.is_disabled
@@ -176,7 +176,7 @@ def test_disable_user(self):
def test_reactivate_user(self):
settings.ENABLE_EMAIL_SUBSCRIPTIONS = False
- with assert_notification(type=NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE, user=self.user):
+ with assert_notification(type=NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE, user=self.user):
self.view().post(self.request)
count = AdminLogEntry.objects.count()
self.view().post(self.request)
@@ -206,7 +206,7 @@ def test_correct_view_permissions(self):
change_permission = Permission.objects.get(codename='change_osfuser')
user.user_permissions.add(change_permission)
user.save()
- with assert_notification(type=NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE, user=user):
+ with assert_notification(type=NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE, user=user):
request = RequestFactory().post(reverse('users:disable', kwargs={'guid': guid}))
request.user = user
diff --git a/api/crossref/views.py b/api/crossref/views.py
index 333ce1e48f7..9baeb37d562 100644
--- a/api/crossref/views.py
+++ b/api/crossref/views.py
@@ -6,7 +6,7 @@
from rest_framework.views import APIView
from api.crossref.permissions import RequestComesFromMailgun
-from osf.models import Preprint, NotificationType
+from osf.models import Preprint, NotificationTypeEnum
from website import settings
from website.preprints.tasks import mint_doi_on_crossref_fail
@@ -78,7 +78,7 @@ def post(self, request):
if unexpected_errors:
batch_id = crossref_email_content.find('batch_id').text
email_error_text = request.POST['body-plain']
- NotificationType.Type.DESK_CROSSREF_ERROR.instance.emit(
+ NotificationTypeEnum.DESK_CROSSREF_ERROR.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
'batch_id': batch_id,
diff --git a/api/institutions/authentication.py b/api/institutions/authentication.py
index 7824b3380a4..ed7ad96d857 100644
--- a/api/institutions/authentication.py
+++ b/api/institutions/authentication.py
@@ -21,7 +21,7 @@
from osf import features
from osf.exceptions import InstitutionAffiliationStateError
-from osf.models import Institution, NotificationType
+from osf.models import Institution, NotificationTypeEnum
from osf.models.institution import SsoFilterCriteriaAction
from website.settings import OSF_SUPPORT_EMAIL, DOMAIN
@@ -348,11 +348,10 @@ def authenticate(self, request):
user.save()
# Send confirmation email for all three: created, confirmed and claimed
- NotificationType.Type.USER_WELCOME_OSF4I.instance.emit(
+ NotificationTypeEnum.USER_WELCOME_OSF4I.instance.emit(
user=user,
event_context={
'domain': DOMAIN,
- 'osf_support_email': OSF_SUPPORT_EMAIL,
'user_fullname': user.fullname,
'storage_flag_is_active': flag_is_active(request, features.STORAGE_I18N),
},
@@ -363,14 +362,12 @@ def authenticate(self, request):
if email_to_add:
assert not is_created and email_to_add == sso_email
user.emails.create(address=email_to_add)
- NotificationType.Type.USER_ADD_SSO_EMAIL_OSF4I.instance.emit(
+ NotificationTypeEnum.USER_ADD_SSO_EMAIL_OSF4I.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
'email_to_add': email_to_add,
- 'domain': DOMAIN,
'osf_support_email': OSF_SUPPORT_EMAIL,
- 'storage_flag_is_active': flag_is_active(request, features.STORAGE_I18N),
},
save=False,
)
@@ -383,17 +380,15 @@ def authenticate(self, request):
duplicate_user.remove_sso_identity_from_affiliation(institution)
if secondary_institution:
duplicate_user.remove_sso_identity_from_affiliation(secondary_institution)
- NotificationType.Type.USER_DUPLICATE_ACCOUNTS_OSF4I.instance.emit(
+ NotificationTypeEnum.USER_DUPLICATE_ACCOUNTS_OSF4I.instance.emit(
user=user,
subscribed_object=user,
event_context={
'user_fullname': user.fullname,
'user_username': user.username,
'user__id': user._id,
- 'duplicate_user_fullname': duplicate_user.fullname,
'duplicate_user_username': duplicate_user.username,
'duplicate_user__id': duplicate_user._id,
- 'domain': DOMAIN,
'osf_support_email': OSF_SUPPORT_EMAIL,
},
)
diff --git a/api/nodes/views.py b/api/nodes/views.py
index 3908d5406a3..220cc4fb5de 100644
--- a/api/nodes/views.py
+++ b/api/nodes/views.py
@@ -155,7 +155,7 @@
CedarMetadataRecord,
Preprint,
Collection,
- NotificationType,
+ NotificationTypeEnum,
)
from addons.osfstorage.models import Region
from osf.utils.permissions import ADMIN, WRITE_NODE
@@ -1069,26 +1069,22 @@ def perform_create(self, serializer):
try:
fork = serializer.save(node=node)
except Exception as exc:
- NotificationType.Type.NODE_FORK_FAILED.instance.emit(
+ NotificationTypeEnum.NODE_FORK_FAILED.instance.emit(
user=user,
subscribed_object=node,
event_context={
- 'domain': settings.DOMAIN,
'node_title': node.title,
- 'can_change_preferences': False,
},
)
raise exc
- NotificationType.Type.NODE_FORK_COMPLETED.instance.emit(
+ NotificationTypeEnum.NODE_FORK_COMPLETED.instance.emit(
user=user,
subscribed_object=node,
event_context={
'domain': settings.DOMAIN,
'node_title': node.title,
- 'fork_title': fork.title,
'fork__id': fork._id,
- 'can_change_preferences': False,
},
)
diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py
index 5afa20e8413..4c9c9214924 100644
--- a/api/preprints/serializers.py
+++ b/api/preprints/serializers.py
@@ -50,7 +50,7 @@
PreprintProvider,
Node,
NodeLicense,
- NotificationType,
+ NotificationTypeEnum,
)
from osf.utils import permissions as osf_permissions
from osf.utils.workflows import DefaultStates
@@ -480,7 +480,7 @@ def update(self, preprint, validated_data):
preprint,
contributor=author,
auth=auth,
- notification_type=NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT,
+ notification_type=NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT,
)
return preprint
diff --git a/api/providers/serializers.py b/api/providers/serializers.py
index ffc9097e2e3..3ab0e003ba2 100644
--- a/api/providers/serializers.py
+++ b/api/providers/serializers.py
@@ -10,7 +10,7 @@
from api.preprints.serializers import PreprintProviderRelationshipField
from api.providers.workflows import Workflows
from api.base.metrics import MetricsSerializerMixin
-from osf.models import CitationStyle, NotificationType, RegistrationProvider, CollectionProvider
+from osf.models import CitationStyle, NotificationTypeEnum, RegistrationProvider, CollectionProvider
from osf.models.user import Email, OSFUser
from osf.models.validators import validate_email
from osf.utils.permissions import REVIEW_GROUPS, ADMIN
@@ -385,9 +385,9 @@ def create(self, validated_data):
provider.add_to_group(user, perm_group)
setattr(user, 'permission_group', perm_group) # Allows reserialization
if 'claim_url' in context:
- notification_type = NotificationType.Type.PROVIDER_CONFIRM_EMAIL_MODERATION
+ notification_type = NotificationTypeEnum.PROVIDER_CONFIRM_EMAIL_MODERATION
else:
- notification_type = NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ notification_type = NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
notification_type.instance.emit(
user=user,
event_context=context,
diff --git a/api/providers/tasks.py b/api/providers/tasks.py
index a3b89cb9dc4..4f19821b540 100644
--- a/api/providers/tasks.py
+++ b/api/providers/tasks.py
@@ -27,7 +27,7 @@
RegistrationProvider,
RegistrationSchema,
Subject,
- NotificationType,
+ NotificationTypeEnum,
)
from osf.models.licenses import NodeLicense
from osf.models.registration_bulk_upload_job import JobState
@@ -137,7 +137,7 @@ def prepare_for_registration_bulk_creation(payload_hash, initiator_id, provider_
# Cancel the preparation task if duplicates are found in the CSV and/or in DB
if draft_error_list:
upload.delete()
- NotificationType.Type.REGISTRATION_BULK_UPLOAD_FAILURE_DUPLICATES.instance.emit(
+ NotificationTypeEnum.REGISTRATION_BULK_UPLOAD_FAILURE_DUPLICATES.instance.emit(
user=initiator,
event_context={
'user_fullname': initiator.fullname,
@@ -639,11 +639,11 @@ def bulk_upload_finish_job(upload, row_count, success_count, draft_errors, appro
if not dry_run:
upload.save()
if upload.state == JobState.DONE_FULL:
- notification_type = NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL
+ notification_type = NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL
elif upload.state == JobState.DONE_PARTIAL:
- notification_type = NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL
+ notification_type = NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL
elif upload.state == JobState.DONE_ERROR:
- notification_type = NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL
+ notification_type = NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL
else:
logger.error(f'Unexpected job state for upload [{upload.id}]: {upload.state.name}')
sentry.log_message(f'Unexpected job state for upload [{upload.id}]: {upload.state.name}')
@@ -653,13 +653,11 @@ def bulk_upload_finish_job(upload, row_count, success_count, draft_errors, appro
user=initiator,
event_context={
'user_fullname': initiator.fullname,
- 'initiator_fullname': initiator.fullname,
'auto_approval': auto_approval,
'count': row_count,
'total': row_count,
'pending_submissions_url': f'{get_registration_provider_submissions_url(provider)}?status=pending',
'draft_errors': draft_errors,
- 'approval_errors': approval_errors,
'successes': success_count,
'failures': len(draft_errors),
'osf_support_email': settings.OSF_SUPPORT_EMAIL,
@@ -680,7 +678,7 @@ def handle_internal_error(initiator=None, provider=None, message=None, dry_run=T
if not dry_run:
if initiator:
- NotificationType.Type.DESK_USER_REGISTRATION_BULK_UPLOAD_UNEXPECTED_FAILURE.instance.emit(
+ NotificationTypeEnum.DESK_USER_REGISTRATION_BULK_UPLOAD_UNEXPECTED_FAILURE.instance.emit(
user=initiator,
event_context={
'initiator_fullname': initiator.fullname,
@@ -700,7 +698,7 @@ def inform_product_of_errors(initiator=None, provider=None, message=None):
user_info = f'{initiator._id}, {initiator.fullname}, {initiator.username}' if initiator else 'UNIDENTIFIED'
provider_name = provider.name if provider else 'UNIDENTIFIED'
- NotificationType.Type.DESK_REGISTRATION_BULK_UPLOAD_PRODUCT_OWNER.instance.emit(
+ NotificationTypeEnum.DESK_REGISTRATION_BULK_UPLOAD_PRODUCT_OWNER.instance.emit(
destination_address=email,
event_context={
'user': user_info,
diff --git a/api/requests/serializers.py b/api/requests/serializers.py
index cca3f713094..c276a23f825 100644
--- a/api/requests/serializers.py
+++ b/api/requests/serializers.py
@@ -15,7 +15,7 @@
PreprintRequest,
Institution,
OSFUser,
- NotificationType,
+ NotificationTypeEnum,
)
from osf.utils.workflows import DefaultStates, RequestTypes, NodeRequestTypes
from osf.utils import permissions as osf_permissions
@@ -188,7 +188,7 @@ def make_node_institutional_access_request(self, node, validated_data) -> NodeRe
comment = validated_data.get('comment', '').strip() or language.EMPTY_REQUEST_INSTITUTIONAL_ACCESS_REQUEST_TEXT
- NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST.instance.emit(
+ NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST.instance.emit(
user=recipient,
subscribed_object=node_request.target,
event_context={
@@ -196,10 +196,9 @@ def make_node_institutional_access_request(self, node, validated_data) -> NodeRe
'sender_absolute_url': sender.absolute_url,
'node_absolute_url': node_request.target.absolute_url,
'node_title': node_request.target.title,
- 'recipient_fullname': recipient.username if recipient else None,
+ 'recipient_username': recipient.username if recipient else None,
'comment': comment,
'domain': settings.DOMAIN,
- 'institution_name': institution.name if institution else None,
},
email_context={
'bcc_addr': [sender.username] if validated_data['bcc_sender'] else None,
diff --git a/api/subscriptions/utils.py b/api/subscriptions/utils.py
new file mode 100644
index 00000000000..46b1e927d63
--- /dev/null
+++ b/api/subscriptions/utils.py
@@ -0,0 +1,82 @@
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied
+
+from rest_framework.exceptions import NotFound
+
+from framework import sentry
+
+from osf.models import AbstractNode, OSFUser
+from osf.models.notification_type import NotificationTypeEnum
+from osf.models.notification_subscription import NotificationSubscription
+
+
+def create_missing_notification_from_legacy_id(legacy_id, user):
+ """
+ `global_file_updated` and `global_reviews` should exist by default for every user, and `_files_update`
+ should exist by default if user is a contributor of the node. If not found, create them with `none` frequency
+ and `_is_digest=True` as default. Raise error if not found, not authorized or permission denied.
+ """
+
+ node_ct = ContentType.objects.get_for_model(AbstractNode)
+ user_ct = ContentType.objects.get_for_model(OSFUser)
+
+ user_file_updated_nt = NotificationTypeEnum.USER_FILE_UPDATED.instance
+ reviews_submission_status_nt = NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.instance
+ node_file_updated_nt = NotificationTypeEnum.NODE_FILE_UPDATED.instance
+
+ node_guid = 'n/a'
+
+ if legacy_id == f'{user._id}_global_file_updated':
+ notification_type = user_file_updated_nt
+ content_type = user_ct
+ object_id = user.id
+ elif legacy_id == f'{user._id}_global_reviews':
+ notification_type = reviews_submission_status_nt
+ content_type = user_ct
+ object_id = user.id
+ elif legacy_id.endswith('_global_file_updated') or legacy_id.endswith('_global_reviews'):
+ # Mismatched request user and subscription user
+ sentry.log_message(f'Permission denied: [user={user._id}, legacy_id={legacy_id}]')
+ raise PermissionDenied
+ # `_files_update` should exist by default if user is a contributor of the node.
+ # If not found, create them with `none` frequency and `_is_digest=True` as default.
+ elif legacy_id.endswith('_file_updated'):
+ notification_type = node_file_updated_nt
+ content_type = node_ct
+ node_guid = legacy_id[:-len('_file_updated')]
+ node = AbstractNode.objects.filter(guids___id=node_guid, is_deleted=False, type='osf.node').first()
+ if not node:
+ # The node in the legacy subscription ID does not exist or is invalid
+ sentry.log_message(
+ f'Node not found in legacy subscription ID: [user={user._id}, legacy_id={legacy_id}]',
+ )
+ raise NotFound
+ if not node.is_contributor(user):
+ # The request user is not a contributor of the node
+ sentry.log_message(
+ f'Permission denied: [user={user._id}], node={node_guid}, legacy_id={legacy_id}]',
+ )
+ raise PermissionDenied
+ object_id = node.id
+ else:
+ sentry.log_message(f'Subscription not found: [user={user._id}, legacy_id={legacy_id}]')
+ raise NotFound
+ missing_subscription_created = NotificationSubscription.objects.create(
+ notification_type=notification_type,
+ user=user,
+ content_type=content_type,
+ object_id=object_id,
+ _is_digest=True,
+ message_frequency='none',
+ )
+ sentry.log_message(
+ f'Missing default subscription has been created: [user={user._id}], node={node_guid} type={notification_type}, legacy_id={legacy_id}]',
+ )
+ return missing_subscription_created
+
+def create_missing_notifications_from_event_name(filter_event_names, user):
+ # Note: this may not be needed since 1) missing node subscriptions are created in the LIST view when filter by
+ # legacy ID, and 2) missing user global subscriptions are created in DETAILS view with legacy ID. However, log
+ # this message to sentry for tracking how often this happens.
+ sentry.log_message(f'Detected empty subscription list when filter by event names: [event={filter_event_names}, user={user._id}]')
+ return None
diff --git a/api/subscriptions/views.py b/api/subscriptions/views.py
index 953e2a66aec..2cf3b881b11 100644
--- a/api/subscriptions/views.py
+++ b/api/subscriptions/views.py
@@ -1,13 +1,15 @@
-from django.db.models import Value, When, Case, OuterRef, Subquery
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied
+from django.db.models import Value, When, Case, OuterRef, Subquery, F
from django.db.models.fields import CharField, IntegerField
from django.db.models.functions import Concat, Cast
-from django.contrib.contenttypes.models import ContentType
+
from rest_framework import generics
from rest_framework import permissions as drf_permissions
-from rest_framework.exceptions import NotFound
-from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from rest_framework.response import Response
+
from framework.auth.oauth_scopes import CoreScopes
+
from api.base.views import JSONAPIBaseView
from api.base.filters import ListFilterMixin
from api.base import permissions as base_permissions
@@ -18,6 +20,8 @@
RegistrationSubscriptionSerializer,
)
from api.subscriptions.permissions import IsSubscriptionOwner
+from api.subscriptions import utils
+
from osf.models import (
CollectionProvider,
PreprintProvider,
@@ -25,8 +29,9 @@
AbstractProvider,
AbstractNode,
Guid,
+ OSFUser,
)
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf.models.notification_subscription import NotificationSubscription
@@ -44,63 +49,79 @@ class SubscriptionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
required_write_scopes = [CoreScopes.NULL]
def get_queryset(self):
+
+ user = self.request.user
user_guid = self.request.user._id
+ filter_id = self.request.query_params.get('filter[id]')
+ filter_event_name = self.request.query_params.get('filter[event_name]')
+
+ provider_ct = ContentType.objects.get_for_model(AbstractProvider)
+ node_ct = ContentType.objects.get_for_model(AbstractNode)
+ user_ct = ContentType.objects.get_for_model(OSFUser)
+
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,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.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,
+ _global_reviews_provider = [
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ ]
+ _global_reviews_user = [
+ NotificationTypeEnum.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,
- NotificationType.Type.FILE_UPDATED.value,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
]
- qs = NotificationSubscription.objects.filter(
- 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,
+ full_set_of_types = _global_reviews_provider + _global_reviews_user + _global_file_updated + _node_file_updated
+ annotated_qs = NotificationSubscription.objects.filter(
+ notification_type__name__in=full_set_of_types,
+ user=user,
).annotate(
event_name=Case(
When(
notification_type__name__in=_node_file_updated,
- then=Value('files_updated'),
+ content_type=node_ct,
+ then=Value('file_updated'),
),
When(
notification_type__name__in=_global_file_updated,
+ content_type=user_ct,
then=Value('global_file_updated'),
),
When(
- notification_type__name__in=_global_reviews,
+ notification_type__name__in=_global_reviews_provider,
+ content_type=provider_ct,
+ then=Value('global_reviews'),
+ ),
+ When(
+ notification_type__name__in=_global_reviews_user,
+ content_type=user_ct,
then=Value('global_reviews'),
),
- default=Value('notification_type__name'),
+ default=F('notification_type__name'),
),
legacy_id=Case(
When(
@@ -112,24 +133,44 @@ def get_queryset(self):
then=Value(f'{user_guid}_global_file_updated'),
),
When(
- notification_type__name__in=_global_reviews,
+ notification_type__name__in=_global_reviews_provider,
+ content_type=provider_ct,
then=Value(f'{user_guid}_global_reviews'),
),
- default=Value('notification_type__name'),
+ When(
+ notification_type__name__in=_global_reviews_user,
+ content_type=user_ct,
+ then=Value(f'{user_guid}_global_reviews'),
+ ),
+ default=F('notification_type__name'),
),
).distinct('legacy_id')
+ return_qs = annotated_qs
+
# Apply manual filter for legacy_id if requested
- filter_id = self.request.query_params.get('filter[id]')
if filter_id:
- qs = qs.filter(legacy_id=filter_id)
- # convert to list comprehension because legacy_id is an annotation, not in DB
+ return_qs = annotated_qs.filter(legacy_id=filter_id)
+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
+ # NOTE: `.exists()` errors for unknown reason, possibly due to complex annotation with `.distinct()`
+ if return_qs.count() == 0:
+ missing_subscription_created = utils.create_missing_notification_from_legacy_id(filter_id, user)
+ if missing_subscription_created:
+ return_qs = annotated_qs.filter(legacy_id=filter_id)
+ # `filter_id` takes priority over `filter_event_name`
+ return return_qs
+
# Apply manual filter for event_name if requested
- filter_event_name = self.request.query_params.get('filter[event_name]')
if filter_event_name:
- qs = qs.filter(event_name__in=filter_event_name.split(','))
+ filter_event_names = filter_event_name.split(',')
+ return_qs = annotated_qs.filter(event_name__in=filter_event_names)
+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
+ # NOTE: `.exists()` errors for unknown reason, possibly due to complex annotation with `.distinct()`
+ if return_qs.count() == 0:
+ utils.create_missing_notifications_from_event_name(filter_event_names, user)
+
+ return return_qs
- return qs
class AbstractProviderSubscriptionList(SubscriptionList):
def get_queryset(self):
@@ -155,66 +196,74 @@ class SubscriptionDetail(JSONAPIBaseView, generics.RetrieveUpdateAPIView):
def get_object(self):
subscription_id = self.kwargs['subscription_id']
+ user = self.request.user
user_guid = self.request.user._id
-
- provider_ct = ContentType.objects.get(app_label='osf', model='abstractprovider')
- node_ct = ContentType.objects.get(app_label='osf', model='abstractnode')
+ user_ct = ContentType.objects.get_for_model(OSFUser)
+ node_ct = ContentType.objects.get_for_model(AbstractNode)
node_subquery = AbstractNode.objects.filter(
id=Cast(OuterRef('object_id'), IntegerField()),
).values('guids___id')[:1]
- try:
- annotated_obj_qs = NotificationSubscription.objects.filter(user=self.request.user).annotate(
- legacy_id=Case(
- When(
- notification_type__name=NotificationType.Type.NODE_FILE_UPDATED.value,
- content_type=node_ct,
- then=Concat(Subquery(node_subquery), Value('_files_updated')),
- ),
- When(
- notification_type__name=NotificationType.Type.USER_FILE_UPDATED.value,
- then=Value(f'{user_guid}_global_file_updated'),
- ),
- When(
- notification_type__name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
- content_type=provider_ct,
- then=Value(f'{user_guid}_global_reviews'),
- ),
- default=Value(f'{user_guid}_global'),
- output_field=CharField(),
+ missing_subscription_created = None
+ annotated_obj_qs = NotificationSubscription.objects.filter(user=user).annotate(
+ legacy_id=Case(
+ When(
+ notification_type__name=NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ content_type=node_ct,
+ then=Concat(Subquery(node_subquery), Value('_file_updated')),
),
- )
- obj = annotated_obj_qs.filter(legacy_id=subscription_id)
-
- except ObjectDoesNotExist:
- raise NotFound
-
- obj = obj.filter(user=self.request.user).first()
- if not obj:
+ When(
+ notification_type__name=NotificationTypeEnum.USER_FILE_UPDATED.value,
+ then=Value(f'{user_guid}_global_file_updated'),
+ ),
+ When(
+ notification_type__name=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
+ content_type=user_ct,
+ then=Value(f'{user_guid}_global_reviews'),
+ ),
+ default=Value(f'{user_guid}_global'),
+ output_field=CharField(),
+ ),
+ )
+ existing_subscriptions = annotated_obj_qs.filter(legacy_id=subscription_id)
+
+ # TODO: Rework missing subscription fix after fully populating the OSF DB with all missing notifications
+ if not existing_subscriptions.exists():
+ missing_subscription_created = utils.create_missing_notification_from_legacy_id(subscription_id, user)
+ if missing_subscription_created:
+ # Note: must use `annotated_obj_qs` to insert `legacy_id` so that `SubscriptionSerializer` can build data
+ # properly; in addition, there should be only one result
+ subscription = annotated_obj_qs.get(legacy_id=subscription_id)
+ else:
+ # TODO: Use `get()` and fails/warns on multiple objects after fully de-duplicating the OSF DB
+ subscription = existing_subscriptions.order_by('id').last()
+ if not subscription:
raise PermissionDenied
- self.check_object_permissions(self.request, obj)
- return obj
+ self.check_object_permissions(self.request, subscription)
+ return subscription
def update(self, request, *args, **kwargs):
"""
Update a notification subscription
"""
+ self.get_object()
+
if '_global_file_updated' in self.kwargs['subscription_id']:
# Copy _global_file_updated subscription changes to all file subscriptions
qs = NotificationSubscription.objects.filter(
user=self.request.user,
notification_type__name__in=[
- NotificationType.Type.USER_FILE_UPDATED.value,
- NotificationType.Type.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,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
],
).exclude(content_type=ContentType.objects.get_for_model(AbstractNode))
if not qs.exists():
@@ -230,11 +279,11 @@ def update(self, request, *args, **kwargs):
qs = NotificationSubscription.objects.filter(
user=self.request.user,
notification_type__name__in=[
- 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,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
],
)
if not qs.exists():
@@ -245,24 +294,24 @@ 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
+ elif '_file_updated' in self.kwargs['subscription_id']:
+ # Copy _file_updated subscription changes to all node file subscriptions
+ node_id = Guid.load(self.kwargs['subscription_id'].split('_file_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_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,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
],
)
if not qs.exists():
diff --git a/api/users/serializers.py b/api/users/serializers.py
index 023ce24442d..608f93c98c0 100644
--- a/api/users/serializers.py
+++ b/api/users/serializers.py
@@ -34,7 +34,7 @@
from api.nodes.serializers import NodeSerializer, RegionRelationshipField
from framework.auth.views import send_confirm_email_async
from osf.exceptions import ValidationValueError, ValidationError, BlockedEmailError
-from osf.models import Email, Node, OSFUser, Preprint, Registration, UserMessage, Institution, NotificationType
+from osf.models import Email, Node, OSFUser, Preprint, Registration, UserMessage, Institution, NotificationTypeEnum
from osf.models.user_message import MessageTypes
from osf.models.provider import AbstractProviderGroupObjectPermission
from osf.utils.requests import string_type_request_headers
@@ -737,14 +737,13 @@ def update(self, instance, validated_data):
if primary and instance.confirmed:
user.username = instance.address
user.save()
- notification_type = NotificationType.Type.USER_PRIMARY_EMAIL_CHANGED
+ notification_type = NotificationTypeEnum.USER_PRIMARY_EMAIL_CHANGED
notification_type.instance.emit(
subscribed_object=user,
user=user,
event_context={
'user_fullname': user.fullname,
'new_address': user.username,
- 'can_change_preferences': False,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
},
)
diff --git a/api/users/services.py b/api/users/services.py
index 9237f0b1d9f..3b93b230804 100644
--- a/api/users/services.py
+++ b/api/users/services.py
@@ -2,7 +2,7 @@
from django.utils import timezone
from framework.auth.core import generate_verification_key
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from website import settings
@@ -15,14 +15,13 @@ def send_password_reset_email(user, email, verification_type='password', institu
user.save()
reset_link = furl(settings.DOMAIN).add(path=f'resetpassword/{user._id}/{user.verification_key_v2["token"]}').url
- notification_type = NotificationType.Type.USER_FORGOT_PASSWORD_INSTITUTION if institutional \
- else NotificationType.Type.USER_FORGOT_PASSWORD
+ notification_type = NotificationTypeEnum.USER_FORGOT_PASSWORD_INSTITUTION if institutional \
+ else NotificationTypeEnum.USER_FORGOT_PASSWORD
notification_type.instance.emit(
destination_address=email,
event_context={
'reset_link': reset_link,
- 'can_change_preferences': False,
**mail_kwargs,
},
)
diff --git a/api/users/views.py b/api/users/views.py
index 6485fc625ad..b920798be86 100644
--- a/api/users/views.py
+++ b/api/users/views.py
@@ -99,7 +99,7 @@
OSFUser,
Email,
Tag,
- NotificationType,
+ NotificationTypeEnum,
PreprintProvider,
)
from osf.utils.tokens import TokenHandler
@@ -645,14 +645,13 @@ def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.get_user()
- NotificationType.Type.DESK_REQUEST_EXPORT.instance.emit(
+ NotificationTypeEnum.DESK_REQUEST_EXPORT.instance.emit(
user=user,
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
'user_username': user.username,
'user_absolute_url': user.absolute_url,
'user__id': user._id,
- 'can_change_preferences': False,
},
)
user.email_last_sent = timezone.now()
@@ -857,15 +856,14 @@ def get(self, request, *args, **kwargs):
user_obj.save()
reset_link = f'{settings.DOMAIN}resetpassword/{user_obj._id}/{user_obj.verification_key_v2["token"]}/'
if institutional:
- notification_type = NotificationType.Type.USER_FORGOT_PASSWORD_INSTITUTION
+ notification_type = NotificationTypeEnum.USER_FORGOT_PASSWORD_INSTITUTION
else:
- notification_type = NotificationType.Type.USER_FORGOT_PASSWORD
+ notification_type = NotificationTypeEnum.USER_FORGOT_PASSWORD
notification_type.instance.emit(
user=user_obj,
message_frequency='instantly',
event_context={
- 'can_change_preferences': False,
'reset_link': reset_link,
},
)
@@ -1168,12 +1166,11 @@ def _process_external_identity(self, user, external_identity, service_url):
if external_status == 'CREATE':
service_url += '&' + urlencode({'new': 'true'})
elif external_status == 'LINK':
- NotificationType.Type.USER_EXTERNAL_LOGIN_LINK_SUCCESS.instance.emit(
+ NotificationTypeEnum.USER_EXTERNAL_LOGIN_LINK_SUCCESS.instance.emit(
user=user,
message_frequency='instantly',
event_context={
'user_fullname': user.fullname,
- 'can_change_preferences': False,
'external_id_provider': provider,
},
)
@@ -1490,11 +1487,10 @@ def post(self, request, *args, **kwargs):
if external_status == 'CREATE':
service_url += '&{}'.format(urlencode({'new': 'true'}))
elif external_status == 'LINK':
- NotificationType.Type.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK.instance.emit(
+ NotificationTypeEnum.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK.instance.emit(
user=user,
message_frequency='instantly',
event_context={
- 'can_change_preferences': False,
'external_id_provider': provider.name,
},
)
diff --git a/api_tests/actions/views/test_action_list.py b/api_tests/actions/views/test_action_list.py
index 2881706ba7c..fc5de8a1d02 100644
--- a/api_tests/actions/views/test_action_list.py
+++ b/api_tests/actions/views/test_action_list.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
PreprintFactory,
AuthUserFactory,
@@ -193,8 +193,8 @@ def test_accept_permissions_accept(self, app, url, preprint, node_admin, moderat
with capture_notifications() as notifications:
res = app.post_json_api(url, accept_payload, auth=moderator.auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.REVIEWS_SUBMISSION_STATUS
- assert notifications['emits'][1]['type'] == NotificationType.Type.REVIEWS_SUBMISSION_STATUS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
assert res.status_code == 201
preprint.refresh_from_db()
assert preprint.machine_state == 'accepted'
diff --git a/api_tests/base/test_throttling.py b/api_tests/base/test_throttling.py
index a1c2921ca2f..22de7950bbd 100644
--- a/api_tests/base/test_throttling.py
+++ b/api_tests/base/test_throttling.py
@@ -1,7 +1,7 @@
from unittest import mock
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from tests.base import ApiTestCase
from osf_tests.factories import AuthUserFactory, ProjectFactory
@@ -131,7 +131,7 @@ def test_add_contrib_throttle_rate_allow_request_called(self, mock_allow):
auth=self.user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
assert res.status_code == 201
assert mock_allow.call_count == 1
diff --git a/api_tests/collection_submission_actions/views/test_collection_submissions_actions_list.py b/api_tests/collection_submission_actions/views/test_collection_submissions_actions_list.py
index 9c9fb239fb2..9de82198b96 100644
--- a/api_tests/collection_submission_actions/views/test_collection_submissions_actions_list.py
+++ b/api_tests/collection_submission_actions/views/test_collection_submissions_actions_list.py
@@ -5,7 +5,7 @@
from django.utils import timezone
from osf_tests.factories import NodeFactory, CollectionFactory, CollectionProviderFactory
-from osf.models import CollectionSubmission, NotificationType
+from osf.models import CollectionSubmission, NotificationTypeEnum
from osf.utils.workflows import CollectionSubmissionsTriggers, CollectionSubmissionStates
from tests.utils import capture_notifications
@@ -133,9 +133,9 @@ def test_status_code__collection_moderator_accept_reject_moderated(self, app, no
)
assert len(notifications['emits']) == 1
if moderator_trigger is CollectionSubmissionsTriggers.ACCEPT:
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED
if moderator_trigger is CollectionSubmissionsTriggers.REJECT:
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REJECTED
assert resp.status_code == 201
@pytest.mark.parametrize('moderator_trigger', [CollectionSubmissionsTriggers.ACCEPT, CollectionSubmissionsTriggers.REJECT])
@@ -181,13 +181,13 @@ def test_status_code__remove(self, app, node, collection_submission, user_role):
assert resp.status_code == 201
if user_role == UserRoles.MODERATOR:
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_MODERATOR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_MODERATOR
assert notifications['emits'][0]['kwargs']['user'] == collection_submission.creator
else:
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
assert notifications['emits'][0]['kwargs']['user'] == collection_submission.creator
- assert notifications['emits'][1]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
assert notifications['emits'][1]['kwargs']['user'] == node.contributors.last()
@@ -211,7 +211,7 @@ def test_POST_accept__writes_action_and_advances_state(self, app, collection_sub
with capture_notifications() as notifications:
app.post_json_api(POST_URL, payload, auth=test_auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED
user = collection_submission.collection.provider.get_group('moderator').user_set.first()
collection_submission.refresh_from_db()
@@ -230,7 +230,7 @@ def test_POST_reject__writes_action_and_advances_state(self, app, collection_sub
with capture_notifications() as notifications:
app.post_json_api(POST_URL, payload, auth=test_auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REJECTED
user = collection_submission.collection.provider.get_group('moderator').user_set.first()
collection_submission.refresh_from_db()
@@ -249,9 +249,9 @@ def test_POST_cancel__writes_action_and_advances_state(self, app, collection_sub
with capture_notifications() as notifications:
app.post_json_api(POST_URL, payload, auth=test_auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_CANCEL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_CANCEL
assert notifications['emits'][0]['kwargs']['user'] == collection_submission.creator
- assert notifications['emits'][1]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_CANCEL
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_CANCEL
assert notifications['emits'][0]['kwargs']['user'] == node.creator
collection_submission.refresh_from_db()
@@ -271,7 +271,7 @@ def test_POST_remove__writes_action_and_advances_state(self, app, collection_sub
with capture_notifications() as notifications:
app.post_json_api(POST_URL, payload, auth=test_auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_MODERATOR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_MODERATOR
user = collection_submission.collection.provider.get_group('moderator').user_set.first()
collection_submission.refresh_from_db()
action = collection_submission.actions.last()
@@ -322,7 +322,7 @@ def test_status_code__private_collection_moderator(self, app, node, collection,
expect_errors=True
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED
assert resp.status_code == 201
diff --git a/api_tests/crossref/views/test_crossref_email_response.py b/api_tests/crossref/views/test_crossref_email_response.py
index 33345f4f442..196c0debd2a 100644
--- a/api_tests/crossref/views/test_crossref_email_response.py
+++ b/api_tests/crossref/views/test_crossref_email_response.py
@@ -5,7 +5,7 @@
from django.utils import timezone
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests import factories
from tests.utils import capture_notifications
from website import settings
@@ -163,7 +163,7 @@ def test_error_response_sends_message_does_not_set_doi(self, app, url, preprint,
with capture_notifications() as notifications:
app.post(url, context_data)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_CROSSREF_ERROR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_CROSSREF_ERROR
assert not preprint.get_identifier_value('doi')
def test_success_response_sets_doi(self, app, url, preprint, success_xml):
diff --git a/api_tests/draft_registrations/views/test_draft_registration_contributor_list.py b/api_tests/draft_registrations/views/test_draft_registration_contributor_list.py
index c40886863fa..5d76ae1522b 100644
--- a/api_tests/draft_registrations/views/test_draft_registration_contributor_list.py
+++ b/api_tests/draft_registrations/views/test_draft_registration_contributor_list.py
@@ -16,7 +16,7 @@
TestNodeContributorFiltering,
)
from api_tests.nodes.views.utils import NodeCRUDTestCase
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf_tests.factories import (
DraftRegistrationFactory,
AuthUserFactory,
@@ -238,7 +238,7 @@ def test_add_contributor_sends_email(self, app, user, user_two, url_project_cont
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
# Overrides TestNodeContributorCreateEmail
def test_add_contributor_signal_if_default(
@@ -281,7 +281,7 @@ def test_add_unregistered_contributor_sends_email(self, app, user, url_project_c
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DRAFT_REGISTRATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DRAFT_REGISTRATION
# Overrides TestNodeContributorCreateEmail
def test_add_unregistered_contributor_signal_if_default(self, app, user, url_project_contribs):
@@ -300,7 +300,7 @@ def test_add_unregistered_contributor_signal_if_default(self, app, user, url_pro
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DRAFT_REGISTRATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DRAFT_REGISTRATION
# Overrides TestNodeContributorCreateEmail
def test_add_unregistered_contributor_without_email_no_email(self, app, user, url_project_contribs):
diff --git a/api_tests/draft_registrations/views/test_draft_registration_list.py b/api_tests/draft_registrations/views/test_draft_registration_list.py
index 4aed087605a..7e0ebf92e3c 100644
--- a/api_tests/draft_registrations/views/test_draft_registration_list.py
+++ b/api_tests/draft_registrations/views/test_draft_registration_list.py
@@ -6,7 +6,7 @@
from api.base.settings.defaults import API_BASE
from osf.migrations import ensure_invisible_and_inactive_schema
-from osf.models import DraftRegistration, NodeLicense, RegistrationProvider, RegistrationSchema, NotificationType
+from osf.models import DraftRegistration, NodeLicense, RegistrationProvider, RegistrationSchema, NotificationTypeEnum
from osf_tests.factories import (
RegistrationFactory,
CollectionFactory,
@@ -435,7 +435,7 @@ def test_create_no_project_draft_emails_initiator(self, app, user, url_draft_reg
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
assert notifications['emits'][0]['kwargs']['user'] == user
def test_create_draft_with_provider(
@@ -515,7 +515,7 @@ def test_draft_registration_attributes_not_copied_from_node(self, app, project_p
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
assert res.status_code == 201
attributes = res.json['data']['attributes']
assert attributes['title'] == ''
diff --git a/api_tests/institutions/views/test_institution_relationship_nodes.py b/api_tests/institutions/views/test_institution_relationship_nodes.py
index f99802dbe91..7b3fc54f05f 100644
--- a/api_tests/institutions/views/test_institution_relationship_nodes.py
+++ b/api_tests/institutions/views/test_institution_relationship_nodes.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
RegistrationFactory,
InstitutionFactory,
@@ -425,7 +425,7 @@ def test_email_sent_on_affiliation_addition(
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
def test_email_sent_on_affiliation_removal(self, app, admin, institution, node_public, url_institution_nodes):
current_institution = InstitutionFactory()
@@ -448,7 +448,7 @@ def test_email_sent_on_affiliation_removal(self, app, admin, institution, node_p
assert res.status_code == 204
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert notifications['emits'][0]['kwargs']['user'] == node_public.creator
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert notifications['emits'][1]['kwargs']['user'] == admin
diff --git a/api_tests/mailhog/provider/test_collection_submission.py b/api_tests/mailhog/provider/test_collection_submission.py
index 24e6b010d64..97de8e9b2c4 100644
--- a/api_tests/mailhog/provider/test_collection_submission.py
+++ b/api_tests/mailhog/provider/test_collection_submission.py
@@ -7,7 +7,7 @@
CollectionProviderFactory,
CollectionFactory,
)
-from osf.models import NotificationType, CollectionSubmission
+from osf.models import NotificationTypeEnum, CollectionSubmission
from tests.utils import get_mailhog_messages, delete_mailhog_messages, capture_notifications
from osf.utils.workflows import CollectionSubmissionStates
@@ -49,8 +49,8 @@ def test_notify_contributors_pending(self, node, moderated_collection):
)
collection_submission.save()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert collection_submission.state == CollectionSubmissionStates.PENDING
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
@@ -67,8 +67,8 @@ def test_notify_moderators_pending(self, node, moderated_collection):
)
collection_submission.save()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert collection_submission.state == CollectionSubmissionStates.PENDING
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
diff --git a/api_tests/mailhog/provider/test_collections_provider_moderator_list.py b/api_tests/mailhog/provider/test_collections_provider_moderator_list.py
index 635deb42c4b..49d139062b7 100644
--- a/api_tests/mailhog/provider/test_collections_provider_moderator_list.py
+++ b/api_tests/mailhog/provider/test_collections_provider_moderator_list.py
@@ -2,7 +2,7 @@
from waffle.testutils import override_switch
from osf import features
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
CollectionProviderFactory,
@@ -69,7 +69,7 @@ def test_POST_admin_success_existing_user(self, app, url, nonmoderator, moderato
with capture_notifications(passthrough=True) as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
assert res.status_code == 201
assert res.json['data']['id'] == nonmoderator._id
assert res.json['data']['attributes']['permission_group'] == 'moderator'
@@ -97,7 +97,7 @@ def test_POST_admin_failure_unreg_moderator(self, app, url, moderator, nonmodera
res = app.post_json_api(url, payload, auth=admin.auth)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_CONFIRM_EMAIL_MODERATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_CONFIRM_EMAIL_MODERATION
assert notifications['emits'][0]['kwargs']['user'].username == unreg_user['email']
massages = get_mailhog_messages()
@@ -113,7 +113,7 @@ def test_POST_admin_success_email(self, app, url, nonmoderator, moderator, admin
with capture_notifications(passthrough=True) as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_CONFIRM_EMAIL_MODERATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_CONFIRM_EMAIL_MODERATION
assert res.status_code == 201
assert len(res.json['data']['id']) == 5
assert res.json['data']['attributes']['permission_group'] == 'moderator'
diff --git a/api_tests/mailhog/provider/test_preprints.py b/api_tests/mailhog/provider/test_preprints.py
index 96d8a8c099c..7aa5fed3a9b 100644
--- a/api_tests/mailhog/provider/test_preprints.py
+++ b/api_tests/mailhog/provider/test_preprints.py
@@ -2,7 +2,7 @@
from osf import features
from framework.auth.core import Auth
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
ProjectFactory,
AuthUserFactory,
@@ -32,7 +32,7 @@ def test_creator_gets_email(self):
with capture_notifications(passthrough=True) as notifications:
self.preprint.set_published(True, auth=Auth(self.user), save=True)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
messages = get_mailhog_messages()
assert_emails(messages, notifications)
@@ -40,7 +40,7 @@ def test_creator_gets_email(self):
with capture_notifications(passthrough=True) as notifications:
self.preprint_branded.set_published(True, auth=Auth(self.user), save=True)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
messages = get_mailhog_messages()
assert_emails(messages, notifications)
diff --git a/api_tests/mailhog/provider/test_reviewable.py b/api_tests/mailhog/provider/test_reviewable.py
index c493c6a3b22..f07a874b061 100644
--- a/api_tests/mailhog/provider/test_reviewable.py
+++ b/api_tests/mailhog/provider/test_reviewable.py
@@ -1,7 +1,7 @@
import pytest
from waffle.testutils import override_switch
from osf import features
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf.utils.workflows import DefaultStates
from osf_tests.factories import PreprintFactory, AuthUserFactory
from tests.utils import get_mailhog_messages, delete_mailhog_messages, capture_notifications, assert_emails
@@ -20,7 +20,7 @@ def test_reject_resubmission_sends_emails(self):
with capture_notifications(passthrough=True) as notifications:
preprint.run_submit(user)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
assert preprint.machine_state == DefaultStates.PENDING.value
delete_mailhog_messages()
@@ -30,7 +30,7 @@ def test_reject_resubmission_sends_emails(self):
preprint.run_reject(user, 'comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.REVIEWS_SUBMISSION_STATUS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
assert preprint.machine_state == DefaultStates.REJECTED.value
massages = get_mailhog_messages()
@@ -41,7 +41,7 @@ def test_reject_resubmission_sends_emails(self):
with capture_notifications(passthrough=True) as notifications:
preprint.run_submit(user) # Resubmission alerts users and moderators
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION
assert preprint.machine_state == DefaultStates.PENDING.value
messages = get_mailhog_messages()
diff --git a/api_tests/mailhog/provider/test_schema_responses.py b/api_tests/mailhog/provider/test_schema_responses.py
index f8fec0b42bf..cab6a5a8da4 100644
--- a/api_tests/mailhog/provider/test_schema_responses.py
+++ b/api_tests/mailhog/provider/test_schema_responses.py
@@ -2,7 +2,7 @@
from waffle.testutils import override_switch
from osf import features
from api.providers.workflows import Workflows
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf.models import schema_response # import module for mocking purposes
from osf.utils.workflows import ApprovalStates
from osf_tests.factories import AuthUserFactory, ProjectFactory, RegistrationFactory, RegistrationProviderFactory
@@ -125,9 +125,9 @@ def test_submit_response_notification(
with capture_notifications(passthrough=True) as notifications:
revised_response.submit(user=admin_user, required_approvers=[admin_user])
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
assert_emails(massages, notifications)
@@ -145,9 +145,9 @@ def test_approve_response_notification(
with capture_notifications(passthrough=True) as notifications:
revised_response.approve(user=alternate_user)
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
assert_emails(massages, notifications)
@@ -164,9 +164,9 @@ def test_reject_response_notification(
with capture_notifications(passthrough=True) as notifications:
revised_response.reject(user=admin_user)
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
assert_emails(massages, notifications)
@@ -207,9 +207,9 @@ def test_accept_notification_sent_on_admin_approval(self, revised_response, admi
revised_response.approve(user=admin_user)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == moderator
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert notifications['emits'][1]['kwargs']['user'] == admin_user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
assert_emails(massages, notifications)
@@ -226,9 +226,9 @@ def test_moderators_notified_on_admin_approval(self, revised_response, admin_use
revised_response.approve(user=admin_user)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == moderator
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert notifications['emits'][1]['kwargs']['user'] == admin_user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
massages = get_mailhog_messages()
assert massages['count'] == len(notifications['emails'])
assert_emails(massages, notifications)
diff --git a/api_tests/mailhog/provider/test_submissions.py b/api_tests/mailhog/provider/test_submissions.py
index b9d26e155df..542d4410a99 100644
--- a/api_tests/mailhog/provider/test_submissions.py
+++ b/api_tests/mailhog/provider/test_submissions.py
@@ -18,7 +18,7 @@
from tests.base import get_default_metaschema
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf.migrations import update_provider_auth_groups
from tests.utils import capture_notifications, get_mailhog_messages, delete_mailhog_messages, assert_emails
@@ -82,8 +82,8 @@ def test_get_registration_actions(self, app, registration_actions_url, registrat
resp = app.get(registration_actions_url, auth=moderator.auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS
messages = get_mailhog_messages()
assert_emails(messages, notifications)
@@ -115,8 +115,8 @@ def test_get_provider_actions(self, app, provider_actions_url, registration, mod
resp = app.get(provider_actions_url, auth=moderator.auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
send_users_instant_digest_email.delay()
messages = get_mailhog_messages()
assert messages['count'] == 1
diff --git a/api_tests/mailhog/test_mailhog.py b/api_tests/mailhog/test_mailhog.py
index 998f588e452..6ef82c641a8 100644
--- a/api_tests/mailhog/test_mailhog.py
+++ b/api_tests/mailhog/test_mailhog.py
@@ -12,7 +12,7 @@
fake
)
from framework import auth
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationType, NotificationTypeEnum
from tests.base import (
OsfTestCase,
)
@@ -28,7 +28,7 @@ class TestMailHog:
def test_mailhog_received_mail(self):
delete_mailhog_messages()
- NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL.instance.emit(
+ NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL.instance.emit(
message_frequency='instantly',
destination_address='to_addr@mail.com',
event_context={
@@ -44,7 +44,7 @@ def test_mailhog_received_mail(self):
assert res['count'] == 1
assert res['items'][0]['Content']['Headers']['To'][0] == 'to_addr@mail.com'
assert res['items'][0]['Content']['Headers']['Subject'][0] == NotificationType.objects.get(
- name=NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL
+ name=NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL
).subject
delete_mailhog_messages()
diff --git a/api_tests/nodes/views/test_node_contributors_list.py b/api_tests/nodes/views/test_node_contributors_list.py
index 93ea9378f4c..854f5288a14 100644
--- a/api_tests/nodes/views/test_node_contributors_list.py
+++ b/api_tests/nodes/views/test_node_contributors_list.py
@@ -6,7 +6,7 @@
from api.base.settings.defaults import API_BASE
from api.nodes.serializers import NodeContributorsCreateSerializer
from framework.auth.core import Auth
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf_tests.factories import (
fake_email,
AuthUserFactory,
@@ -1273,7 +1273,7 @@ def test_add_contributor_signal_if_default(
res = app.post_json_api(url, payload, auth=user.auth)
args, kwargs = mock_send.call_args
assert res.status_code == 201
- assert NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT == kwargs['notification_type']
+ assert NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT == kwargs['notification_type']
def test_add_contributor_signal_preprint_email_disallowed(
self, app, user, user_two, url_project_contribs
@@ -1311,7 +1311,7 @@ def test_add_unregistered_contributor_sends_email(
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
@mock.patch('website.project.signals.unreg_contributor_added.send')
def test_add_unregistered_contributor_signal_if_default(
@@ -1333,7 +1333,7 @@ def test_add_unregistered_contributor_signal_if_default(
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
def test_add_unregistered_contributor_signal_preprint_email_disallowed(
self, app, user, url_project_contribs
diff --git a/api_tests/nodes/views/test_node_detail_update.py b/api_tests/nodes/views/test_node_detail_update.py
index de6bd55158e..b4000a31db6 100644
--- a/api_tests/nodes/views/test_node_detail_update.py
+++ b/api_tests/nodes/views/test_node_detail_update.py
@@ -8,7 +8,7 @@
from api_tests.nodes.views.utils import NodeCRUDTestCase
from api_tests.subjects.mixins import UpdateSubjectsMixin
from framework.auth.core import Auth
-from osf.models import NodeLog, NotificationType
+from osf.models import NodeLog, NotificationTypeEnum
from osf.utils.sanitize import strip_html
from osf.utils import permissions
from osf_tests.factories import (
@@ -47,7 +47,7 @@ def test_node_institution_update(self, app, user_two, project_private, url_priva
]
}
}
- with assert_notification(type=NotificationType.Type.NODE_AFFILIATION_CHANGED, user=user_two, times=2):
+ with assert_notification(type=NotificationTypeEnum.NODE_AFFILIATION_CHANGED, user=user_two, times=2):
res = app.patch_json_api(
url_private,
make_node_payload(
diff --git a/api_tests/nodes/views/test_node_forks_list.py b/api_tests/nodes/views/test_node_forks_list.py
index e51d8f95bed..e892468e1ef 100644
--- a/api_tests/nodes/views/test_node_forks_list.py
+++ b/api_tests/nodes/views/test_node_forks_list.py
@@ -3,7 +3,7 @@
from api.base.settings.defaults import API_BASE
from framework.auth.core import Auth
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf_tests.factories import (
NodeFactory,
ProjectFactory,
@@ -255,7 +255,7 @@ def test_create_fork_from_public_project_with_new_title(
fork_data_with_title,
public_project_url
):
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
public_project_url,
fork_data_with_title,
@@ -273,7 +273,7 @@ def test_create_fork_from_private_project_with_new_title(
fork_data_with_title,
private_project_url
):
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
private_project_url,
fork_data_with_title,
@@ -291,7 +291,7 @@ def test_can_fork_public_node_logged_in(
public_project_url
):
non_contrib = AuthUserFactory()
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=non_contrib):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=non_contrib):
res = app.post_json_api(
public_project_url,
fork_data,
@@ -333,7 +333,7 @@ def test_cannot_fork_errors(
def test_can_fork_public_node_logged_in_contributor(
self, app, user, public_project, fork_data, public_project_url):
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
public_project_url,
fork_data,
@@ -346,7 +346,7 @@ def test_can_fork_public_node_logged_in_contributor(
def test_can_fork_private_node_logged_in_contributor(
self, app, user, private_project, fork_data, private_project_url):
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
private_project_url +
'?embed=children&embed=node_links&embed=logs&embed=contributors&embed=forked_from',
@@ -374,7 +374,7 @@ def test_fork_private_components_no_access(
creator=user_two,
is_public=False
)
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user_three):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user_three):
res = app.post_json_api(url, fork_data, auth=user_three.auth)
assert res.status_code == 201
# Private components that you do not have access to are not forked
@@ -385,7 +385,7 @@ def test_fork_components_you_can_access(
fork_data, private_project_url):
url = private_project_url + '?embed=children'
new_component = NodeFactory(parent=private_project, creator=user)
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(url, fork_data, auth=user.auth)
assert res.status_code == 201
assert res.json['data']['embeds']['children']['links']['meta']['total'] == 1
@@ -403,7 +403,7 @@ def test_fork_private_node_links(
url = private_project_url + '?embed=node_links'
# Node link is forked, but shows up as a private node link
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
url,
fork_data,
@@ -424,7 +424,7 @@ def test_fork_node_links_you_can_access(
private_project.add_pointer(pointer, auth=Auth(user_two), save=True)
url = private_project_url + '?embed=node_links'
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
url,
fork_data,
@@ -440,7 +440,7 @@ def test_can_fork_registration(
registration = RegistrationFactory(project=private_project, user=user)
url = f'/{API_BASE}registrations/{registration._id}/forks/'
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(url, fork_data, auth=user.auth)
assert res.status_code == 201
assert res.json['data']['id'] == registration.forks.first()._id
@@ -453,7 +453,7 @@ def test_read_only_contributor_can_fork_private_registration(
private_project.add_contributor(
read_contrib,
permissions=permissions.READ, save=True)
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=read_contrib):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=read_contrib):
res = app.post_json_api(
private_project_url,
fork_data,
@@ -464,7 +464,7 @@ def test_read_only_contributor_can_fork_private_registration(
assert res.json['data']['id'] == private_project.forks.first()._id
def test_send_email_success(self, app, user, public_project_url, fork_data_with_title, public_project):
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=user):
res = app.post_json_api(
public_project_url,
fork_data_with_title,
@@ -477,7 +477,7 @@ def test_send_email_success(self, app, user, public_project_url, fork_data_with_
def test_send_email_failed(self, app, user, public_project_url, fork_data_with_title):
with mock.patch.object(NodeForksSerializer, 'save', side_effect=Exception()):
with pytest.raises(Exception):
- with assert_notification(type=NotificationType.Type.NODE_FORK_FAILED, user=user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_FAILED, user=user):
app.post_json_api(
public_project_url,
fork_data_with_title,
diff --git a/api_tests/nodes/views/test_node_list.py b/api_tests/nodes/views/test_node_list.py
index ddef9428952..ce6751c2c16 100644
--- a/api_tests/nodes/views/test_node_list.py
+++ b/api_tests/nodes/views/test_node_list.py
@@ -8,7 +8,7 @@
from api_tests.nodes.filters.test_filters import NodesListFilteringMixin, NodesListDateFilteringMixin
from api_tests.subjects.mixins import SubjectsFilterMixin
from framework.auth.core import Auth
-from osf.models import AbstractNode, Node, NodeLog, NotificationType
+from osf.models import AbstractNode, Node, NodeLog, NotificationTypeEnum
from osf.models.licenses import NodeLicense
from osf.utils.sanitize import strip_html
from osf.utils import permissions
@@ -1428,7 +1428,7 @@ def test_create_node_errors(self, app, user_one, public_project, private_project
def test_creates_public_project_logged_in(
self, app, user_one, public_project, url, institution_one):
- with assert_notification(type=NotificationType.Type.NODE_AFFILIATION_CHANGED, user=user_one):
+ with assert_notification(type=NotificationTypeEnum.NODE_AFFILIATION_CHANGED, user=user_one):
res = app.post_json_api(
url, public_project,
auth=user_one.auth
@@ -1531,7 +1531,7 @@ def test_non_contributor_create_project_from_public_template_success(self, app,
auth=user_without_permissions.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
assert res.status_code == 201
def test_non_contributor_create_project_from_private_template_no_permission_fails(self, app, user_one, category, url):
@@ -1576,7 +1576,7 @@ def test_contributor_create_project_from_private_template_with_permission_succes
auth=user_without_permissions.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
assert res.status_code == 201
assert template_from.has_permission(user_without_permissions, permissions.READ)
@@ -1593,7 +1593,7 @@ def test_contributor_create_project_from_private_template_with_permission_succes
auth=user_without_permissions.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
assert res.status_code == 201
assert template_from.has_permission(user_without_permissions, permissions.WRITE)
@@ -1610,7 +1610,7 @@ def test_contributor_create_project_from_private_template_with_permission_succes
auth=user_without_permissions.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
assert res.status_code == 201
assert template_from.has_permission(user_without_permissions, permissions.ADMIN)
@@ -1759,7 +1759,7 @@ def test_create_project_with_region_relationship(
}
}
}
- with assert_notification(type=NotificationType.Type.NODE_AFFILIATION_CHANGED, user=user_one, times=2):
+ with assert_notification(type=NotificationTypeEnum.NODE_AFFILIATION_CHANGED, user=user_one, times=2):
res = app.post_json_api(
url,
private_project,
diff --git a/api_tests/nodes/views/test_node_relationship_institutions.py b/api_tests/nodes/views/test_node_relationship_institutions.py
index fa0eeca1edb..475a03b0001 100644
--- a/api_tests/nodes/views/test_node_relationship_institutions.py
+++ b/api_tests/nodes/views/test_node_relationship_institutions.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
InstitutionFactory,
AuthUserFactory,
@@ -198,7 +198,7 @@ def test_user_with_institution_and_permissions(
)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 201
data = res.json['data']
@@ -228,9 +228,9 @@ def test_user_with_institution_and_permissions_through_patch(
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert notifications['emits'][1]['kwargs']['user'] == user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
def test_remove_institutions_with_affiliated_user(
self,
@@ -254,7 +254,7 @@ def test_remove_institutions_with_affiliated_user(
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 200
assert node.affiliated_institutions.count() == 0
@@ -287,7 +287,7 @@ def test_put_not_admin_but_affiliated(self, app, institution_one, node, node_ins
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 200
assert institution_one in node.affiliated_institutions.all()
@@ -314,7 +314,7 @@ def test_add_through_patch_one_inst_to_node_with_inst(
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 200
assert institution_one in node.affiliated_institutions.all()
@@ -342,10 +342,10 @@ def test_add_through_patch_one_inst_while_removing_other(
)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert notifications['emits'][1]['kwargs']['user'] == user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 200
assert institution_one not in node.affiliated_institutions.all()
@@ -373,7 +373,7 @@ def test_add_one_inst_with_post_to_node_with_inst(
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 201
assert institution_one in node.affiliated_institutions.all()
@@ -406,7 +406,7 @@ def test_delete_existing_inst(
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert institution_one not in node.affiliated_institutions.all()
@@ -426,7 +426,7 @@ def test_delete_not_affiliated_and_affiliated_insts(
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert institution_one not in node.affiliated_institutions.all()
@@ -444,7 +444,7 @@ def test_delete_user_is_admin(self, app, user, institution_one, node, resource_u
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
@@ -488,7 +488,7 @@ def test_delete_user_is_admin_but_not_affiliated_with_inst(self, app, institutio
auth=user_auth.auth,
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert institution_one not in project.affiliated_institutions.all()
@@ -509,7 +509,7 @@ def test_admin_can_add_affiliated_institution(self, app, user, institution_one,
)
assert len(notifications['emits']) == 1
assert notifications['emits'][0]['kwargs']['user'] == user
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 201
assert institution_one in node.affiliated_institutions.all()
@@ -530,7 +530,7 @@ def test_admin_can_remove_admin_affiliated_institution(
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert institution_one not in node.affiliated_institutions.all()
@@ -552,7 +552,7 @@ def test_admin_can_remove_read_write_contributor_affiliated_institution(
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert read_contrib_institution not in node.affiliated_institutions.all()
@@ -572,7 +572,7 @@ def test_read_write_contributor_can_add_affiliated_institution(
auth=write_contrib.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 201
assert write_contrib_institution in node.affiliated_institutions.all()
@@ -594,7 +594,7 @@ def test_read_write_contributor_can_remove_affiliated_institution(
auth=write_contrib.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_AFFILIATION_CHANGED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_AFFILIATION_CHANGED
assert res.status_code == 204
assert write_contrib_institution not in node.affiliated_institutions.all()
diff --git a/api_tests/notifications/test_notification_digest.py b/api_tests/notifications/test_notification_digest.py
index 70433f983c5..a1065c27604 100644
--- a/api_tests/notifications/test_notification_digest.py
+++ b/api_tests/notifications/test_notification_digest.py
@@ -1,7 +1,7 @@
import pytest
from django.contrib.contenttypes.models import ContentType
-from osf.models import Notification, NotificationType, EmailTask, Email
+from osf.models import Notification, NotificationType, NotificationTypeEnum, EmailTask, Email
from notifications.tasks import (
send_user_email_task,
send_moderator_email_task,
@@ -18,7 +18,7 @@ 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, AbstractProvider
+ from osf.models import NotificationSubscription
kwargs = {
'user': user,
'notification_type': NotificationType.objects.get(name=notification_type),
@@ -26,10 +26,7 @@ def add_notification_subscription(user, notification_type, frequency, subscribed
}
if subscribed_object is not None:
kwargs['object_id'] = subscribed_object.id
- 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
+ kwargs['content_type'] = ContentType.objects.get_for_model(subscribed_object)
if subscription is not None:
kwargs['object_id'] = subscription.id
kwargs['content_type'] = ContentType.objects.get_for_model(subscription)
@@ -41,14 +38,14 @@ class TestNotificationDigestTasks:
def test_send_user_email_task_success(self):
user = AuthUserFactory()
- notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
subscription_type = add_notification_subscription(
user,
notification_type,
'daily',
subscription=add_notification_subscription(
user,
- NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED),
+ NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED),
'daily'
)
)
@@ -74,7 +71,7 @@ def test_send_user_email_task_success(self):
with capture_notifications() as notifications:
send_user_email_task.apply(args=(user._id, notification_ids)).get()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_DIGEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_DIGEST
assert notifications['emits'][0]['kwargs']['user'] == user
email_task = EmailTask.objects.get(user_id=user.id)
assert email_task.status == 'SUCCESS'
@@ -94,9 +91,9 @@ def test_send_user_email_task_user_disabled(self):
user = AuthUserFactory()
user.deactivate_account()
user.save()
- notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_DIGEST)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_DIGEST)
notification = Notification.objects.create(
- subscription=add_notification_subscription(user, NotificationType.Type.USER_FILE_UPDATED, notification_type),
+ subscription=add_notification_subscription(user, NotificationTypeEnum.USER_FILE_UPDATED, notification_type),
sent=None,
event_context={},
)
@@ -120,7 +117,7 @@ def test_send_moderator_email_task_registration_provider_admin(self):
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_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
notification = Notification.objects.create(
subscription=add_notification_subscription(
user,
@@ -143,7 +140,7 @@ def test_send_moderator_email_task_registration_provider_admin(self):
with capture_notifications() as notifications:
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]['type'] == NotificationTypeEnum.DIGEST_REVIEWS_MODERATORS
assert notifications['emits'][0]['kwargs']['user'] == user
email_task = EmailTask.objects.filter(user_id=user.id).first()
@@ -158,7 +155,7 @@ def test_send_moderator_email_task_no_notifications(self):
RegistrationFactory(provider=provider)
notification_ids = []
- notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
add_notification_subscription(
user,
notification_type,
@@ -177,7 +174,7 @@ def test_send_moderator_email_task_user_not_found(self):
def test_get_users_emails(self):
user = AuthUserFactory()
- notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
notification1 = Notification.objects.create(
subscription=add_notification_subscription(user, notification_type, 'daily'),
sent=None,
@@ -193,7 +190,7 @@ def test_get_moderators_emails(self):
user = AuthUserFactory()
provider = RegistrationProviderFactory()
reg = RegistrationFactory(provider=provider)
- notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
subscription = add_notification_subscription(user, notification_type, 'daily', subscribed_object=reg)
Notification.objects.create(
subscription=subscription,
@@ -209,14 +206,14 @@ def test_get_moderators_emails(self):
def test_send_users_digest_email_end_to_end(self):
user = AuthUserFactory()
- notification_type = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
subscription_type = add_notification_subscription(
user,
notification_type,
'daily',
subscription=add_notification_subscription(
user,
- NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED),
+ NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED),
'daily'
)
)
@@ -246,7 +243,7 @@ def test_send_users_digest_email_end_to_end(self):
with capture_notifications() as notifications:
send_users_digest_email.delay()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_DIGEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_DIGEST
email_task = EmailTask.objects.get(user_id=user.id)
assert email_task.status == 'SUCCESS'
@@ -256,7 +253,7 @@ def test_send_moderators_digest_email_end_to_end(self):
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_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
Notification.objects.create(
subscription=add_notification_subscription(user, notification_type, 'daily', subscribed_object=provider),
sent=None,
@@ -275,7 +272,7 @@ def test_send_moderators_digest_email_end_to_end(self):
with capture_notifications() as notifications:
send_moderators_digest_email.delay()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DIGEST_REVIEWS_MODERATORS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DIGEST_REVIEWS_MODERATORS
email_task = EmailTask.objects.filter(user_id=user.id).first()
assert email_task.status == 'SUCCESS'
@@ -288,7 +285,7 @@ def test_user_invalid_username_success(self):
RegistrationFactory(provider=provider)
notification_ids = []
- notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
add_notification_subscription(
user,
notification_type,
@@ -309,7 +306,7 @@ def test_user_no_email_failure(self):
RegistrationFactory(provider=provider)
notification_ids = []
- notification_type = NotificationType.objects.get(name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS)
+ notification_type = NotificationType.objects.get(name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS)
add_notification_subscription(
user,
notification_type,
diff --git a/api_tests/notifications/test_notifications_cleanup.py b/api_tests/notifications/test_notifications_cleanup.py
new file mode 100644
index 00000000000..848b142b740
--- /dev/null
+++ b/api_tests/notifications/test_notifications_cleanup.py
@@ -0,0 +1,189 @@
+import pytest
+from osf.models import Notification, NotificationType, EmailTask, NotificationSubscription
+from notifications.tasks import (
+ notifications_cleanup_task
+)
+from osf_tests.factories import AuthUserFactory
+from website.settings import NOTIFICATIONS_CLEANUP_AGE
+from django.utils import timezone
+from datetime import timedelta
+
+def create_notification(subscription, sent_date=None):
+ return Notification.objects.create(
+ subscription=subscription,
+ event_context={},
+ sent=sent_date
+ )
+
+def create_email_task(user, created_date):
+ et = EmailTask.objects.create(
+ task_id=f'test-{created_date.timestamp()}',
+ user=user,
+ status='SUCCESS',
+ )
+ et.created_at = created_date
+ et.save()
+ return et
+
+@pytest.mark.django_db
+class TestNotificationCleanUpTask:
+
+ @pytest.fixture()
+ def user(self):
+ return AuthUserFactory()
+
+ @pytest.fixture()
+ def notification_type(self):
+ return NotificationType.objects.get_or_create(
+ name='Test Notification',
+ subject='Hello',
+ template='Sample Template',
+ )[0]
+
+ @pytest.fixture()
+ def subscription(self, user, notification_type):
+ return NotificationSubscription.objects.get_or_create(
+ user=user,
+ notification_type=notification_type,
+ message_frequency='daily',
+ )[0]
+
+ def test_dry_run_does_not_delete_records(self, user, subscription):
+ now = timezone.now()
+
+ old_notification = create_notification(
+ subscription,
+ sent_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ old_email_task = create_email_task(
+ user,
+ created_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+
+ notifications_cleanup_task(dry_run=True)
+
+ assert Notification.objects.filter(id=old_notification.id).exists()
+ assert EmailTask.objects.filter(id=old_email_task.id).exists()
+
+ def test_deletes_old_notifications_and_email_tasks(self, user, subscription):
+ now = timezone.now()
+
+ old_notification = create_notification(
+ subscription,
+ sent_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ new_notification = create_notification(
+ subscription,
+ sent_date=now - timedelta(days=10),
+ )
+
+ old_email_task = create_email_task(
+ user,
+ created_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ new_email_task = create_email_task(
+ user,
+ created_date=now - timedelta(days=10),
+ )
+
+ notifications_cleanup_task()
+
+ assert not Notification.objects.filter(id=old_notification.id).exists()
+ assert Notification.objects.filter(id=new_notification.id).exists()
+
+ assert not EmailTask.objects.filter(id=old_email_task.id).exists()
+ assert EmailTask.objects.filter(id=new_email_task.id).exists()
+
+ def test_records_at_cutoff_are_not_deleted(self, user, subscription):
+ now = timezone.now()
+ cutoff = now - NOTIFICATIONS_CLEANUP_AGE + timedelta(hours=1)
+
+ notification = create_notification(
+ subscription,
+ sent_date=cutoff,
+ )
+ email_task = create_email_task(
+ user,
+ created_date=cutoff,
+ )
+
+ notifications_cleanup_task()
+
+ assert Notification.objects.filter(id=notification.id).exists()
+ assert EmailTask.objects.filter(id=email_task.id).exists()
+
+ def test_cleanup_when_only_notifications_exist(self, user, subscription):
+ now = timezone.now()
+
+ notification = create_notification(
+ subscription,
+ sent_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+
+ notifications_cleanup_task()
+
+ assert not Notification.objects.filter(id=notification.id).exists()
+
+ def test_cleanup_when_only_email_tasks_exist(self, user, subscription):
+ now = timezone.now()
+
+ email_task = create_email_task(
+ user,
+ created_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+
+ notifications_cleanup_task()
+
+ assert not EmailTask.objects.filter(id=email_task.id).exists()
+
+ def test_task_is_idempotent(self, user, subscription):
+ now = timezone.now()
+
+ create_notification(
+ subscription,
+ sent_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ create_email_task(
+ user,
+ created_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+
+ notifications_cleanup_task()
+ notifications_cleanup_task()
+
+ assert Notification.objects.count() == 0
+ assert EmailTask.objects.count() == 0
+
+ def test_recent_records_are_not_deleted(self, user, subscription):
+ now = timezone.now()
+
+ create_notification(
+ subscription,
+ sent_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ create_email_task(
+ user,
+ created_date=now - NOTIFICATIONS_CLEANUP_AGE - timedelta(days=1),
+ )
+ create_notification(
+ subscription,
+ sent_date=now,
+ )
+ create_email_task(
+ user,
+ created_date=now,
+ )
+
+ notifications_cleanup_task()
+
+ assert Notification.objects.count() == 1
+ assert EmailTask.objects.count() == 1
+
+ def test_not_sent_notifications_are_not_deleted(self, user, subscription):
+ create_notification(subscription)
+ create_notification(subscription)
+ create_notification(subscription)
+
+ notifications_cleanup_task()
+
+ assert Notification.objects.count() == 3
diff --git a/api_tests/notifications/test_notifications_db_transaction.py b/api_tests/notifications/test_notifications_db_transaction.py
index dc09dd46487..8fb651de996 100644
--- a/api_tests/notifications/test_notifications_db_transaction.py
+++ b/api_tests/notifications/test_notifications_db_transaction.py
@@ -1,12 +1,14 @@
+from django.db import reset_queries, connection
+from django.utils import timezone
+
import pytest
+
+from osf.models import Notification, NotificationTypeEnum, NotificationSubscription
from osf_tests.factories import (
AuthUserFactory,
NotificationTypeFactory
)
-from datetime import datetime
-from osf.models import Notification, NotificationType, NotificationSubscription
from tests.utils import capture_notifications
-from django.db import reset_queries, connection
@pytest.mark.django_db
@@ -25,9 +27,9 @@ def test_notification_type(self):
)
def test_notification_type_cache(self):
- NotificationType.Type.NODE_FILE_UPDATED.instance
+ NotificationTypeEnum.NODE_FILE_UPDATED.instance
reset_queries()
- NotificationType.Type.NODE_FILE_UPDATED.instance
+ NotificationTypeEnum.NODE_FILE_UPDATED.instance
assert len(connection.queries) == 0
def test_emit_without_saving(self, user_one, test_notification_type):
@@ -47,12 +49,21 @@ def test_emit_without_saving(self, user_one, test_notification_type):
).exists()
def test_emit_frequency_none(self, user_one, test_notification_type):
+ assert not Notification.objects.filter(
+ subscription__notification_type=test_notification_type,
+ fake_sent=True
+ ).exists()
+ time_before = timezone.now()
test_notification_type.emit(
user=user_one,
event_context={'notifications': 'test template for Test notification'},
message_frequency='none'
)
- assert Notification.objects.filter(
+ time_after = timezone.now()
+ notifications = Notification.objects.filter(
subscription__notification_type=test_notification_type,
- sent=datetime(1000, 1, 1)
- ).exists()
+ fake_sent=True
+ )
+ assert notifications.exists()
+ assert notifications.count() == 1
+ assert time_before < notifications.first().sent < time_after
diff --git a/api_tests/preprints/views/test_preprint_contributors_list.py b/api_tests/preprints/views/test_preprint_contributors_list.py
index e069ec7d9d9..4572a7749e6 100644
--- a/api_tests/preprints/views/test_preprint_contributors_list.py
+++ b/api_tests/preprints/views/test_preprint_contributors_list.py
@@ -7,7 +7,7 @@
from api.base.settings.defaults import API_BASE
from api.nodes.serializers import NodeContributorsCreateSerializer
from framework.auth.core import Auth
-from osf.models import PreprintLog, NotificationType
+from osf.models import PreprintLog, NotificationTypeEnum
from osf_tests.factories import (
fake_email,
AuthUserFactory,
@@ -1418,7 +1418,7 @@ def test_add_contributor_signal_if_preprint(
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
def test_add_contributor_signal_no_query_param(
self, app, user, user_two, url_preprint_contribs):
@@ -1444,7 +1444,7 @@ def test_add_contributor_signal_no_query_param(
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
def test_add_unregistered_contributor_sends_email(
self, app, user, url_preprint_contribs):
@@ -1463,7 +1463,7 @@ def test_add_unregistered_contributor_sends_email(
auth=user.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_USER_INVITE_PREPRINT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_USER_INVITE_PREPRINT
assert res.status_code == 201
def test_add_unregistered_contributor_signal_if_preprint(self, app, user, url_preprint_contribs):
@@ -1483,7 +1483,7 @@ def test_add_unregistered_contributor_signal_if_preprint(self, app, user, url_pr
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_USER_INVITE_PREPRINT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_USER_INVITE_PREPRINT
def test_add_contributor_invalid_send_email_param(self, app, user, url_preprint_contribs):
url = f'{url_preprint_contribs}?send_email=true'
@@ -1529,7 +1529,7 @@ def test_publishing_preprint_sends_emails_to_contributors(
user_two = AuthUserFactory()
preprint_unpublished.add_contributor(user_two, permissions=permissions.WRITE, save=True)
with capture_signals() as mock_signal:
- with assert_notification(type=NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
+ with assert_notification(type=NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
res = app.patch_json_api(
url,
{
@@ -1564,7 +1564,7 @@ def test_contributor_added_signal_not_specified(self, app, user, url_preprint_co
)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_USER_INVITE_PREPRINT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_USER_INVITE_PREPRINT
@pytest.mark.django_db
class TestPreprintContributorBulkCreate(NodeCRUDTestCase):
diff --git a/api_tests/preprints/views/test_preprint_detail_update.py b/api_tests/preprints/views/test_preprint_detail_update.py
index b796da0f908..f3096cba10d 100644
--- a/api_tests/preprints/views/test_preprint_detail_update.py
+++ b/api_tests/preprints/views/test_preprint_detail_update.py
@@ -11,7 +11,7 @@
from osf.models import (
NodeLicense,
PreprintContributor,
- PreprintLog, NotificationType
+ PreprintLog, NotificationTypeEnum
)
from osf.utils import permissions as osf_permissions
from osf.utils.permissions import WRITE
@@ -502,7 +502,7 @@ def test_update_contributors(
self, mock_update_doi_metadata, app, user, preprint, url
):
new_user = AuthUserFactory()
- with assert_notification(type=NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT, user=new_user):
+ with assert_notification(type=NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT, user=new_user):
res = app.post_json_api(
url + 'contributors/',
{
@@ -602,7 +602,7 @@ def test_noncontrib_cannot_set_primary_file(self, app, user, preprint, url):
def test_update_published(self, app, user):
unpublished = PreprintFactory(creator=user, is_published=False)
- with assert_notification(type=NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
+ with assert_notification(type=NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
app.patch_json_api(
f'/{API_BASE}preprints/{unpublished._id}/',
build_preprint_update_payload(
@@ -622,7 +622,7 @@ def test_update_published_does_not_make_node_public(self, app, user):
project=project
)
assert not unpublished.node.is_public
- with assert_notification(type=NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
+ with assert_notification(type=NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION, user=user):
app.patch_json_api(
f'/{API_BASE}preprints/{unpublished._id}/',
build_preprint_update_payload(
diff --git a/api_tests/providers/collections/views/test_collections_provider_moderator_list.py b/api_tests/providers/collections/views/test_collections_provider_moderator_list.py
index 249fb1365a2..ff0cc0035b7 100644
--- a/api_tests/providers/collections/views/test_collections_provider_moderator_list.py
+++ b/api_tests/providers/collections/views/test_collections_provider_moderator_list.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
CollectionProviderFactory,
@@ -111,7 +111,7 @@ def test_POST_admin_success_existing_user(self, app, url, nonmoderator, moderato
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
assert res.status_code == 201
assert res.json['data']['id'] == nonmoderator._id
assert res.json['data']['attributes']['permission_group'] == 'moderator'
@@ -136,7 +136,7 @@ def test_POST_admin_failure_unreg_moderator(self, app, url, moderator, nonmodera
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_CONFIRM_EMAIL_MODERATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_CONFIRM_EMAIL_MODERATION
assert notifications['emits'][0]['kwargs']['user'].username == unreg_user['email']
def test_POST_admin_failure_invalid_group(self, app, url, nonmoderator, moderator, admin, provider):
@@ -149,7 +149,7 @@ def test_POST_admin_success_email(self, app, url, nonmoderator, moderator, admin
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_CONFIRM_EMAIL_MODERATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_CONFIRM_EMAIL_MODERATION
assert res.status_code == 201
assert len(res.json['data']['id']) == 5
assert res.json['data']['attributes']['permission_group'] == 'moderator'
diff --git a/api_tests/providers/preprints/views/test_preprint_provider_moderator_list.py b/api_tests/providers/preprints/views/test_preprint_provider_moderator_list.py
index 0703dcebc83..3d09d783320 100644
--- a/api_tests/providers/preprints/views/test_preprint_provider_moderator_list.py
+++ b/api_tests/providers/preprints/views/test_preprint_provider_moderator_list.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
PreprintProviderFactory,
@@ -89,7 +89,7 @@ def test_list_post_admin_success_existing_user(self, app, url, nonmoderator, mod
assert res.json['data']['id'] == nonmoderator._id
assert res.json['data']['attributes']['permission_group'] == 'moderator'
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
def test_list_post_admin_failure_existing_moderator(self, app, url, moderator, admin):
payload = self.create_payload(user_id=moderator._id, permission_group='moderator')
diff --git a/api_tests/providers/tasks/test_bulk_upload.py b/api_tests/providers/tasks/test_bulk_upload.py
index 2da245c50b8..4ebc5ac8c53 100644
--- a/api_tests/providers/tasks/test_bulk_upload.py
+++ b/api_tests/providers/tasks/test_bulk_upload.py
@@ -5,7 +5,7 @@
from osf.exceptions import RegistrationBulkCreationContributorError, RegistrationBulkCreationRowError
from osf.models import RegistrationBulkUploadJob, RegistrationBulkUploadRow, RegistrationProvider, RegistrationSchema, \
- NotificationType
+ NotificationTypeEnum
from osf.models.registration_bulk_upload_job import JobState
from osf.models.registration_bulk_upload_row import RegistrationBulkUploadContributors
from osf.utils.permissions import ADMIN, READ, WRITE
@@ -331,7 +331,7 @@ def test_bulk_creation_done_full(
with capture_notifications() as notifications:
bulk_create_registrations(upload_job_done_full.id, dry_run=False)
notification_types = [notifications['type'] for notifications in notifications['emits']]
- assert NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL in notification_types
+ assert NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL in notification_types
upload_job_done_full.reload()
assert upload_job_done_full.state == JobState.DONE_FULL
assert upload_job_done_full.email_sent
@@ -359,7 +359,7 @@ def test_bulk_creation_done_partial(
with capture_notifications() as notifications:
bulk_create_registrations(upload_job_done_partial.id, dry_run=False)
notification_types = [notifications['type'] for notifications in notifications['emits']]
- assert NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL in notification_types
+ assert NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL in notification_types
upload_job_done_partial.reload()
assert upload_job_done_partial.state == JobState.DONE_PARTIAL
assert upload_job_done_partial.email_sent
@@ -387,7 +387,7 @@ def test_bulk_creation_done_error(
with capture_notifications() as notifications:
bulk_create_registrations(upload_job_done_error.id, dry_run=False)
notification_types = [notifications['type'] for notifications in notifications['emits']]
- assert NotificationType.Type.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL in notification_types
+ assert NotificationTypeEnum.USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL in notification_types
upload_job_done_error.reload()
assert upload_job_done_error.state == JobState.DONE_ERROR
diff --git a/api_tests/requests/views/test_node_request_institutional_access.py b/api_tests/requests/views/test_node_request_institutional_access.py
index b351cbfd787..d8d8d520a44 100644
--- a/api_tests/requests/views/test_node_request_institutional_access.py
+++ b/api_tests/requests/views/test_node_request_institutional_access.py
@@ -2,7 +2,7 @@
from api.base.settings.defaults import API_BASE
from api_tests.requests.mixins import NodeRequestTestMixin
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import NodeFactory, InstitutionFactory, AuthUserFactory
from osf.utils.workflows import DefaultStates, NodeRequestTypes
@@ -141,7 +141,7 @@ def test_institutional_admin_can_make_institutional_request(
"""
Test that an institutional admin can make an institutional access request.
"""
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -172,7 +172,7 @@ def test_institutional_admin_can_add_requested_permission(
Test that an institutional admin can make an institutional access request with requested_permissions.
"""
create_payload['data']['attributes']['requested_permissions'] = 'admin'
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -243,7 +243,7 @@ def test_email_send_institutional_request_specific_email(
project.save()
# Perform the action
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
# Ensure response is successful
@@ -284,7 +284,7 @@ def test_email_sent_on_creation(
"""
Test that an email is sent to the appropriate recipients when an institutional access request is made.
"""
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -302,7 +302,7 @@ def test_bcc_institutional_admin(
Ensure BCC option works as expected, sending messages to sender giving them a copy for themselves.
"""
create_payload['data']['attributes']['bcc_sender'] = True
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -320,7 +320,7 @@ def test_reply_to_institutional_admin(
Ensure reply-to option works as expected, allowing a reply to header be added to the email.
"""
create_payload['data']['attributes']['reply_to'] = True
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST, user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -354,7 +354,7 @@ def test_placeholder_text_when_comment_is_empty(
"""
# Test with empty comment
create_payload['data']['attributes']['comment'] = ''
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -372,19 +372,19 @@ def test_requester_can_resubmit(
Test that a requester can submit another access request for the same node.
"""
# Create the first request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
app.post_json_api(url, create_payload, auth=institutional_admin.auth)
node_request = project.requests.get()
- with assert_notification(type=NotificationType.Type.NODE_REQUEST_ACCESS_DENIED,
+ with assert_notification(type=NotificationTypeEnum.NODE_REQUEST_ACCESS_DENIED,
user=node_request.creator):
node_request.run_reject(project.creator, 'test comment2')
node_request.refresh_from_db()
assert node_request.machine_state == 'rejected'
# Attempt to create a second request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -406,11 +406,11 @@ def test_requester_can_make_insti_request_after_access_resubmit(
"""
# Create the first request a basic request_type == `access` request
- with assert_notification(type=NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED,
+ with assert_notification(type=NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED,
user=project.creator):
app.post_json_api(url, create_payload_non_institutional_access, auth=institutional_admin.auth)
node_request = project.requests.get()
- with assert_notification(type=NotificationType.Type.NODE_REQUEST_ACCESS_DENIED,
+ with assert_notification(type=NotificationTypeEnum.NODE_REQUEST_ACCESS_DENIED,
user=node_request.creator):
node_request.run_reject(project.creator, 'test comment2')
node_request.refresh_from_db()
@@ -421,7 +421,7 @@ def test_requester_can_make_insti_request_after_access_resubmit(
create_payload['data']['relationships']['message_recipient']['data']['id'] = project.creator._id
# Attempt to create a second request, refresh and update as institutional
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=project.creator):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -441,12 +441,12 @@ def test_requester_can_resubmit_after_approval(
Test that a requester can submit another access request for the same node.
"""
# Create the first request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
app.post_json_api(url, create_payload, auth=institutional_admin.auth)
node_request = project.requests.get()
- with assert_notification(type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
user=node_request.creator):
node_request.run_accept(project.creator, 'test comment2')
node_request.refresh_from_db()
@@ -456,7 +456,7 @@ def test_requester_can_resubmit_after_approval(
node_request = project.requests.get()
# Attempt to create a second request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -468,12 +468,12 @@ def test_requester_can_resubmit_after_2_approvals(self, app, project, institutio
Test that a requester can submit another access request for the same node.
"""
# Create the first request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
app.post_json_api(url, create_payload, auth=institutional_admin.auth)
node_request = project.requests.get()
- with assert_notification(type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
user=node_request.creator):
node_request.run_accept(project.creator, 'test comment2')
node_request.refresh_from_db()
@@ -482,7 +482,7 @@ def test_requester_can_resubmit_after_2_approvals(self, app, project, institutio
project.remove_contributor(node_request.creator, Auth(node_request.creator))
# Attempt to create a second request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
@@ -493,7 +493,7 @@ def test_requester_can_resubmit_after_2_approvals(self, app, project, institutio
assert project.requests.all().count() == 1
# Attempt to create a second request
- with assert_notification(type=NotificationType.Type.NODE_INSTITUTIONAL_ACCESS_REQUEST,
+ with assert_notification(type=NotificationTypeEnum.NODE_INSTITUTIONAL_ACCESS_REQUEST,
user=user_with_affiliation):
res = app.post_json_api(url, create_payload, auth=institutional_admin.auth)
assert res.status_code == 201
diff --git a/api_tests/requests/views/test_node_request_institutional_access_logging.py b/api_tests/requests/views/test_node_request_institutional_access_logging.py
index 903422037bd..b4675058c24 100644
--- a/api_tests/requests/views/test_node_request_institutional_access_logging.py
+++ b/api_tests/requests/views/test_node_request_institutional_access_logging.py
@@ -3,7 +3,7 @@
from api.base.settings.defaults import API_BASE
from osf_tests.factories import NodeFactory, InstitutionFactory, AuthUserFactory
-from osf.models import NodeLog, NodeRequest, NotificationType
+from osf.models import NodeLog, NodeRequest, NotificationTypeEnum
from osf.utils.workflows import NodeRequestTypes
from tests.utils import assert_notification
@@ -70,7 +70,7 @@ def test_post_node_request_action_success_logged_as_curator(self, app, action_pa
Test a successful POST request to create a node-request action and log it.
"""
# Perform the POST request
- with assert_notification(type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST, user=institutional_admin):
+ with assert_notification(type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST, user=institutional_admin):
res = app.post_json_api(url, action_payload, auth=user_with_affiliation.auth)
assert res.status_code == 201
assert res.json['data']['attributes']['trigger'] == 'accept'
@@ -92,7 +92,7 @@ def test_post_node_request_action_reject_curator(self, app, action_payload, url,
"""
# Perform the POST request
action_payload['data']['attributes']['trigger'] = 'reject'
- with assert_notification(type=NotificationType.Type.NODE_REQUEST_ACCESS_DENIED, user=institutional_admin):
+ with assert_notification(type=NotificationTypeEnum.NODE_REQUEST_ACCESS_DENIED, user=institutional_admin):
res = app.post_json_api(url, action_payload, auth=user_with_affiliation.auth)
assert res.status_code == 201
assert res.json['data']['attributes']['trigger'] == 'reject'
diff --git a/api_tests/requests/views/test_node_request_list.py b/api_tests/requests/views/test_node_request_list.py
index 6dafdb18a0d..5067005b78f 100644
--- a/api_tests/requests/views/test_node_request_list.py
+++ b/api_tests/requests/views/test_node_request_list.py
@@ -2,7 +2,7 @@
from api.base.settings.defaults import API_BASE
from api_tests.requests.mixins import NodeRequestTestMixin
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import NodeFactory, NodeRequestFactory, InstitutionFactory
from osf.utils.workflows import DefaultStates, NodeRequestTypes
@@ -90,8 +90,8 @@ def test_email_sent_to_all_admins_on_submit(self, app, project, noncontrib, url,
res = app.post_json_api(url, create_payload, auth=noncontrib.auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED
assert res.status_code == 201
def test_email_not_sent_to_parent_admins_on_submit(self, app, project, noncontrib, url, create_payload, second_admin):
@@ -105,7 +105,7 @@ def test_email_not_sent_to_parent_admins_on_submit(self, app, project, noncontri
auth=noncontrib.auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED
assert res.status_code == 201
assert component.parent_admin_contributors.count() == 1
assert component.contributors.count() == 1
@@ -156,7 +156,7 @@ def test_requester_can_make_access_request_after_insti_access_accepted(self, app
# Create the first request a basic request_type == `institutional_request` request
app.post_json_api(url, create_payload, auth=noncontrib.auth)
node_request = project.requests.get()
- with assert_notification(type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST, user=node_request.creator):
+ with assert_notification(type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST, user=node_request.creator):
node_request.run_accept(project.creator, 'test comment2')
node_request.refresh_from_db()
assert node_request.machine_state == 'accepted'
@@ -166,7 +166,7 @@ def test_requester_can_make_access_request_after_insti_access_accepted(self, app
create_payload['data']['attributes']['request_type'] = NodeRequestTypes.ACCESS.value
# Attempt to create a second request, refresh and update as institutional
- with assert_notification(type=NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED, user=project.creator):
+ with assert_notification(type=NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED, user=project.creator):
res = app.post_json_api(url, create_payload, auth=noncontrib.auth)
assert res.status_code == 201
node_request.refresh_from_db()
diff --git a/api_tests/requests/views/test_request_actions_create.py b/api_tests/requests/views/test_request_actions_create.py
index ba1e9ef6409..26661048617 100644
--- a/api_tests/requests/views/test_request_actions_create.py
+++ b/api_tests/requests/views/test_request_actions_create.py
@@ -2,7 +2,7 @@
from api.base.settings.defaults import API_BASE
from api_tests.requests.mixins import NodeRequestTestMixin, PreprintRequestTestMixin
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf.utils import permissions
from tests.utils import capture_notifications, assert_notification
@@ -201,7 +201,7 @@ def test_email_sent_on_approve(self, app, admin, url, node_request):
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
assert res.status_code == 201
node_request.reload()
assert initial_state != node_request.machine_state
@@ -214,7 +214,7 @@ def test_email_sent_on_reject(self, app, admin, url, node_request):
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_REQUEST_ACCESS_DENIED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_REQUEST_ACCESS_DENIED
assert res.status_code == 201
node_request.reload()
@@ -326,7 +326,7 @@ def test_moderator_can_approve_moderated_requests(self, app, moderator, url, pre
initial_state = request.machine_state
assert not request.target.is_retracted
payload = self.create_payload(request._id, trigger='accept')
- with assert_notification(type=NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_APPROVED, user=request.target.creator):
+ with assert_notification(type=NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_APPROVED, user=request.target.creator):
res = app.post_json_api(url, payload, auth=moderator.auth)
assert res.status_code == 201
request.reload()
@@ -363,7 +363,7 @@ def test_moderator_can_reject_moderated_requests(self, app, moderator, url, pre_
initial_state = request.machine_state
assert not request.target.is_retracted
payload = self.create_payload(request._id, trigger='reject')
- with assert_notification(type=NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_DECLINED, user=request.target.creator):
+ with assert_notification(type=NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_DECLINED, user=request.target.creator):
res = app.post_json_api(url, payload, auth=moderator.auth)
assert res.status_code == 201
request.reload()
@@ -402,8 +402,8 @@ def test_email_sent_on_approve(self, app, moderator, url, pre_request, post_requ
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=moderator.auth)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_APPROVED
- assert notifications['emits'][1]['type'] == NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_APPROVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_APPROVED
assert res.status_code == 201
request.reload()
request.target.reload()
diff --git a/api_tests/subscriptions/views/test_subscriptions_detail.py b/api_tests/subscriptions/views/test_subscriptions_detail.py
index f14ca4e2522..a7246bbbd19 100644
--- a/api_tests/subscriptions/views/test_subscriptions_detail.py
+++ b/api_tests/subscriptions/views/test_subscriptions_detail.py
@@ -1,11 +1,18 @@
import pytest
+
from django.contrib.contenttypes.models import ContentType
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import (
+ AbstractNode,
+ NotificationSubscription,
+ NotificationTypeEnum,
+ OSFUser
+)
from osf_tests.factories import (
AuthUserFactory,
- NotificationSubscriptionFactory
+ NodeFactory,
+ NotificationSubscriptionFactory,
)
@pytest.mark.django_db
@@ -16,22 +23,83 @@ def user(self):
return AuthUserFactory()
@pytest.fixture()
- def user_no_auth(self):
+ def user_missing_subscriptions(self):
+ return AuthUserFactory()
+
+ @pytest.fixture()
+ def user_no_permission(self):
return AuthUserFactory()
@pytest.fixture()
- def notification(self, user):
+ def node(self, user):
+ return NodeFactory(creator=user)
+
+ @pytest.fixture()
+ def node_missing_subscriptions(self, user_missing_subscriptions):
+ node = NodeFactory(creator=user_missing_subscriptions)
+ subscription = NotificationSubscription.objects.get(
+ user=user_missing_subscriptions,
+ notification_type__name=NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ object_id=node.id,
+ content_type=ContentType.objects.get_for_model(AbstractNode)
+ )
+ subscription.delete()
+ return node
+
+ @pytest.fixture()
+ def notification_user_global_file_updated(self, user):
+ return NotificationSubscriptionFactory(
+ notification_type=NotificationTypeEnum.USER_FILE_UPDATED.instance,
+ object_id=user.id,
+ content_type_id=ContentType.objects.get_for_model(OSFUser).id,
+ user=user,
+ _is_digest=True,
+ message_frequency='daily',
+ )
+
+ @pytest.fixture()
+ def notification_user_global_reviews(self, user):
return NotificationSubscriptionFactory(
- notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.instance,
object_id=user.id,
- content_type_id=ContentType.objects.get_for_model(user).id,
- user=user
+ content_type_id=ContentType.objects.get_for_model(OSFUser).id,
+ user=user,
+ _is_digest=True,
+ message_frequency='daily',
)
@pytest.fixture()
- def url(self, user):
+ def url_user_global_file_updated(self, user):
return f'/{API_BASE}subscriptions/{user._id}_global_file_updated/'
+ @pytest.fixture()
+ def url_user_global_reviews(self, user):
+ return f'/{API_BASE}subscriptions/{user._id}_global_reviews/'
+
+ @pytest.fixture()
+ def url_user_global_file_updated_missing(self, user_missing_subscriptions):
+ return f'/{API_BASE}subscriptions/{user_missing_subscriptions._id}_global_file_updated/'
+
+ @pytest.fixture()
+ def url_user_global_reviews_missing(self, user_missing_subscriptions):
+ return f'/{API_BASE}subscriptions/{user_missing_subscriptions._id}_global_reviews/'
+
+ @pytest.fixture()
+ def url_node_file_updated(self, node):
+ return f'/{API_BASE}subscriptions/{node._id}_file_updated/'
+
+ @pytest.fixture()
+ def url_node_file_updated_not_found(self):
+ return f'/{API_BASE}subscriptions/12345_file_updated/'
+
+ @pytest.fixture()
+ def url_node_file_updated_without_permission(self, node_without_permission):
+ return f'/{API_BASE}subscriptions/{node_without_permission._id}_file_updated/'
+
+ @pytest.fixture()
+ def url_node_file_updated_missing(self, node_missing_subscriptions):
+ return f'/{API_BASE}subscriptions/{node_missing_subscriptions._id}_file_updated/'
+
@pytest.fixture()
def url_invalid(self):
return f'/{API_BASE}subscriptions/invalid-notification-id/'
@@ -58,40 +126,158 @@ def payload_invalid(self):
}
}
- def test_subscription_detail_invalid_user(self, app, user, user_no_auth, notification, url, payload):
- res = app.get(
- url,
- auth=user_no_auth.auth,
- expect_errors=True
- )
+ def test_user_global_subscription_detail_permission_denied(
+ self,
+ app,
+ user,
+ user_no_permission,
+ notification_user_global_file_updated,
+ notification_user_global_reviews,
+ url_user_global_file_updated,
+ url_user_global_reviews
+ ):
+ res = app.get(url_user_global_file_updated, auth=user_no_permission.auth, expect_errors=True)
+ assert res.status_code == 403
+ res = app.get(url_user_global_reviews, auth=user_no_permission.auth, expect_errors=True)
assert res.status_code == 403
- def test_subscription_detail_no_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ def test_user_global_subscription_detail_forbidden(
+ self,
+ app,
+ user,
+ user_no_permission,
+ notification_user_global_file_updated,
+ notification_user_global_reviews,
+ url_user_global_file_updated,
+ url_user_global_reviews
):
- res = app.get(
- url,
- expect_errors=True
- )
+ res = app.get(url_user_global_file_updated, expect_errors=True)
+ assert res.status_code == 401
+ res = app.get(url_user_global_reviews, expect_errors=True)
assert res.status_code == 401
- def test_subscription_detail_valid_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ def test_user_global_subscription_detail_success(
+ self,
+ app,
+ user,
+ user_no_permission,
+ notification_user_global_file_updated,
+ notification_user_global_reviews,
+ url_user_global_file_updated,
+ url_user_global_reviews
):
-
- res = app.get(url, auth=user.auth)
+ res = app.get(url_user_global_file_updated, auth=user.auth)
notification_id = res.json['data']['id']
assert res.status_code == 200
assert notification_id == f'{user._id}_global_file_updated'
+ res = app.get(url_user_global_reviews, auth=user.auth)
+ notification_id = res.json['data']['id']
+ assert res.status_code == 200
+ assert notification_id == f'{user._id}_global_reviews'
+
+ def test_user_global_file_updated_subscription_detail_missing_and_created(
+ self,
+ app,
+ user_missing_subscriptions,
+ url_user_global_file_updated_missing,
+ ):
+ assert not NotificationSubscription.objects.filter(
+ user=user_missing_subscriptions,
+ notification_type__name=NotificationTypeEnum.USER_FILE_UPDATED.value,
+ object_id=user_missing_subscriptions.id,
+ content_type=ContentType.objects.get_for_model(OSFUser)
+ ).exists()
+ res = app.get(url_user_global_file_updated_missing, auth=user_missing_subscriptions.auth)
+ notification_id = res.json['data']['id']
+ assert res.status_code == 200
+ assert notification_id == f'{user_missing_subscriptions._id}_global_file_updated'
+
+ def test_user_global_reviews_subscription_detail_missing_and_created(
+ self,
+ app,
+ user_missing_subscriptions,
+ url_user_global_reviews_missing,
+ ):
+ assert not NotificationSubscription.objects.filter(
+ user=user_missing_subscriptions,
+ notification_type__name=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
+ object_id=user_missing_subscriptions.id,
+ content_type=ContentType.objects.get_for_model(OSFUser)
+ ).exists()
+ res = app.get(url_user_global_reviews_missing, auth=user_missing_subscriptions.auth)
+ notification_id = res.json['data']['id']
+ assert res.status_code == 200
+ assert notification_id == f'{user_missing_subscriptions._id}_global_reviews'
+
+ def test_node_file_updated_subscription_detail_success(
+ self,
+ app,
+ user,
+ node,
+ url_node_file_updated
+ ):
+ res = app.get(url_node_file_updated, auth=user.auth)
+ notification_id = res.json['data']['id']
+ assert res.status_code == 200
+ assert notification_id == f'{node._id}_file_updated'
+
+ def test_node_file_updated_subscription_detail_missing_and_created(
+ self,
+ app,
+ user_missing_subscriptions,
+ node_missing_subscriptions,
+ url_node_file_updated_missing,
+ ):
+ assert not NotificationSubscription.objects.filter(
+ user=user_missing_subscriptions,
+ notification_type__name=NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ object_id=node_missing_subscriptions.id,
+ content_type=ContentType.objects.get_for_model(AbstractNode)
+ ).exists()
+ res = app.get(url_node_file_updated_missing, auth=user_missing_subscriptions.auth)
+ notification_id = res.json['data']['id']
+ assert res.status_code == 200
+ assert notification_id == f'{node_missing_subscriptions._id}_file_updated'
+
+ def test_node_file_updated_subscription_detail_not_found(
+ self,
+ app,
+ user,
+ node,
+ url_node_file_updated_not_found
+ ):
+ res = app.get(url_node_file_updated_not_found, auth=user.auth, expect_errors=True)
+ assert res.status_code == 404
+
+ def test_node_file_updated_subscription_detail_permission_denied(
+ self,
+ app,
+ user,
+ user_no_permission,
+ node,
+ url_node_file_updated
+ ):
+ res = app.get(url_node_file_updated, auth=user_no_permission.auth, expect_errors=True)
+ assert res.status_code == 403
+
+ def test_node_file_updated_subscription_detail_forbidden(
+ self,
+ app,
+ user,
+ node,
+ url_node_file_updated
+ ):
+ res = app.get(url_node_file_updated, expect_errors=True)
+ assert res.status_code == 401
def test_subscription_detail_invalid_notification_id_no_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
res = app.get(url_invalid, expect_errors=True)
assert res.status_code == 404
def test_subscription_detail_invalid_notification_id_existing_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
res = app.get(
url_invalid,
@@ -101,22 +287,22 @@ def test_subscription_detail_invalid_notification_id_existing_user(
assert res.status_code == 404
def test_subscription_detail_invalid_payload_403(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
- res = app.patch_json_api(url, payload_invalid, auth=user_no_auth.auth, expect_errors=True)
+ res = app.patch_json_api(url_user_global_file_updated, payload_invalid, auth=user_no_permission.auth, expect_errors=True)
assert res.status_code == 403
def test_subscription_detail_invalid_payload_401(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
- res = app.patch_json_api(url, payload_invalid, expect_errors=True)
+ res = app.patch_json_api(url_user_global_file_updated, payload_invalid, expect_errors=True)
assert res.status_code == 401
def test_subscription_detail_invalid_payload_400(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
res = app.patch_json_api(
- url,
+ url_user_global_file_updated,
payload_invalid,
auth=user.auth,
expect_errors=True,
@@ -126,33 +312,33 @@ def test_subscription_detail_invalid_payload_400(
assert res.json['errors'][0]['detail'] == ('"invalid-frequency" is not a valid choice.')
def test_subscription_detail_patch_invalid_notification_id_no_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
res = app.patch_json_api(url_invalid, payload, expect_errors=True)
assert res.status_code == 404
def test_subscription_detail_patch_invalid_notification_id_existing_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
res = app.patch_json_api(url_invalid, payload, auth=user.auth, expect_errors=True)
assert res.status_code == 404
def test_subscription_detail_patch_invalid_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
- res = app.patch_json_api(url, payload, auth=user_no_auth.auth, expect_errors=True)
+ res = app.patch_json_api(url_user_global_file_updated, payload, auth=user_no_permission.auth, expect_errors=True)
assert res.status_code == 403
def test_subscription_detail_patch_no_user(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
- res = app.patch_json_api(url, payload, expect_errors=True)
+ res = app.patch_json_api(url_user_global_file_updated, payload, expect_errors=True)
assert res.status_code == 401
def test_subscription_detail_patch(
- self, app, user, user_no_auth, notification, url, url_invalid, payload, payload_invalid
+ self, app, user, user_no_permission, notification_user_global_file_updated, url_user_global_file_updated, url_invalid, payload, payload_invalid
):
- res = app.patch_json_api(url, payload, auth=user.auth)
+ res = app.patch_json_api(url_user_global_file_updated, payload, auth=user.auth)
assert res.status_code == 200
assert res.json['data']['attributes']['frequency'] == 'none'
diff --git a/api_tests/subscriptions/views/test_subscriptions_list.py b/api_tests/subscriptions/views/test_subscriptions_list.py
index 599df9ddcd6..f4e858a6f93 100644
--- a/api_tests/subscriptions/views/test_subscriptions_list.py
+++ b/api_tests/subscriptions/views/test_subscriptions_list.py
@@ -2,7 +2,7 @@
from django.contrib.contenttypes.models import ContentType
from api.base.settings.defaults import API_BASE
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
PreprintProviderFactory,
@@ -31,7 +31,7 @@ def node(self, user):
@pytest.fixture()
def global_user_notification(self, user):
return NotificationSubscriptionFactory(
- notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.USER_FILE_UPDATED.instance,
object_id=user.id,
content_type_id=ContentType.objects.get_for_model(user).id,
user=user,
@@ -40,7 +40,7 @@ def global_user_notification(self, user):
@pytest.fixture()
def file_updated_notification(self, node, user):
return NotificationSubscriptionFactory(
- notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.NODE_FILE_UPDATED.instance,
object_id=node.id,
content_type_id=ContentType.objects.get_for_model(node).id,
user=user,
@@ -49,7 +49,7 @@ def file_updated_notification(self, node, user):
@pytest.fixture()
def provider_notification(self, provider, user):
return NotificationSubscriptionFactory(
- notification_type=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance,
+ notification_type=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.instance,
object_id=provider.id,
content_type_id=ContentType.objects.get_for_model(provider).id,
subscribed_object=provider,
@@ -91,10 +91,10 @@ def test_cannot_post_patch_put_or_delete(self, app, url, user):
assert delete_res.status_code == 405
def test_multiple_values_filter(self, app, url, user):
- res = app.get(url + '?filter[event_name]=global_file_updated,files_updated', auth=user.auth)
+ res = app.get(url + '?filter[event_name]=global_file_updated,file_updated', auth=user.auth)
assert len(res.json['data']) == 2
for subscription in res.json['data']:
- subscription['attributes']['event_name'] in ['global', 'comments']
+ assert subscription['attributes']['event_name'] in ['global_file_updated', 'file_updated']
def test_value_filter_id(
self,
@@ -122,5 +122,5 @@ def test_value_filter_id(
# Confirm it’s the expected subscription object
attributes = data[0]['attributes']
- assert attributes['event_name'] == 'files_updated' # event names are legacy
+ assert attributes['event_name'] == 'file_updated' # event names are legacy
assert attributes['frequency'] in ['instantly', 'daily', 'none']
diff --git a/api_tests/users/views/test_user_claim.py b/api_tests/users/views/test_user_claim.py
index 15e00c82feb..739b9757ea5 100644
--- a/api_tests/users/views/test_user_claim.py
+++ b/api_tests/users/views/test_user_claim.py
@@ -6,7 +6,7 @@
from api.users.views import ClaimUser
from api_tests.utils import only_supports_methods
from framework.auth.core import Auth
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
ProjectFactory,
@@ -126,7 +126,7 @@ def test_claim_unauth_success_with_original_email(self, app, url, project, unreg
self.payload(email='david@david.son', id=project._id),
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
assert res.status_code == 204
def test_claim_unauth_success_with_claimer_email(self, app, url, unreg_user, project, claimer):
@@ -137,8 +137,8 @@ def test_claim_unauth_success_with_claimer_email(self, app, url, unreg_user, pro
)
assert res.status_code == 204
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORWARD_INVITE_REGISTERED
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION_REGISTERED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION_REGISTERED
def test_claim_unauth_success_with_unknown_email(self, app, url, project, unreg_user):
with capture_notifications() as notifications:
@@ -148,8 +148,8 @@ def test_claim_unauth_success_with_unknown_email(self, app, url, project, unreg_
)
assert res.status_code == 204
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_FORWARD_INVITE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE
def test_claim_unauth_success_with_preprint_id(self, app, url, preprint, unreg_user):
with capture_notifications() as notifications:
@@ -159,7 +159,7 @@ def test_claim_unauth_success_with_preprint_id(self, app, url, preprint, unreg_u
)
assert res.status_code == 204
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
def test_claim_auth_failure(self, app, url, claimer, wrong_preprint, project, unreg_user, referrer):
_url = url.format(unreg_user._id)
@@ -228,8 +228,8 @@ def test_claim_auth_throttle_error(self, app, url, claimer, unreg_user, project)
expect_errors=True
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORWARD_INVITE_REGISTERED
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION_REGISTERED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION_REGISTERED
res = app.post_json_api(
url.format(unreg_user._id),
self.payload(id=project._id),
@@ -248,8 +248,8 @@ def test_claim_auth_success(self, app, url, claimer, unreg_user, project):
)
assert res.status_code == 204
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORWARD_INVITE_REGISTERED
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION_REGISTERED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION_REGISTERED
@pytest.mark.django_db
diff --git a/api_tests/users/views/test_user_confirm.py b/api_tests/users/views/test_user_confirm.py
index 230b4a5644a..5c9643e3e02 100644
--- a/api_tests/users/views/test_user_confirm.py
+++ b/api_tests/users/views/test_user_confirm.py
@@ -1,7 +1,7 @@
import pytest
from api.base.settings.defaults import API_BASE
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import AuthUserFactory
from tests.utils import capture_notifications
@@ -168,7 +168,7 @@ def test_post_success_link(self, app, confirm_url, user_with_email_verification)
assert res.status_code == 201
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_EXTERNAL_LOGIN_LINK_SUCCESS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_EXTERNAL_LOGIN_LINK_SUCCESS
user.reload()
assert user.external_identity['ORCID']['0000-0000-0000-0000'] == 'VERIFIED'
diff --git a/api_tests/users/views/test_user_list.py b/api_tests/users/views/test_user_list.py
index 883defee671..a98fc253fee 100644
--- a/api_tests/users/views/test_user_list.py
+++ b/api_tests/users/views/test_user_list.py
@@ -10,7 +10,7 @@
from api.base.settings.defaults import API_BASE
from framework.auth.cas import CasResponse
-from osf.models import OSFUser, ApiOAuth2PersonalToken, NotificationType
+from osf.models import OSFUser, ApiOAuth2PersonalToken, NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
UserFactory,
@@ -320,7 +320,7 @@ def test_cookied_requests_can_create_and_email(
data
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
assert res.status_code == 201
assert OSFUser.objects.filter(username=email_unconfirmed).count() == 1
@@ -359,7 +359,7 @@ def test_properly_scoped_token_can_create_and_send_email(
headers={'Authorization': f'Bearer {token.token_id}'}
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
assert res.status_code == 201
assert res.json['data']['attributes']['username'] == email_unconfirmed
@@ -519,7 +519,7 @@ def test_admin_scoped_token_can_create_and_send_email(
headers={'Authorization': f'Bearer {token.token_id}'}
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_MODERATOR_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_MODERATOR_ADDED
assert res.status_code == 201
assert res.json['data']['attributes']['username'] == email_unconfirmed
diff --git a/api_tests/users/views/test_user_message_institutional_access.py b/api_tests/users/views/test_user_message_institutional_access.py
index 04a6cff1f4c..069409ccb0f 100644
--- a/api_tests/users/views/test_user_message_institutional_access.py
+++ b/api_tests/users/views/test_user_message_institutional_access.py
@@ -1,6 +1,6 @@
import pytest
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf.models.user_message import MessageTypes, UserMessage
from api.base.settings.defaults import API_BASE
from osf_tests.factories import (
@@ -221,7 +221,7 @@ def test_cc_institutional_admin(
auth=institutional_admin.auth,
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INSTITUTIONAL_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INSTITUTIONAL_ACCESS_REQUEST
assert notifications['emits'][0]['kwargs']['user'].username == user_with_affiliation.username
assert res.status_code == 201
user_message = UserMessage.objects.get()
@@ -235,7 +235,7 @@ def test_cc_field_defaults_to_false(self, app, institutional_admin, url_with_aff
with capture_notifications() as notifications:
res = app.post_json_api(url_with_affiliation, payload, auth=institutional_admin.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INSTITUTIONAL_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INSTITUTIONAL_ACCESS_REQUEST
assert notifications['emits'][0]['kwargs']['user'].username == user_with_affiliation.username
assert res.status_code == 201
diff --git a/api_tests/users/views/test_user_settings.py b/api_tests/users/views/test_user_settings.py
index 53dc254b519..d543c781427 100644
--- a/api_tests/users/views/test_user_settings.py
+++ b/api_tests/users/views/test_user_settings.py
@@ -7,7 +7,7 @@
AuthUserFactory,
UserFactory,
)
-from osf.models import Email, NotableDomain, NotificationType
+from osf.models import Email, NotableDomain, NotificationTypeEnum
from framework.auth.views import auth_email_logout
from tests.utils import capture_notifications
@@ -59,7 +59,7 @@ def test_post(self, app, user_one, user_two, url, payload):
with capture_notifications() as notifications:
res = app.post_json_api(url, payload, auth=user_one.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_REQUEST_EXPORT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_REQUEST_EXPORT
assert res.status_code == 204
user_one.reload()
assert user_one.email_last_sent is not None
diff --git a/api_tests/users/views/test_user_settings_reset_password.py b/api_tests/users/views/test_user_settings_reset_password.py
index 62687a58612..51c2db80038 100644
--- a/api_tests/users/views/test_user_settings_reset_password.py
+++ b/api_tests/users/views/test_user_settings_reset_password.py
@@ -3,7 +3,7 @@
from api.base.settings.defaults import API_BASE
from api.base.settings import CSRF_COOKIE_NAME
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import (
UserFactory,
)
@@ -49,7 +49,7 @@ def test_get(self, app, url, user_one):
with capture_notifications() as notifications:
res = app.get(url)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORGOT_PASSWORD
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORGOT_PASSWORD
assert res.status_code == 200
user_one.reload()
diff --git a/framework/auth/campaigns.py b/framework/auth/campaigns.py
index 307413c779c..d0be8e1707e 100644
--- a/framework/auth/campaigns.py
+++ b/framework/auth/campaigns.py
@@ -4,7 +4,7 @@
from django.utils import timezone
from website import settings
-from osf.models import PreprintProvider, NotificationType
+from osf.models import PreprintProvider, NotificationTypeEnum
from website.settings import DOMAIN, CAMPAIGN_REFRESH_THRESHOLD
from website.util.metrics import OsfSourceTags, OsfClaimedTags, CampaignSourceTags, CampaignClaimedTags, provider_source_tag
from framework.utils import throttle_period_expired
@@ -14,7 +14,7 @@
CAMPAIGNS = None
CAMPAIGNS_LAST_REFRESHED = timezone.now()
-
+# TODO: Notification Refactor have replaced deprecated notification types with placeholder ones; still need to clean up deprecated campaigns.
def get_campaigns():
global CAMPAIGNS
@@ -26,7 +26,7 @@ def get_campaigns():
'erpc': {
'system_tag': CampaignSourceTags.ErpChallenge.value,
'redirect_url': furl(DOMAIN).add(path='erpc/').url,
- 'confirmation_email_template': NotificationType.Type.USER_CAMPAIGN_CONFIRM_EMAIL_ERPC,
+ 'confirmation_email_template': NotificationTypeEnum.USER_CAMPAIGN_CONFIRM_EMAIL_ERPC,
'login_type': 'native',
},
}
@@ -44,12 +44,12 @@ def get_campaigns():
preprint_providers = PreprintProvider.objects.all()
for provider in preprint_providers:
if provider._id == 'osf':
- confirmation_email_template = NotificationType.Type.USER_CAMPAIGN_CONFIRM_PREPRINTS_OSF
+ confirmation_email_template = NotificationTypeEnum.USER_CONFIRM_EMAIL # added as a placeholder as removed NotificationType
name = 'OSF'
url_path = 'preprints/'
external_url = None
else:
- confirmation_email_template = NotificationType.Type.USER_CAMPAIGN_CONFIRM_PREPRINTS_BRANDED
+ confirmation_email_template = NotificationTypeEnum.USER_CONFIRM_EMAIL # added as a placeholder as removed NotificationType
name = provider.name
url_path = f'preprints/{provider._id}'
@@ -85,7 +85,7 @@ def get_campaigns():
'osf-registered-reports': {
'system_tag': CampaignSourceTags.OsfRegisteredReports.value,
'redirect_url': furl(DOMAIN).add(path='rr/').url,
- 'confirmation_email_template': NotificationType.Type.USER_CAMPAIGN_CONFIRM_EMAIL_REGISTRIES_OSF,
+ 'confirmation_email_template': NotificationTypeEnum.USER_CAMPAIGN_CONFIRM_EMAIL_REGISTRIES_OSF,
'login_type': 'proxy',
'provider': 'osf',
'logo': settings.OSF_REGISTRIES_LOGO
@@ -96,7 +96,7 @@ def get_campaigns():
'agu_conference_2023': {
'system_tag': CampaignSourceTags.AguConference2023.value,
'redirect_url': furl(DOMAIN).add(path='dashboard/').url,
- 'confirmation_email_template': NotificationType.Type.USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE_2023,
+ 'confirmation_email_template': NotificationTypeEnum.USER_CONFIRM_EMAIL, # added as a placeholder as removed NotificationType
'login_type': 'native',
}
})
@@ -105,7 +105,7 @@ def get_campaigns():
'agu_conference': {
'system_tag': CampaignSourceTags.AguConference.value,
'redirect_url': furl(DOMAIN).add(path='dashboard/').url,
- 'confirmation_email_template': NotificationType.Type.USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE,
+ 'confirmation_email_template': NotificationTypeEnum.USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE,
'login_type': 'native',
}
})
diff --git a/framework/auth/views.py b/framework/auth/views.py
index 0338654fac3..aefb1b4f0e8 100644
--- a/framework/auth/views.py
+++ b/framework/auth/views.py
@@ -26,7 +26,7 @@
from framework.sessions.utils import remove_sessions_for_user
from framework.sessions import get_session
from framework.utils import throttle_period_expired
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationTypeEnum
from osf.utils.sanitize import strip_html
from website import settings, language
from website.util import web_url_for
@@ -208,7 +208,7 @@ def forgot_password_post():
"""Dispatches to ``_forgot_password_post`` passing non-institutional user mail template
and reset action."""
return _forgot_password_post(
- notificaton_type=NotificationType.Type.USER_FORGOT_PASSWORD,
+ notificaton_type=NotificationTypeEnum.USER_FORGOT_PASSWORD,
reset_route='reset_password_get'
)
@@ -217,7 +217,7 @@ def forgot_password_institution_post():
"""Dispatches to `_forgot_password_post` passing institutional user mail template, reset
action, and setting the ``institutional`` flag."""
return _forgot_password_post(
- notificaton_type=NotificationType.Type.USER_FORGOT_PASSWORD_INSTITUTION,
+ notificaton_type=NotificationTypeEnum.USER_FORGOT_PASSWORD_INSTITUTION,
reset_route='reset_password_institution_get',
institutional=True
)
@@ -280,8 +280,6 @@ def _forgot_password_post(notificaton_type, reset_route, institutional=False):
user=user_obj,
event_context={
'reset_link': reset_link,
- 'can_change_preferences': False,
- 'osf_contact_email': settings.OSF_CONTACT_EMAIL,
},
)
@@ -659,12 +657,11 @@ def external_login_confirm_email_get(auth, uid, token):
if external_status == 'CREATE':
service_url += '&{}'.format(urlencode({'new': 'true'}))
elif external_status == 'LINK':
- NotificationType.Type.USER_EXTERNAL_LOGIN_LINK_SUCCESS.instance.emit(
+ NotificationTypeEnum.USER_EXTERNAL_LOGIN_LINK_SUCCESS.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
'external_id_provider': provider,
- 'can_change_preferences': False,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
},
)
@@ -839,9 +836,9 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte
if external_id_provider and external_id:
# First time login through external identity provider, link or create an OSF account confirmation
if user.external_identity[external_id_provider][external_id] == 'CREATE':
- notification_type = NotificationType.Type.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_CREATE
+ notification_type = NotificationTypeEnum.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_CREATE
elif user.external_identity[external_id_provider][external_id] == 'LINK':
- notification_type = NotificationType.Type.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK
+ notification_type = NotificationTypeEnum.USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK
else:
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={})
elif merge_target:
@@ -851,25 +848,24 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte
'user_username': user.username,
'email': merge_target.email,
}
- notification_type = NotificationType.Type.USER_CONFIRM_MERGE
+ notification_type = NotificationTypeEnum.USER_CONFIRM_MERGE
logout_query = '?logout=1'
elif user.is_active:
# Add email confirmation
- notification_type = NotificationType.Type.USER_CONFIRM_EMAIL
+ notification_type = NotificationTypeEnum.USER_CONFIRM_EMAIL
logout_query = '?logout=1'
elif campaign:
# Account creation confirmation: from campaign
notification_type = campaigns.email_template_for_campaign(campaign)
else:
# Account creation confirmation: from OSF
- notification_type = NotificationType.Type.USER_INITIAL_CONFIRM_EMAIL
+ notification_type = NotificationTypeEnum.USER_INITIAL_CONFIRM_EMAIL
notification_type.instance.emit(
destination_address=email,
event_context={
'user_fullname': user.fullname,
'confirmation_url': f'{confirmation_url}{logout_query}',
- 'can_change_preferences': False,
'external_id_provider': external_id_provider,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
'osf_support_email': settings.OSF_SUPPORT_EMAIL,
diff --git a/notifications.yaml b/notifications.yaml
index 29d896cdc89..b867c3a2d38 100644
--- a/notifications.yaml
+++ b/notifications.yaml
@@ -46,27 +46,6 @@ notification_types:
tests: ['osf_tests/test_user.py', 'tests/test_auth.py']
template: 'website/templates/password_reset.html.mako'
- - name: user_contributor_added_preprint_node_from_osf
- subject: 'You have been added as a contributor to an OSF preprint'
- __docs__: ...
- object_content_type_model_name: osfuser
- template: 'website/templates/contributor_added_preprint_node_from_osf.html.mako'
- tests: []
-
- - name: user_contributor_added_access_request
- subject: 'You have been added as a contributor to an OSF project'
- __docs__: ...
- object_content_type_model_name: osfuser
- tests: ['tests/test_claim_views.py', 'api_tests/requests/views/test_request_actions_create.py', 'api_tests/requests/mixins.py', 'osf_tests/test_institutional_admin_contributors.py']
- template: 'website/templates/contributor_added_access_request.html.mako'
-
- - name: user_contributor_added_osf_preprint
- subject: 'You have been added as a contributor to an OSF preprint.'
- __docs__: ...
- object_content_type_model_name: osfuser
- tests: []
- template: 'website/templates/contributor_added_preprint_node_from_osf.html.mako'
-
- name: user_external_login_link_success
subject: 'OSF Verification Success'
__docs__: ...
@@ -81,13 +60,6 @@ notification_types:
tests: ['tests/test_auth_views.py']
template: 'website/templates/confirm.html.mako'
- - name: forgot_password
- subject: 'Reset Password'
- __docs__: ...
- object_content_type_model_name: osfuser
- tests: ['api_tests/users/views/test_user_settings_reset_password.py', 'tests/test_forgot_password.py']
- template: 'website/templates/forgot_password.html.mako'
-
- name: user_welcome
subject: 'Welcome to OSF'
__docs__: ...
@@ -137,13 +109,6 @@ notification_types:
tests: ['tests/test_adding_contributor_views.py', 'api_tests/users/views/test_user_claim.py', 'tests/test_claim_views.py']
template: 'website/templates/invite_default.html.mako'
- - name: user_pending_invite
- subject: 'You have been invited to contribute to an OSF project'
- __docs__: ...
- object_content_type_model_name: osfuser
- tests: []
- template: 'website/templates/pending_invite.html.mako'
-
- name: user_forward_invite_registered
subject: 'Please forward to {user_fullname}'
__docs__: ...
@@ -165,13 +130,6 @@ notification_types:
template: 'website/templates/initial_confirm.html.mako'
tests: ['tests/test_resend_confirmation.py', 'tests/test_auth.py']
- - name: user_export_data_request
- subject: 'Data Export Request'
- __docs__: ...
- object_content_type_model_name: osfuser
- template: 'website/templates/support_request.html.mako'
- tests: []
-
- name: user_request_deactivation
subject: '[via OSF] Deactivation Request'
__docs__: ...
@@ -193,13 +151,6 @@ notification_types:
template: 'website/templates/storage_cap_exceeded_announcement.html.mako'
tests: []
- - name: user_duplicate_accounts_sso_osf4i
- subject: 'Duplicate Account Detection'
- __docs__: ...
- object_content_type_model_name: osfuser
- template: 'website/templates/duplicate_accounts_sso_osf4i.html.mako'
- tests: []
-
- name: user_duplicate_accounts_osf4i
subject: 'Duplicate Account Detection'
__docs__: ...
@@ -242,14 +193,14 @@ notification_types:
template: 'website/templates/registration_bulk_upload_success_partial.html.mako'
tests: ['api_tests/providers/tasks/test_bulk_upload.py']
- - name: user_registration_bulk_upload_failure_duplicates
+ - name: registration_bulk_upload_failure_duplicates
subject: 'Bulk Registration Upload Failed - Duplicates Found'
__docs__: ...
object_content_type_model_name: osfuser
template: 'website/templates/registration_bulk_upload_failure_duplicates.html.mako'
tests: []
- - name: registration_bulk_upload_unexpected_failure
+ - name: desk_user_registration_bulk_upload_unexpected_failure
subject: 'Bulk Registration Upload Unexpected Failed'
__docs__: ...
object_content_type_model_name: osfuser
@@ -319,30 +270,24 @@ notification_types:
template: 'website/templates/archive_uncaught_error_user.html.mako'
tests: []
- - name: user_confirm_email_erpc
+ - name: user_campaign_confirm_email_erpc
subject: 'OSF Account Verification, Election Research Preacceptance Competition'
object_content_type_model_name: osfuser
template: 'website/templates/confirm_erpc.html.mako'
tests: []
- - name: user_confirm_email_agu_conference
+ - name: user_campaign_confirm_email_agu_conference
subject: 'OSF Account Verification, from the American Geophysical Union Conference'
object_content_type_model_name: osfuser
template: 'website/templates/confirm_erpc.html.mako'
tests: []
- - name: user_confirm_email_registries_osf
+ - name: user_campaign_confirm_email_registries_osf # TODO: recheck
subject: 'OSF Account Verification, OSF Registries'
object_content_type_model_name: osfuser
template: 'website/templates/confirm_registries_osf.html.mako'
tests: []
- - name: user_confirm_email_preprints
- subject: 'OSF Account Verification, Preprints'
- object_content_type_model_name: osfuser
- template: 'website/templates/confirm_preprints_osf.html.mako'
- tests: ['']
-
- name: user_confirm_merge
subject: 'Confirm account merge'
object_content_type_model_name: osfuser
@@ -373,7 +318,7 @@ notification_types:
template: 'website/templates/tou_notif.html.mako'
tests: []
- - name: user_registration_bulk_upload_product_owner
+ - name: desk_registration_bulk_upload_product_owner
subject: 'Registry Could Not Bulk Upload Registrations'
object_content_type_model_name: osfuser
template: 'website/templates/registration_bulk_upload_product_owner.html.mako'
@@ -412,13 +357,6 @@ notification_types:
template: 'website/templates/new_pending_withdraw_requests.html.mako'
tests: ['tests/test_registrations/test_review_flows.py']
- - name: provider_contributor_added_preprint
- subject: 'Contributor Added to Preprint'
- __docs__: ...
- object_content_type_model_name: abstractprovider
- template: 'website/templates/contributor_added_preprints.html.mako'
- tests: []
-
- name: provider_confirm_email_moderation
subject: 'OSF Account Verification, {provider_name}'
__docs__: ...
@@ -447,23 +385,8 @@ notification_types:
template: 'website/templates/reviews_resubmission_confirmation.html.mako'
tests: ['osf_tests/test_reviewable.py']
- #### NODE
- - name: node_wiki_updated
- subject: 'Wiki Updated'
- __docs__: ...
- object_content_type_model_name: abstractnode
- template: 'website/templates/file_updated.html.mako'
- tests: ['tests/test_events.py']
-
- - name: node_files_updated
- subject: 'Files Updated'
- __docs__: ...
- object_content_type_model_name: abstractnode
- template: 'website/templates/file_updated.html.mako'
- tests: ['tests/test_events.py']
-
- name: node_file_updated
- subject: 'File Updated'
+ subject: 'Files Updated'
__docs__: ...
object_content_type_model_name: abstractnode
template: 'website/templates/file_updated.html.mako'
@@ -488,7 +411,7 @@ notification_types:
__docs__: ...
object_content_type_model_name: abstractnode
template: 'website/templates/contributor_added_access_request.html.mako'
- tests: []
+ tests: ['tests/test_claim_views.py', 'api_tests/requests/views/test_request_actions_create.py', 'api_tests/requests/mixins.py', 'osf_tests/test_institutional_admin_contributors.py']
- name: node_pending_registration_admin
subject: 'Pending Registration - Admin Notification'
@@ -610,26 +533,13 @@ notification_types:
template: 'website/templates/updates_rejected.html.mako'
tests: ['osf_tests/test_schema_responses.py']
- - name: node_archive_file_not_found_desk
- subject: 'Problem registering {src_title}'
- __docs__: ...
- object_content_type_model_name: abstractnode
- template: 'website/templates/archive_file_not_found_desk.html.mako'
- tests: []
-
- - name: node_archive_file_not_found_user
+ - name: user_archive_job_file_not_found
subject: 'Registration failed because of altered files'
object_content_type_model_name: abstractnode
template: 'website/templates/archive_file_not_found_user.html.mako'
tests: []
- - name: node_archive_uncaught_error_desk
- subject: 'Problem registering {src_title}'
- object_content_type_model_name: abstractnode
- template: 'website/templates/archive_uncaught_error_desk.html.mako'
- tests: []
-
- - name: node_archive_registration_stuck_desk
+ - name: desk_archive_registration_stuck
subject: '[auto] Stuck registrations audit'
object_content_type_model_name: abstractnode
template: 'website/templates/archive_registration_stuck_desk.html.mako'
@@ -641,18 +551,10 @@ notification_types:
template: 'website/templates/archive_success.html.mako'
tests: []
- - name: node_withdraw_registration_approved
- object_content_type_model_name: abstractnode
- template: 'website/templates/withdrawal_request_granted.html.mako'
-
- name: node_withdrawal_request_approved
object_content_type_model_name: abstractnode
template: 'website/templates/withdrawal_request_granted.html.mako'
- - name: node_withdraw_registration_declined
- object_content_type_model_name: abstractnode
- template: 'website/templates/withdrawal_request_declined.html.mako'
-
- name: node_new_public_project
subject: 'Now, public. Next, impact.'
__docs__: ...
@@ -690,18 +592,6 @@ notification_types:
template: 'website/templates/contributor_added_preprints.html.mako'
tests: ['api_tests/preprints/views/test_preprint_contributors_list.py']
- - name: preprint_withdrawal_request_granted
- subject: 'Your {document_type} has been withdrawn'
- object_content_type_model_name: preprint
- template: 'website/templates/withdrawal_request_granted.html.mako'
- tests: []
-
- - name: preprint_withdrawal_request_declined
- subject: 'Your withdrawal request has been declined'
- object_content_type_model_name: preprint
- template: 'website/templates/withdrawal_request_declined.html.mako'
- tests: []
-
#### SUPPORT
#### Collection Submissions
- name: collection_submission_removed_moderator
@@ -796,13 +686,6 @@ notification_types:
template: 'website/templates/archive_uncaught_error_desk.html.mako'
tests: []
- - name: desk_osf_support_email
- subject: 'OSF Support Email'
- __docs__: ...
- object_content_type_model_name: desk
- template: 'website/templates/crossref_doi_error.html.mako'
- tests: []
-
- name: desk_request_deactivation
subject: 'Deactivation Request'
__docs__: ...
diff --git a/notifications/file_event_notifications.py b/notifications/file_event_notifications.py
index 6d0fc3605bf..d57d4d8e9cf 100644
--- a/notifications/file_event_notifications.py
+++ b/notifications/file_event_notifications.py
@@ -15,6 +15,7 @@
from osf.models import (
NotificationType,
+ NotificationTypeEnum,
AbstractNode,
NodeLog,
Preprint,
@@ -96,13 +97,13 @@ def file_updated(self, target=None, user=None, event_type=None, payload=None):
return
event = {
- NodeLog.FILE_RENAMED: NotificationType.Type.ADDON_FILE_RENAMED,
- NodeLog.FILE_COPIED: NotificationType.Type.ADDON_FILE_COPIED,
- NodeLog.FILE_ADDED: NotificationType.Type.FILE_ADDED,
- NodeLog.FILE_MOVED: NotificationType.Type.ADDON_FILE_MOVED,
- NodeLog.FILE_REMOVED: NotificationType.Type.FILE_REMOVED,
- NodeLog.FILE_UPDATED: NotificationType.Type.FILE_UPDATED,
- NodeLog.FOLDER_CREATED: NotificationType.Type.FOLDER_CREATED,
+ NodeLog.FILE_RENAMED: NotificationTypeEnum.ADDON_FILE_RENAMED,
+ NodeLog.FILE_COPIED: NotificationTypeEnum.ADDON_FILE_COPIED,
+ NodeLog.FILE_ADDED: NotificationTypeEnum.FILE_ADDED,
+ NodeLog.FILE_MOVED: NotificationTypeEnum.ADDON_FILE_MOVED,
+ NodeLog.FILE_REMOVED: NotificationTypeEnum.FILE_REMOVED,
+ NodeLog.FILE_UPDATED: NotificationTypeEnum.FILE_UPDATED,
+ NodeLog.FOLDER_CREATED: NotificationTypeEnum.FOLDER_CREATED,
}[event_type]
if event not in event_registry:
@@ -268,7 +269,7 @@ def perform(self):
super().perform()
return
- NotificationType.Type.ADDON_FILE_RENAMED.instance.emit(
+ NotificationTypeEnum.ADDON_FILE_RENAMED.instance.emit(
user=self.user,
event_context={
'user_fullname': self.user.fullname,
@@ -305,7 +306,7 @@ def perform(self):
super().perform()
return
- NotificationType.Type.ADDON_FILE_MOVED.instance.emit(
+ NotificationTypeEnum.ADDON_FILE_MOVED.instance.emit(
user=self.user,
event_context={
'user_fullname': self.user.fullname,
@@ -325,7 +326,7 @@ def perform(self):
super().perform()
return
- NotificationType.Type.ADDON_FILE_COPIED.instance.emit(
+ NotificationTypeEnum.ADDON_FILE_COPIED.instance.emit(
user=self.user,
event_context={
'user_fullname': self.user.fullname,
diff --git a/notifications/listeners.py b/notifications/listeners.py
index 97eba256c53..801690a976c 100644
--- a/notifications/listeners.py
+++ b/notifications/listeners.py
@@ -10,7 +10,7 @@
@project_created.connect
def subscribe_creator(resource):
- from osf.models import NotificationSubscription, NotificationType, Preprint
+ from osf.models import NotificationSubscription, NotificationTypeEnum, Preprint
from django.contrib.contenttypes.models import ContentType
@@ -21,7 +21,7 @@ def subscribe_creator(resource):
try:
NotificationSubscription.objects.get_or_create(
user=user,
- notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.USER_FILE_UPDATED.instance,
object_id=user.id,
content_type=ContentType.objects.get_for_model(user),
_is_digest=True,
@@ -35,7 +35,7 @@ def subscribe_creator(resource):
try:
NotificationSubscription.objects.get_or_create(
user=user,
- notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
@@ -49,7 +49,7 @@ def subscribe_creator(resource):
@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, Preprint
+ from osf.models import NotificationSubscription, NotificationTypeEnum, Preprint
from osf.models import Node
if isinstance(resource, Node):
@@ -59,7 +59,7 @@ def subscribe_contributor(resource, contributor, auth=None, *args, **kwargs):
try:
NotificationSubscription.objects.get_or_create(
user=contributor,
- notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.USER_FILE_UPDATED.instance,
object_id=contributor.id,
content_type=ContentType.objects.get_for_model(contributor),
_is_digest=True,
@@ -73,7 +73,7 @@ def subscribe_contributor(resource, contributor, auth=None, *args, **kwargs):
try:
NotificationSubscription.objects.get_or_create(
user=contributor,
- notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.NODE_FILE_UPDATED.instance,
object_id=resource.id,
content_type=ContentType.objects.get_for_model(resource),
_is_digest=True,
@@ -89,7 +89,7 @@ def subscribe_contributor(resource, contributor, auth=None, *args, **kwargs):
@reviews_signals.reviews_withdraw_requests_notification_moderators.connect
def reviews_withdraw_requests_notification_moderators(self, timestamp, context, user, resource):
from website.settings import DOMAIN
- from osf.models import NotificationType
+ from osf.models import NotificationTypeEnum
provider = resource.provider
context['provider_id'] = provider.id
@@ -99,7 +99,7 @@ def reviews_withdraw_requests_notification_moderators(self, timestamp, context,
# Set submission url
context['reviews_submission_url'] = f'{DOMAIN}{resource._id}?mode=moderator'
context['localized_timestamp'] = str(timestamp)
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
subscribed_object=provider,
user=user,
event_context=context,
@@ -154,9 +154,9 @@ def queue_no_addon_email(user):
`settings.NO_ADDON_WAIT_TIME` months of signing up for the OSF.
"""
from website.settings import DOMAIN
- from osf.models import NotificationType
+ from osf.models import NotificationTypeEnum
- NotificationType.Type.USER_NO_ADDON.instance.emit(
+ NotificationTypeEnum.USER_NO_ADDON.instance.emit(
subscribed_object=user,
user=user,
event_context={
diff --git a/notifications/tasks.py b/notifications/tasks.py
index 84a825088f2..d929c723171 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -1,7 +1,6 @@
import itertools
from calendar import monthrange
-from datetime import date, datetime
-from django.contrib.contenttypes.models import ContentType
+from datetime import date
from django.db import connection
from django.utils import timezone
from django.core.validators import EmailValidator
@@ -11,7 +10,7 @@
from celery.utils.log import get_task_logger
from framework.postcommit_tasks.handlers import run_postcommit
-from osf.models import OSFUser, Notification, NotificationType, EmailTask, RegistrationProvider, \
+from osf.models import OSFUser, Notification, NotificationTypeEnum, EmailTask, RegistrationProvider, \
CollectionProvider, AbstractProvider
from framework.sentry import log_message
from osf.registrations.utils import get_registration_provider_submissions_url
@@ -34,10 +33,8 @@ def safe_render_notification(notifications, email_task):
email_task.save()
failed_notifications.append(notification.id)
# Mark notifications that failed to render as fake sent
- # Use 1000/12/31 to distinguish itself from another type of fake sent 1000/1/1
+ notification.mark_sent(fake_sent=True)
log_message(f'Error rendering notification, mark as fake sent: [notification_id={notification.id}]')
- notification.sent = datetime(1000, 12, 31)
- notification.save()
continue
rendered_notifications.append(rendered)
@@ -102,19 +99,18 @@ def send_user_email_task(self, user_id, notification_ids, **kwargs):
notifications_qs = notifications_qs.exclude(id__in=failed_notifications)
if not rendered_notifications:
+ email_task.status = 'SUCCESS'
if email_task.error_message:
logger.error(f'Partial success for send_user_email_task for user {user_id}. Task id: {self.request.id}. Errors: {email_task.error_message}')
- email_task.status = 'SUCCESS'
+ email_task.status = 'PARTIAL_SUCCESS'
email_task.save()
return
event_context = {
'notifications': rendered_notifications,
- 'user_fullname': user.fullname,
- 'can_change_preferences': False
}
- NotificationType.Type.USER_DIGEST.instance.emit(
+ NotificationTypeEnum.USER_DIGEST.instance.emit(
user=user,
event_context=event_context,
save=False
@@ -123,10 +119,10 @@ def send_user_email_task(self, user_id, notification_ids, **kwargs):
notifications_qs.update(sent=timezone.now())
email_task.status = 'SUCCESS'
- email_task.save()
-
if email_task.error_message:
logger.error(f'Partial success for send_user_email_task for user {user_id}. Task id: {self.request.id}. Errors: {email_task.error_message}')
+ email_task.status = 'PARTIAL_SUCCESS'
+ email_task.save()
except Exception as e:
retry_count = self.request.retries
@@ -177,32 +173,32 @@ def send_moderator_email_task(self, user_id, notification_ids, provider_content_
notifications_qs = notifications_qs.exclude(id__in=failed_notifications)
if not rendered_notifications:
+ email_task.status = 'SUCCESS'
if email_task.error_message:
logger.error(f'Partial success for send_moderator_email_task for user {user_id}. Task id: {self.request.id}. Errors: {email_task.error_message}')
- email_task.status = 'SUCCESS'
+ email_task.status = 'PARTIAL_SUCCESS'
email_task.save()
return
- ProviderModel = ContentType.objects.get_for_id(provider_content_type_id).model_class()
try:
- provider = ProviderModel.objects.get(id=provider_id)
+ provider = AbstractProvider.objects.get(id=provider_id)
except AbstractProvider.DoesNotExist:
- log_message(f'Provider with id {provider_id} does not exist for model {ProviderModel.name}')
+ log_message(f'Provider with id {provider_id} does not exist for model {provider.type}')
email_task.status = 'FAILURE'
- email_task.error_message = f'Provider with id {provider_id} does not exist for model {ProviderModel.name}'
+ email_task.error_message = f'Provider with id {provider_id} does not exist for model {provider.type}'
email_task.save()
return
except AttributeError as err:
- log_message(f'Error retrieving provider with id {provider_id} for model {ProviderModel.name}: {err}')
+ log_message(f'Error retrieving provider with id {provider_id} for model {provider.type}: {err}')
email_task.status = 'FAILURE'
- email_task.error_message = f'Error retrieving provider with id {provider_id} for model {ProviderModel.name}: {err}'
+ email_task.error_message = f'Error retrieving provider with id {provider_id} for model {provider.type}: {err}'
email_task.save()
return
if provider is None:
- log_message(f'Provider with id {provider_id} does not exist for model {ProviderModel.name}')
+ log_message(f'Provider with id {provider_id} does not exist for model {provider.type}')
email_task.status = 'FAILURE'
- email_task.error_message = f'Provider with id {provider_id} does not exist for model {ProviderModel.name}'
+ email_task.error_message = f'Provider with id {provider_id} does not exist for model {provider.type}'
email_task.save()
return
@@ -211,10 +207,10 @@ def send_moderator_email_task(self, user_id, notification_ids, provider_content_
current_admins = provider.get_group('admin')
if current_admins is None or not current_admins.user_set.filter(id=user.id).exists():
log_message(f"User is not a moderator for provider {provider._id} - notifications will be marked as sent.")
- email_task.status = 'FAILURE'
+ email_task.status = 'AUTO_FIXED'
email_task.error_message = f'User is not a moderator for provider {provider._id}'
email_task.save()
- notifications_qs.update(sent=datetime(1000, 1, 1))
+ notifications_qs.update(sent=timezone.now(), fake_sent=True)
return
additional_context = {}
@@ -254,7 +250,6 @@ def send_moderator_email_task(self, user_id, notification_ids, provider_content_
event_context = {
'notifications': rendered_notifications,
'user_fullname': user.fullname,
- 'can_change_preferences': False,
'notification_settings_url': notification_settings_url,
'reviews_withdrawal_url': withdrawals_url,
'reviews_submissions_url': submissions_url,
@@ -264,7 +259,7 @@ def send_moderator_email_task(self, user_id, notification_ids, provider_content_
**additional_context,
}
- NotificationType.Type.DIGEST_REVIEWS_MODERATORS.instance.emit(
+ NotificationTypeEnum.DIGEST_REVIEWS_MODERATORS.instance.emit(
user=user,
subscribed_object=user,
event_context=event_context,
@@ -274,10 +269,10 @@ def send_moderator_email_task(self, user_id, notification_ids, provider_content_
notifications_qs.update(sent=timezone.now())
email_task.status = 'SUCCESS'
- email_task.save()
-
if email_task.error_message:
logger.error(f'Partial success for send_moderator_email_task for user {user_id}. Task id: {self.request.id}. Errors: {email_task.error_message}')
+ email_task.status = 'PARTIAL_SUCCESS'
+ email_task.save()
except Exception as e:
retry_count = self.request.retries
@@ -369,11 +364,11 @@ def get_moderators_emails(message_freq: str):
cursor.execute(sql,
[
message_freq,
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
- NotificationType.Type.DIGEST_REVIEWS_MODERATORS.value,
- NotificationType.Type.USER_DIGEST.value,
- NotificationType.Type.USER_NO_ADDON.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.DIGEST_REVIEWS_MODERATORS.value,
+ NotificationTypeEnum.USER_DIGEST.value,
+ NotificationTypeEnum.USER_NO_ADDON.value,
]
)
return itertools.chain.from_iterable(cursor.fetchall())
@@ -411,11 +406,11 @@ def get_users_emails(message_freq):
cursor.execute(sql,
[
message_freq,
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
- NotificationType.Type.DIGEST_REVIEWS_MODERATORS.value,
- NotificationType.Type.USER_DIGEST.value,
- NotificationType.Type.USER_NO_ADDON.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.DIGEST_REVIEWS_MODERATORS.value,
+ NotificationTypeEnum.USER_DIGEST.value,
+ NotificationTypeEnum.USER_NO_ADDON.value,
]
)
return itertools.chain.from_iterable(cursor.fetchall())
@@ -476,7 +471,7 @@ def send_no_addon_email(self, dry_run=False, **kwargs):
"""
notification_qs = Notification.objects.filter(
sent__isnull=True,
- subscription__notification_type__name=NotificationType.Type.USER_NO_ADDON.value,
+ subscription__notification_type__name=NotificationTypeEnum.USER_NO_ADDON.value,
created__lte=timezone.now() - settings.NO_ADDON_WAIT_TIME
)
for notification in notification_qs:
@@ -490,3 +485,22 @@ def send_no_addon_email(self, dry_run=False, **kwargs):
pass
else:
notification.mark_sent()
+
+
+@celery_app.task(bind=True, name='notifications.tasks.notifications_cleanup_task')
+def notifications_cleanup_task(self, dry_run=False, **kwargs):
+ """Remove old notifications and email tasks from the database."""
+
+ cutoff_date = timezone.now() - settings.NOTIFICATIONS_CLEANUP_AGE
+ old_notifications = Notification.objects.filter(sent__lt=cutoff_date)
+ old_email_tasks = EmailTask.objects.filter(created_at__lt=cutoff_date)
+
+ if dry_run:
+ notifications_count = old_notifications.count()
+ email_tasks_count = old_email_tasks.count()
+ logger.info(f'[Dry Run] Notifications Cleanup Task: {notifications_count} notifications and {email_tasks_count} email tasks would be deleted.')
+ return
+
+ deleted_notifications_count, _ = old_notifications.delete()
+ deleted_email_tasks_count, _ = old_email_tasks.delete()
+ logger.info(f'Notifications Cleanup Task: Deleted {deleted_notifications_count} notifications and {deleted_email_tasks_count} email tasks older than {cutoff_date}.')
diff --git a/osf/admin.py b/osf/admin.py
index 2f5698b2aca..d9fed50b7ff 100644
--- a/osf/admin.py
+++ b/osf/admin.py
@@ -367,7 +367,7 @@ class EmailTaskAdmin(admin.ModelAdmin):
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
- list_display = ('user', 'notification_type_name', 'sent', 'seen')
+ list_display = ('user', 'notification_type_name', 'sent', 'fake_sent')
list_filter = ('sent',)
search_fields = ('subscription__notification_type__name', 'subscription__user__username')
list_per_page = 50
diff --git a/osf/management/commands/check_crossref_dois.py b/osf/management/commands/check_crossref_dois.py
index 348a92d429a..3d7edbe2753 100644
--- a/osf/management/commands/check_crossref_dois.py
+++ b/osf/management/commands/check_crossref_dois.py
@@ -9,7 +9,7 @@
from framework import sentry
from framework.celery_tasks import app as celery_app
-from osf.models import Guid, Preprint, NotificationType
+from osf.models import Guid, Preprint, NotificationTypeEnum
from website import settings
@@ -123,12 +123,12 @@ def report_stuck_dois(dry_run=True):
if preprints_with_pending_dois:
guids = ', '.join(preprints_with_pending_dois.values_list('guids___id', flat=True))
if not dry_run:
- NotificationType.Type.USER_CROSSREF_DOI_PENDING.instance.emit(
+ NotificationTypeEnum.USER_CROSSREF_DOI_PENDING.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
- 'pending_doi_count': preprints_with_pending_dois.count(),
'time_since_published': time_since_published.days,
'guids': guids,
+ 'pending_doi_count': preprints_with_pending_dois.count(),
}
)
else:
diff --git a/osf/management/commands/deactivate_requested_accounts.py b/osf/management/commands/deactivate_requested_accounts.py
index 8a4eeaf9ad1..6f85296dec2 100644
--- a/osf/management/commands/deactivate_requested_accounts.py
+++ b/osf/management/commands/deactivate_requested_accounts.py
@@ -5,7 +5,7 @@
from framework.celery_tasks import app as celery_app
from website.app import setup_django
setup_django()
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationTypeEnum
from django.core.management.base import BaseCommand
from website.settings import OSF_SUPPORT_EMAIL
@@ -20,14 +20,13 @@ def deactivate_requested_accounts(dry_run=True):
if user.has_resources:
logger.info(f'OSF support is being emailed about deactivating the account of user {user._id}.')
if not dry_run:
- NotificationType.Type.DESK_REQUEST_DEACTIVATION.instance.emit(
+ NotificationTypeEnum.DESK_REQUEST_DEACTIVATION.instance.emit(
destination_address=OSF_SUPPORT_EMAIL,
user=user,
event_context={
'user__id': user._id,
'user_absolute_url': user.absolute_url,
'user_username': user.username,
- 'can_change_preferences': False,
}
)
else:
@@ -35,12 +34,11 @@ def deactivate_requested_accounts(dry_run=True):
if not dry_run:
user.deactivate_account()
user.is_registered = False
- NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE.instance.emit(
+ NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
'contact_email': OSF_SUPPORT_EMAIL,
- 'can_change_preferences': False,
}
)
diff --git a/osf/management/commands/find_spammy_files.py b/osf/management/commands/find_spammy_files.py
index 8c7202e21cf..5a8f18cd63a 100644
--- a/osf/management/commands/find_spammy_files.py
+++ b/osf/management/commands/find_spammy_files.py
@@ -8,7 +8,7 @@
from addons.osfstorage.models import OsfStorageFile
from framework.celery_tasks import app
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
logger = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ def find_spammy_files(sniff_r=None, n=None, t=None, to_addrs=None):
'attachment_content': output.getvalue(),
}
for addr in to_addrs:
- NotificationType.Type.USER_SPAM_FILES_DETECTED.instance.emit(
+ NotificationTypeEnum.USER_SPAM_FILES_DETECTED.instance.emit(
destination_address=addr,
event_context=event_context,
email_context=email_context,
diff --git a/osf/management/commands/migrate_notifications.py b/osf/management/commands/migrate_notifications.py
index 10fd397c09d..dbd95f77cef 100644
--- a/osf/management/commands/migrate_notifications.py
+++ b/osf/management/commands/migrate_notifications.py
@@ -7,7 +7,7 @@
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction, connection
-from osf.models import NotificationType, NotificationSubscription
+from osf.models import NotificationType, NotificationTypeEnum, NotificationSubscription
from osf.models.notifications import NotificationSubscriptionLegacy
from osf.management.commands.populate_notification_types import populate_notification_types
from tqdm import tqdm
@@ -25,13 +25,13 @@
EVENT_NAME_TO_NOTIFICATION_TYPE = {
# Provider notifications
- 'global_reviews': NotificationType.Type.REVIEWS_SUBMISSION_STATUS,
+ 'global_reviews': NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS,
# Node notifications
- 'file_updated': NotificationType.Type.NODE_FILE_UPDATED,
+ 'file_updated': NotificationTypeEnum.NODE_FILE_UPDATED,
# User notifications
- 'global_file_updated': NotificationType.Type.USER_FILE_UPDATED,
+ 'global_file_updated': NotificationTypeEnum.USER_FILE_UPDATED,
}
@@ -152,7 +152,7 @@ def migrate_legacy_notification_subscriptions(
notif_enum = EVENT_NAME_TO_NOTIFICATION_TYPE.get(event_name)
if subscribed_object == legacy.user and event_name == 'global_file_updated':
- notif_enum = NotificationType.Type.USER_FILE_UPDATED
+ notif_enum = NotificationTypeEnum.USER_FILE_UPDATED
if not notif_enum:
skipped += 1
continue
diff --git a/osf/management/commands/remove_duplicate_notification_subscriptions_v2.py b/osf/management/commands/remove_duplicate_notification_subscriptions_v2.py
index 7e84b52a99b..2c7aec2a2fa 100644
--- a/osf/management/commands/remove_duplicate_notification_subscriptions_v2.py
+++ b/osf/management/commands/remove_duplicate_notification_subscriptions_v2.py
@@ -2,7 +2,7 @@
from django.db import transaction
from django.db.models import OuterRef, Exists, Q
-from osf.models import NotificationSubscription, NotificationType
+from osf.models import NotificationSubscription, NotificationType, NotificationTypeEnum
class Command(BaseCommand):
@@ -23,22 +23,22 @@ def handle(self, *args, **options):
self.stdout.write('Finding duplicate NotificationSubscription records…')
digest_type_names = {
# User types
- NotificationType.Type.USER_NO_ADDON.value,
+ NotificationTypeEnum.USER_NO_ADDON.value,
# File types
- NotificationType.Type.ADDON_FILE_COPIED.value,
- NotificationType.Type.ADDON_FILE_MOVED.value,
- NotificationType.Type.ADDON_FILE_RENAMED.value,
- NotificationType.Type.FILE_ADDED.value,
- NotificationType.Type.FILE_REMOVED.value,
- NotificationType.Type.FILE_UPDATED.value,
- NotificationType.Type.FOLDER_CREATED.value,
- NotificationType.Type.NODE_FILE_UPDATED.value,
- NotificationType.Type.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
# Review types
- NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
- NotificationType.Type.REVIEWS_SUBMISSION_STATUS.value,
+ NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
}
digest_type_ids = NotificationType.objects.filter(
diff --git a/osf/management/commands/send_storage_exceeded_announcement.py b/osf/management/commands/send_storage_exceeded_announcement.py
index 8c4a687f3ce..cc84414ed4d 100644
--- a/osf/management/commands/send_storage_exceeded_announcement.py
+++ b/osf/management/commands/send_storage_exceeded_announcement.py
@@ -4,7 +4,7 @@
from django.core.management.base import BaseCommand
-from osf.models import Node, OSFUser, NotificationType
+from osf.models import Node, OSFUser, NotificationType, NotificationTypeEnum
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@@ -40,13 +40,12 @@ def main(json_file, dry=False):
if not dry:
try:
NotificationType.objects.get(
- name=NotificationType.Type.USER_STORAGE_CAP_EXCEEDED_ANNOUNCEMENT
+ name=NotificationTypeEnum.USER_STORAGE_CAP_EXCEEDED_ANNOUNCEMENT
).emit(
user=user,
event_context={
'public_nodes': public_nodes,
'private_nodes': private_nodes,
- 'can_change_preferences': False,
}
)
except Exception:
diff --git a/osf/migrations/0036_remove_duplicate_notification_subscriptions_v2.py b/osf/migrations/0036_remove_duplicate_notification_subscriptions_v2.py
new file mode 100644
index 00000000000..d4f308fedd8
--- /dev/null
+++ b/osf/migrations/0036_remove_duplicate_notification_subscriptions_v2.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.17 on 2026-01-27 21:03
+
+from django.db import migrations
+
+from django.core.management import call_command
+
+
+def run_deduplication_command(apps, schema_editor):
+ call_command('remove_duplicate_notification_subscriptions_v2')
+
+
+def reverse(apps, schema_editor):
+ """
+ This is a no-op since we can't restore deleted records.
+ """
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('osf', '0035_merge_20251215_1451'),
+ ]
+
+ operations = [
+ migrations.RunPython(run_deduplication_command, reverse),
+ ]
diff --git a/osf/migrations/0037_notification_refactor_post_release.py b/osf/migrations/0037_notification_refactor_post_release.py
new file mode 100644
index 00000000000..67ffafb600c
--- /dev/null
+++ b/osf/migrations/0037_notification_refactor_post_release.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.17 on 2026-03-03 11:29
+
+from django.db import migrations, models
+import osf.utils.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('osf', '0036_remove_duplicate_notification_subscriptions_v2'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='notification',
+ name='seen',
+ ),
+ migrations.AddField(
+ model_name='notification',
+ name='fake_sent',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='osfuser',
+ name='no_login_email_last_sent',
+ field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='emailtask',
+ name='status',
+ field=models.CharField(choices=[('PENDING', 'Pending'), ('NO_USER_FOUND', 'No User Found'), ('USER_DISABLED', 'User Disabled'), ('STARTED', 'Started'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('RETRY', 'Retry'), ('PARTIAL_SUCCESS', 'Partial Success'), ('AUTO_FIXED', 'Auto Fixed')], default='PENDING', max_length=20),
+ ),
+ migrations.AlterUniqueTogether(
+ name='notificationsubscription',
+ unique_together={('notification_type', 'user', 'content_type', 'object_id', '_is_digest')},
+ ),
+ ]
diff --git a/osf/models/__init__.py b/osf/models/__init__.py
index ccf0544f777..7f334a357cc 100644
--- a/osf/models/__init__.py
+++ b/osf/models/__init__.py
@@ -65,7 +65,7 @@
from .notable_domain import NotableDomain, DomainReference
from .notifications import NotificationSubscriptionLegacy
from .notification_subscription import NotificationSubscription
-from .notification_type import NotificationType
+from .notification_type import NotificationType, NotificationTypeEnum
from .notification import Notification
from .oauth import (
diff --git a/osf/models/collection_submission.py b/osf/models/collection_submission.py
index 1e646745dc4..f2de5ba6610 100644
--- a/osf/models/collection_submission.py
+++ b/osf/models/collection_submission.py
@@ -16,7 +16,7 @@
from website import settings
from osf.utils.machines import CollectionSubmissionMachine
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from django.db.models.signals import post_save
from django.dispatch import receiver
@@ -103,7 +103,7 @@ def _notify_contributors_pending(self, event_data):
assert str(e) == f'No unclaimed record for user {contributor._id} on node {self.guid.referent._id}'
claim_url = None
- NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED.instance.emit(
is_digest=True,
user=contributor,
subscribed_object=self,
@@ -135,22 +135,23 @@ def _notify_contributors_pending(self, event_data):
def _notify_moderators_pending(self, event_data):
user = event_data.kwargs.get('user', None)
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
+ logo_context = {}
+ if self.collection.provider and not self.collection.provider.is_default:
+ logo_context['logo_url'] = self.collection.provider.get_asset_url('favicon')
+ else:
+ logo_context['logo'] = settings.OSF_PREPRINTS_LOGO
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
user=user,
subscribed_object=self.collection.provider,
event_context={
- 'provider_id': self.collection.provider.id,
- 'submitter_fullname': self.creator.fullname,
'requester_fullname': event_data.kwargs.get('user').fullname,
'requester_contributor_names': ''.join(self.guid.referent.contributors.values_list('fullname', flat=True)),
'localized_timestamp': str(timezone.now()),
'message': f'submitted "{self.guid.referent.title}".',
'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}',
'is_request_email': False,
- 'is_initiator': self.creator == user,
'profile_image_url': user.profile_image_url(),
- 'logo': self.collection.provider._id if
- self.collection.provider and not self.collection.provider.is_default else settings.OSF_PREPRINTS_LOGO,
+ **logo_context,
},
is_digest=True,
)
@@ -166,8 +167,13 @@ def _validate_accept(self, event_data):
def _notify_accepted(self, event_data):
if self.collection.provider:
+ logo_context = {}
+ if not self.collection.provider.is_default:
+ logo_context['logo_url'] = self.collection.provider.get_asset_url('favicon')
+ else:
+ logo_context['logo'] = settings.OSF_PREPRINTS_LOGO
for contributor in self.guid.referent.contributors:
- NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED.instance.emit(
user=contributor,
subscribed_object=self,
event_context={
@@ -191,8 +197,7 @@ def _notify_accepted(self, event_data):
'domain': settings.DOMAIN,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
'is_initiator': self.creator == contributor,
- 'logo': self.collection.provider._id if
- self.collection.provider and not self.collection.provider.is_default else settings.OSF_PREPRINTS_LOGO,
+ **logo_context,
},
)
@@ -210,8 +215,13 @@ def _validate_reject(self, event_data):
raise PermissionsError(f'{user} must have moderator permissions.')
def _notify_moderated_rejected(self, event_data):
+ logo_context = {}
+ if self.collection.provider and not self.collection.provider.is_default:
+ logo_context['logo_url'] = self.collection.provider.get_asset_url('favicon')
+ else:
+ logo_context['logo'] = settings.OSF_PREPRINTS_LOGO
for contributor in self.guid.referent.contributors:
- NotificationType.Type.COLLECTION_SUBMISSION_REJECTED.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_REJECTED.instance.emit(
user=contributor,
subscribed_object=self,
event_context={
@@ -224,7 +234,7 @@ def _notify_moderated_rejected(self, event_data):
'collection_title': self.collection.title,
'collection_provider_name': self.collection.provider.name,
'collections_link': DOMAIN + 'collections/' + self.collection.provider._id,
- 'node_absolute_url': self.guid.referent.get_absolute_url(),
+ 'node_absolute_url': self.guid.referent.absolute_url,
'profile_image_url': contributor.profile_image_url(),
'message': f'submission of "{self.collection.title} was rejected',
'node_title': self.guid.referent.title,
@@ -232,8 +242,7 @@ def _notify_moderated_rejected(self, event_data):
'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}',
'rejection_justification': event_data.kwargs.get('comment'),
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
- 'logo': self.collection.provider._id if
- self.collection.provider and not self.collection.provider.is_default else settings.OSF_PREPRINTS_LOGO,
+ **logo_context,
},
)
@@ -261,10 +270,15 @@ def _notify_removed(self, event_data):
is_moderator = user.has_perm('withdraw_submissions', self.collection.provider)
is_admin = self.guid.referent.has_permission(user, ADMIN)
node = self.guid.referent
+ logo_context = {}
+ if self.collection.provider and not self.collection.provider.is_default:
+ logo_context['logo_url'] = self.collection.provider.get_asset_url('favicon')
+ else:
+ logo_context['logo'] = settings.OSF_PREPRINTS_LOGO
event_context_base = {
'remover_fullname': user.fullname,
- 'remover_absolute_url': user.get_absolute_url(),
+ 'remover_absolute_url': user.absolute_url if user else None,
'requester_fullname': user.fullname,
'collections_link': DOMAIN + 'collections/' + self.collection.provider._id if self.collection.provider else None,
'collection_id': self.collection.id,
@@ -277,14 +291,13 @@ def _notify_removed(self, event_data):
'profile_image_url': user.profile_image_url(),
'domain': settings.DOMAIN,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
- 'logo': self.collection.provider._id if
- self.collection.provider and not self.collection.provider.is_default else settings.OSF_PREPRINTS_LOGO,
+ **logo_context,
}
if removed_due_to_privacy and self.collection.provider:
if self.is_moderated:
for moderator in self.collection.moderators:
- NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_PRIVATE.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_PRIVATE.instance.emit(
user=moderator,
event_context={
**event_context_base,
@@ -293,7 +306,7 @@ def _notify_removed(self, event_data):
},
)
for contributor in node.contributors.all():
- NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_PRIVATE.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_PRIVATE.instance.emit(
user=contributor,
event_context={
**event_context_base,
@@ -303,50 +316,34 @@ def _notify_removed(self, event_data):
)
elif is_moderator and self.collection.provider:
for contributor in node.contributors.all():
- NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_MODERATOR.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_MODERATOR.instance.emit(
user=contributor,
event_context={
**event_context_base,
'requester_contributor_names': ''.join(
self.guid.referent.contributors.values_list('fullname', flat=True)),
-
'is_admin': node.has_permission(contributor, ADMIN),
'rejection_justification': event_data.kwargs.get('comment'),
- 'collections_title': self.collection.title,
'collection_provider_name': self.collection.provider.name,
'collection_provider__id': self.collection.provider._id,
- 'remover_absolute_url': user.get_absolute_url() if user is not None else None,
'node_absolute_url': node.absolute_url,
'collection_provider': self.collection.provider.name,
'collections_link': DOMAIN + 'collections/' + self.collection.provider._id,
'user_fullname': contributor.fullname,
- 'is_request_email': False,
- 'message': '',
- 'localized_timestamp': str(timezone.now()),
- 'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}',
},
)
elif is_admin and self.collection.provider:
for contributor in node.contributors.all():
- NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN.instance.emit(
user=contributor,
event_context={
**event_context_base,
- 'requester_contributor_names': ''.join(
- self.guid.referent.contributors.values_list('fullname', flat=True)),
- 'localized_timestamp': str(timezone.now()),
'user_fullname': contributor.fullname,
- 'collections_title': self.collection.title,
'collection_provider_name': self.collection.provider.name,
- 'collection_provider__id': self.collection.provider._id,
'collection_provider': self.collection.provider.name,
'collections_link': DOMAIN + 'collections/' + self.collection.provider._id,
- 'node_absolute_url': node.get_absolute_url(),
- 'is_request_email': False,
- 'message': '',
+ 'node_absolute_url': node.absolute_url,
'is_admin': node.has_permission(contributor, ADMIN),
- 'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}',
-
},
)
@@ -388,33 +385,21 @@ def _notify_cancel(self, event_data):
collection_provider_name = self.collection.title
for contributor in node.contributors.all():
- NotificationType.Type.COLLECTION_SUBMISSION_CANCEL.instance.emit(
+ NotificationTypeEnum.COLLECTION_SUBMISSION_CANCEL.instance.emit(
user=contributor,
subscribed_object=self.collection,
event_context={
- 'requester_contributor_names': ''.join(
- node.contributors.values_list('fullname', flat=True)),
- 'profile_image_url': user.profile_image_url(),
'user_fullname': contributor.fullname,
- 'requester_fullname': self.creator.fullname,
'is_admin': node.has_permission(contributor, ADMIN),
'node_title': node.title,
- 'node_absolute_url': node.get_absolute_url(),
+ 'node_absolute_url': node.absolute_url,
'remover_fullname': user.fullname if user else '',
- 'remover_absolute_url': user.get_absolute_url() if user else '',
- 'localized_timestamp': str(timezone.now()),
+ 'remover_absolute_url': user.absolute_url if user else '',
'collections_link': collections_link,
- 'collection_title': self.collection.title,
'collection_provider_name': collection_provider_name,
- 'node_absolute_url"': node.get_absolute_url(),
'collection_provider': collection_provider_name,
'domain': settings.DOMAIN,
- 'is_request_email': False,
- 'message': '',
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
- 'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}',
- 'logo': self.collection.provider._id if
- self.collection.provider and not self.collection.provider.is_default else settings.OSF_PREPRINTS_LOGO,
},
)
diff --git a/osf/models/email_task.py b/osf/models/email_task.py
index f89f2285e5c..12def4c8c12 100644
--- a/osf/models/email_task.py
+++ b/osf/models/email_task.py
@@ -9,6 +9,8 @@ class EmailTask(models.Model):
('SUCCESS', 'Success'),
('FAILURE', 'Failure'),
('RETRY', 'Retry'),
+ ('PARTIAL_SUCCESS', 'Partial Success'),
+ ('AUTO_FIXED', 'Auto Fixed'),
)
task_id = models.CharField(max_length=255, unique=True)
diff --git a/osf/models/institution.py b/osf/models/institution.py
index 39d57637da5..3671e7bef1f 100644
--- a/osf/models/institution.py
+++ b/osf/models/institution.py
@@ -14,7 +14,7 @@
from django.utils import timezone
from framework import sentry
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .base import BaseModel, ObjectIDMixin
from .contributor import InstitutionalContributor
from .institution_affiliation import InstitutionAffiliation
@@ -220,7 +220,7 @@ def _send_deactivation_email(self):
success = 0
for user in self.get_institution_users():
attempts += 1
- NotificationType.Type.USER_INSTITUTION_DEACTIVATION.instance.emit(
+ NotificationTypeEnum.USER_INSTITUTION_DEACTIVATION.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
diff --git a/osf/models/mixins.py b/osf/models/mixins.py
index 2604b7d32cb..c115a23610c 100644
--- a/osf/models/mixins.py
+++ b/osf/models/mixins.py
@@ -27,7 +27,7 @@
InvalidTagError,
BlockedEmailError,
)
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf.models.notification_subscription import NotificationSubscription
from .node_relation import NodeRelation
from .nodelog import NodeLog
@@ -312,7 +312,7 @@ def add_affiliated_institution(self, inst, user, log=True, ignore_user_affiliati
if notify and getattr(self, 'type', False) == 'osf.node':
for user, _ in self.get_admin_contributors_recursive(unique_users=True):
- NotificationType.Type.NODE_AFFILIATION_CHANGED.instance.emit(
+ NotificationTypeEnum.NODE_AFFILIATION_CHANGED.instance.emit(
user=user,
subscribed_object=self,
event_context={
@@ -354,7 +354,7 @@ def remove_affiliated_institution(self, inst, user, save=False, log=True, notify
self.update_search()
if notify and getattr(self, 'type', False) == 'osf.node':
for user, _ in self.get_admin_contributors_recursive(unique_users=True):
- NotificationType.Type.NODE_AFFILIATION_CHANGED.instance.emit(
+ NotificationTypeEnum.NODE_AFFILIATION_CHANGED.instance.emit(
user=user,
subscribed_object=self,
event_context={
@@ -1035,7 +1035,7 @@ class Meta:
reviews_comments_anonymous = models.BooleanField(null=True, blank=True)
DEFAULT_SUBSCRIPTIONS = [
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
]
@property
@@ -1084,7 +1084,7 @@ def add_to_group(self, user, group):
for subscription in self.DEFAULT_SUBSCRIPTIONS:
NotificationSubscription.objects.get_or_create(
user=user,
- content_type=ContentType.objects.get_for_model(self, for_concrete_model=False),
+ content_type=ContentType.objects.get_for_model(self),
object_id=self.id,
notification_type=subscription.instance,
message_frequency='instantly',
@@ -1102,7 +1102,7 @@ def remove_from_group(self, user, group, unsubscribe=True):
NotificationSubscription.objects.filter(
notification_type=subscription.instance,
user=user,
- content_type=ContentType.objects.get_for_model(self, for_concrete_model=False),
+ content_type=ContentType.objects.get_for_model(self),
object_id=self.id,
).delete()
@@ -1422,14 +1422,14 @@ def add_contributor(
from osf.models import AbstractNode, Preprint, DraftRegistration
if isinstance(self, AbstractNode):
- notification_type = NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type = NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
elif isinstance(self, Preprint):
if self.is_published:
- notification_type = NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type = NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_DEFAULT
else:
notification_type = False
elif isinstance(self, DraftRegistration):
- notification_type = NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type = NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
if contrib_to_add.is_disabled:
raise ValidationValueError('Deactivated users cannot be added as contributors.')
@@ -1597,14 +1597,14 @@ def add_unregistered_contributor(
from osf.models import AbstractNode, Preprint, DraftRegistration
if isinstance(self, AbstractNode):
- notification_type = NotificationType.Type.USER_INVITE_DEFAULT
+ notification_type = NotificationTypeEnum.USER_INVITE_DEFAULT
elif isinstance(self, Preprint):
if self.provider.is_default:
- notification_type = NotificationType.Type.USER_INVITE_OSF_PREPRINT
+ notification_type = NotificationTypeEnum.USER_INVITE_OSF_PREPRINT
else:
- notification_type = NotificationType.Type.PROVIDER_USER_INVITE_PREPRINT
+ notification_type = NotificationTypeEnum.PROVIDER_USER_INVITE_PREPRINT
elif isinstance(self, DraftRegistration):
- notification_type = NotificationType.Type.USER_INVITE_DRAFT_REGISTRATION
+ notification_type = NotificationTypeEnum.USER_INVITE_DRAFT_REGISTRATION
self.add_contributor(
contributor,
@@ -2322,12 +2322,11 @@ def suspend_spam_user(self, user):
user.flag_spam()
if not user.is_disabled:
user.deactivate_account()
- NotificationType.Type.USER_SPAM_BANNED.instance.emit(
+ NotificationTypeEnum.USER_SPAM_BANNED.instance.emit(
user,
event_context={
'user_fullname': user.fullname,
'osf_support_email': settings.OSF_SUPPORT_EMAIL,
- 'can_change_preferences': False
}
)
user.save()
diff --git a/osf/models/node.py b/osf/models/node.py
index 380bc6cda0e..fa08e77eee8 100644
--- a/osf/models/node.py
+++ b/osf/models/node.py
@@ -35,7 +35,7 @@
from framework.exceptions import PermissionsError, HTTPError
from framework.sentry import log_exception
from osf.exceptions import InvalidTagError, NodeStateError, TagNotFoundError, ValidationError
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .contributor import Contributor
from .collection_submission import CollectionSubmission
@@ -1297,7 +1297,7 @@ def set_privacy(self, permissions, auth=None, log=True, save=True, meeting_creat
self.save()
if auth and permissions == 'public':
for contributor in self.contributors:
- NotificationType.Type.NODE_NEW_PUBLIC_PROJECT.instance.emit(
+ NotificationTypeEnum.NODE_NEW_PUBLIC_PROJECT.instance.emit(
user=contributor,
subscribed_object=self,
event_context={
@@ -1611,7 +1611,7 @@ def fork_node(
:return: Forked node
"""
if notification_type is None:
- notification_type = NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type = NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
Registration = apps.get_model('osf.Registration')
PREFIX = 'Fork of '
user = auth.user
@@ -1839,7 +1839,7 @@ def use_as_template(self, auth, changes=None, top_level=True, parent=None):
new,
contributor=auth.user,
auth=auth,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
)
# Log the creation
diff --git a/osf/models/notification.py b/osf/models/notification.py
index aab9f2b0f0e..533a05a4e97 100644
--- a/osf/models/notification.py
+++ b/osf/models/notification.py
@@ -16,8 +16,8 @@ class Notification(models.Model):
)
event_context: dict = models.JSONField()
sent = models.DateTimeField(null=True, blank=True)
- seen = models.DateTimeField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
+ fake_sent = models.BooleanField(default=False)
def send(
self,
@@ -56,14 +56,13 @@ def send(
if save:
self.mark_sent()
- def mark_sent(self) -> None:
+ def mark_sent(self, fake_sent=False) -> None:
+ update_fields = ['sent']
self.sent = timezone.now()
- self.save(update_fields=['sent'])
-
- def mark_seen(self) -> None:
- raise NotImplementedError('mark_seen must be implemented by subclasses.')
- # self.seen = timezone.now()
- # self.save(update_fields=['seen'])
+ if fake_sent:
+ update_fields.append('fake_sent')
+ self.fake_sent = True
+ self.save(update_fields=update_fields)
def render(self) -> str:
"""Render the notification message using the event context."""
diff --git a/osf/models/notification_subscription.py b/osf/models/notification_subscription.py
index 6a4a27533b5..1a812639ee6 100644
--- a/osf/models/notification_subscription.py
+++ b/osf/models/notification_subscription.py
@@ -1,10 +1,10 @@
import logging
-from datetime import datetime
+from django.utils import timezone
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
-from osf.models.notification_type import get_default_frequency_choices, NotificationType
+from osf.models.notification_type import get_default_frequency_choices, NotificationTypeEnum
from osf.models.notification import Notification
from api.base import settings
from api.base.utils import absolute_reverse
@@ -59,6 +59,7 @@ class Meta:
verbose_name = 'Notification Subscription'
verbose_name_plural = 'Notification Subscriptions'
db_table = 'osf_notificationsubscription_v2'
+ unique_together = ('notification_type', 'user', 'content_type', 'object_id', '_is_digest')
def emit(
self,
@@ -126,7 +127,8 @@ def emit(
Notification.objects.create(
subscription=self,
event_context=event_context,
- sent=None if self.message_frequency != 'none' else datetime(1000, 1, 1),
+ sent=timezone.now() if self.message_frequency == 'none' else None,
+ fake_sent=True if self.message_frequency == 'none' else False,
)
@property
@@ -144,32 +146,32 @@ def _id(self):
Legacy subscription id for API compatibility.
"""
_global_file_updated = [
- NotificationType.Type.USER_FILE_UPDATED.value,
- NotificationType.Type.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,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.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,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.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,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
]
if self.notification_type.name in _global_file_updated:
return f'{self.user._id}_file_updated'
diff --git a/osf/models/notification_type.py b/osf/models/notification_type.py
index f8900ad4ec5..538dfa969df 100644
--- a/osf/models/notification_type.py
+++ b/osf/models/notification_type.py
@@ -12,166 +12,165 @@ def get_default_frequency_choices():
DEFAULT_FREQUENCY_CHOICES = ['none', 'instantly', 'daily', 'weekly', 'monthly']
return DEFAULT_FREQUENCY_CHOICES.copy()
+class NotificationTypeEnum(str, Enum):
+ EMPTY = 'empty'
+ # Desk notifications
+ REVIEWS_SUBMISSION_STATUS = 'reviews_submission_status'
+ ADDONS_BOA_JOB_FAILURE = 'addon_boa_job_failure'
+ ADDONS_BOA_JOB_COMPLETE = 'addon_boa_job_complete'
+
+ DESK_ARCHIVE_REGISTRATION_STUCK = 'desk_archive_registration_stuck'
+ DESK_REQUEST_EXPORT = 'desk_request_export'
+ DESK_REQUEST_DEACTIVATION = 'desk_request_deactivation'
+ DESK_REGISTRATION_BULK_UPLOAD_PRODUCT_OWNER = 'desk_registration_bulk_upload_product_owner'
+ DESK_USER_REGISTRATION_BULK_UPLOAD_UNEXPECTED_FAILURE = 'desk_user_registration_bulk_upload_unexpected_failure'
+ DESK_ARCHIVE_JOB_EXCEEDED = 'desk_archive_job_exceeded'
+ DESK_ARCHIVE_JOB_COPY_ERROR = 'desk_archive_job_copy_error'
+ DESK_ARCHIVE_JOB_FILE_NOT_FOUND = 'desk_archive_job_file_not_found'
+ DESK_ARCHIVE_JOB_UNCAUGHT_ERROR = 'desk_archive_job_uncaught_error'
+ DESK_CROSSREF_ERROR = 'desk_crossref_error'
+
+ # User notifications
+ USER_PENDING_VERIFICATION = 'user_pending_verification'
+ USER_PENDING_VERIFICATION_REGISTERED = 'user_pending_verification_registered'
+ USER_STORAGE_CAP_EXCEEDED_ANNOUNCEMENT = 'user_storage_cap_exceeded_announcement'
+ USER_SPAM_BANNED = 'user_spam_banned'
+ USER_REQUEST_DEACTIVATION = 'user_request_deactivation' # added as a placeholder
+ USER_REQUEST_DEACTIVATION_COMPLETE = 'user_request_deactivation_complete'
+ USER_PRIMARY_EMAIL_CHANGED = 'user_primary_email_changed'
+ USER_INSTITUTION_DEACTIVATION = 'user_institution_deactivation'
+ USER_FORGOT_PASSWORD = 'user_forgot_password'
+ USER_FORGOT_PASSWORD_INSTITUTION = 'user_forgot_password_institution'
+ USER_DUPLICATE_ACCOUNTS_OSF4I = 'user_duplicate_accounts_osf4i'
+ USER_EXTERNAL_LOGIN_LINK_SUCCESS = 'user_external_login_link_success'
+ USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL = 'user_registration_bulk_upload_failure_all'
+ USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL = 'user_registration_bulk_upload_success_partial'
+ USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL = 'user_registration_bulk_upload_success_all'
+ USER_ADD_SSO_EMAIL_OSF4I = 'user_add_sso_email_osf4i'
+ USER_WELCOME = 'user_welcome' # waiting for the language from the PO
+ USER_WELCOME_OSF4I = 'user_welcome_osf4i'
+ USER_ARCHIVE_JOB_EXCEEDED = 'user_archive_job_exceeded'
+ USER_ARCHIVE_JOB_COPY_ERROR = 'user_archive_job_copy_error'
+ USER_ARCHIVE_JOB_FILE_NOT_FOUND = 'user_archive_job_file_not_found'
+ USER_FILE_UPDATED = 'user_file_updated'
+ USER_FILE_OPERATION_SUCCESS = 'user_file_operation_success'
+ USER_FILE_OPERATION_FAILED = 'user_file_operation_failed'
+ USER_PASSWORD_RESET = 'user_password_reset'
+ USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_CREATE = 'user_external_login_confirm_email_create'
+ USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK = 'user_external_login_email_confirm_link'
+ USER_CONFIRM_MERGE = 'user_confirm_merge'
+ USER_CONFIRM_EMAIL = 'user_confirm_email'
+ USER_INITIAL_CONFIRM_EMAIL = 'user_initial_confirm_email'
+ USER_INVITE_DEFAULT = 'user_invite_default'
+ USER_FORWARD_INVITE = 'user_forward_invite'
+ USER_FORWARD_INVITE_REGISTERED = 'user_forward_invite_registered'
+ USER_INVITE_DRAFT_REGISTRATION = 'user_invite_draft_registration'
+ USER_INVITE_OSF_PREPRINT = 'user_invite_osf_preprint'
+ USER_ARCHIVE_JOB_UNCAUGHT_ERROR = 'user_archive_job_uncaught_error'
+ USER_INSTITUTIONAL_ACCESS_REQUEST = 'user_institutional_access_request'
+ USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE = 'user_campaign_confirm_email_agu_conference'
+ USER_CAMPAIGN_CONFIRM_EMAIL_REGISTRIES_OSF = 'user_campaign_confirm_email_registries_osf'
+ USER_CAMPAIGN_CONFIRM_EMAIL_ERPC = 'user_campaign_confirm_email_erpc'
+ USER_DIGEST = 'user_digest'
+ USER_NO_LOGIN = 'user_no_login'
+ DIGEST_REVIEWS_MODERATORS = 'digest_reviews_moderators'
+ USER_NO_ADDON = 'user_no_addon'
+ USER_SPAM_FILES_DETECTED = 'user_spam_files_detected'
+ USER_CROSSREF_DOI_PENDING = 'user_crossref_doi_pending'
+ USER_TERMS_OF_USE_UPDATED = 'user_terms_of_use_updated' # added as a placeholder
+
+ # Node notifications
+ NODE_FILE_UPDATED = 'node_file_updated'
+ NODE_AFFILIATION_CHANGED = 'node_affiliation_changed'
+ NODE_ARCHIVE_SUCCESS = 'node_archive_success' # added as a placeholder
+ NODE_REQUEST_ACCESS_SUBMITTED = 'node_request_access_submitted'
+ NODE_REQUEST_ACCESS_DENIED = 'node_request_access_denied'
+ NODE_FORK_COMPLETED = 'node_fork_completed'
+ NODE_FORK_FAILED = 'node_fork_failed'
+ NODE_INSTITUTIONAL_ACCESS_REQUEST = 'node_institutional_access_request'
+ NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST = 'node_contributor_added_access_request'
+ NODE_CONTRIBUTOR_ADDED_DEFAULT = 'node_contributor_added_default'
+ NODE_PENDING_EMBARGO_ADMIN = 'node_pending_embargo_admin'
+ NODE_PENDING_EMBARGO_NON_ADMIN = 'node_pending_embargo_non_admin'
+ NODE_PENDING_RETRACTION_NON_ADMIN = 'node_pending_retraction_non_admin'
+ NODE_PENDING_RETRACTION_ADMIN = 'node_pending_retraction_admin'
+ NODE_PENDING_REGISTRATION_NON_ADMIN = 'node_pending_registration_non_admin'
+ NODE_PENDING_REGISTRATION_ADMIN = 'node_pending_registration_admin'
+ NODE_PENDING_EMBARGO_TERMINATION_NON_ADMIN = 'node_pending_embargo_termination_non_admin'
+ NODE_PENDING_EMBARGO_TERMINATION_ADMIN = 'node_pending_embargo_termination_admin'
+ NODE_SCHEMA_RESPONSE_REJECTED = 'node_schema_response_rejected'
+ NODE_SCHEMA_RESPONSE_APPROVED = 'node_schema_response_approved'
+ NODE_SCHEMA_RESPONSE_SUBMITTED = 'node_schema_response_submitted'
+ NODE_SCHEMA_RESPONSE_INITIATED = 'node_schema_response_initiated'
+ NODE_WITHDRAWAl_REQUEST_APPROVED = 'node_withdrawal_request_approved'
+ NODE_WITHDRAWAl_REQUEST_REJECTED = 'node_withdrawal_request_rejected'
+ NODE_NEW_PUBLIC_PROJECT = 'node_new_public_project'
+
+ FILE_UPDATED = 'file_updated'
+ FILE_ADDED = 'file_added'
+ FILE_REMOVED = 'file_removed'
+ ADDON_FILE_COPIED = 'addon_file_copied'
+ ADDON_FILE_RENAMED = 'addon_file_renamed'
+ ADDON_FILE_MOVED = 'addon_file_moved'
+ ADDON_FILE_REMOVED = 'addon_file_removed'
+ FOLDER_CREATED = 'folder_created'
+
+ # Provider notifications
+ PROVIDER_NEW_PENDING_SUBMISSIONS = 'provider_new_pending_submissions'
+ PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS = 'provider_new_pending_withdraw_requests'
+ PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION = 'provider_reviews_submission_confirmation'
+ PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION = 'provider_reviews_resubmission_confirmation'
+ PROVIDER_CONFIRM_EMAIL_MODERATION = 'provider_confirm_email_moderation'
+ PROVIDER_MODERATOR_ADDED = 'provider_moderator_added'
+ PROVIDER_USER_INVITE_PREPRINT = 'provider_user_invite_preprint'
+
+ # Preprint notifications
+ PREPRINT_REQUEST_WITHDRAWAL_APPROVED = 'preprint_request_withdrawal_approved'
+ PREPRINT_REQUEST_WITHDRAWAL_DECLINED = 'preprint_request_withdrawal_declined'
+ PREPRINT_CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF = 'preprint_contributor_added_preprint_node_from_osf'
+ PREPRINT_CONTRIBUTOR_ADDED_DEFAULT = 'preprint_contributor_added_default'
+
+ # Collections Submission notifications
+ COLLECTION_SUBMISSION_REMOVED_ADMIN = 'collection_submission_removed_admin'
+ COLLECTION_SUBMISSION_REMOVED_MODERATOR = 'collection_submission_removed_moderator'
+ COLLECTION_SUBMISSION_REMOVED_PRIVATE = 'collection_submission_removed_private'
+ COLLECTION_SUBMISSION_SUBMITTED = 'collection_submission_submitted'
+ COLLECTION_SUBMISSION_ACCEPTED = 'collection_submission_accepted'
+ COLLECTION_SUBMISSION_REJECTED = 'collection_submission_rejected'
+ COLLECTION_SUBMISSION_CANCEL = 'collection_submission_cancel'
+
+ REGISTRATION_BULK_UPLOAD_FAILURE_DUPLICATES = 'registration_bulk_upload_failure_duplicates'
+
+ DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT = 'draft_registration_contributor_added_default'
+
+ @ttl_cached_property(ttl=settings.TTL_CACHE_LIFETIME)
+ def instance(self):
+ obj, created = NotificationType.objects.get_or_create(name=self.value)
+ return obj
class NotificationType(models.Model):
- class Type(str, Enum):
- EMPTY = 'empty'
- # Desk notifications
- REVIEWS_SUBMISSION_STATUS = 'reviews_submission_status'
- ADDONS_BOA_JOB_FAILURE = 'addon_boa_job_failure'
- ADDONS_BOA_JOB_COMPLETE = 'addon_boa_job_complete'
-
- DESK_ARCHIVE_REGISTRATION_STUCK = 'desk_archive_registration_stuck'
- DESK_REQUEST_EXPORT = 'desk_request_export'
- DESK_REQUEST_DEACTIVATION = 'desk_request_deactivation'
- DESK_REGISTRATION_BULK_UPLOAD_PRODUCT_OWNER = 'desk_registration_bulk_upload_product_owner'
- DESK_USER_REGISTRATION_BULK_UPLOAD_UNEXPECTED_FAILURE = 'desk_user_registration_bulk_upload_unexpected_failure'
- DESK_ARCHIVE_JOB_EXCEEDED = 'desk_archive_job_exceeded'
- DESK_ARCHIVE_JOB_COPY_ERROR = 'desk_archive_job_copy_error'
- DESK_ARCHIVE_JOB_FILE_NOT_FOUND = 'desk_archive_job_file_not_found'
- DESK_ARCHIVE_JOB_UNCAUGHT_ERROR = 'desk_archive_job_uncaught_error'
- DESK_CROSSREF_ERROR = 'desk_crossref_error'
-
- # User notifications
- USER_PENDING_VERIFICATION = 'user_pending_verification'
- USER_PENDING_VERIFICATION_REGISTERED = 'user_pending_verification_registered'
- USER_STORAGE_CAP_EXCEEDED_ANNOUNCEMENT = 'user_storage_cap_exceeded_announcement'
- USER_SPAM_BANNED = 'user_spam_banned'
- USER_REQUEST_DEACTIVATION_COMPLETE = 'user_request_deactivation_complete'
- USER_PRIMARY_EMAIL_CHANGED = 'user_primary_email_changed'
- USER_INSTITUTION_DEACTIVATION = 'user_institution_deactivation'
- USER_FORGOT_PASSWORD = 'user_forgot_password'
- USER_FORGOT_PASSWORD_INSTITUTION = 'user_forgot_password_institution'
- USER_DUPLICATE_ACCOUNTS_OSF4I = 'user_duplicate_accounts_osf4i'
- USER_EXTERNAL_LOGIN_LINK_SUCCESS = 'user_external_login_link_success'
- USER_REGISTRATION_BULK_UPLOAD_FAILURE_ALL = 'user_registration_bulk_upload_failure_all'
- USER_REGISTRATION_BULK_UPLOAD_SUCCESS_PARTIAL = 'user_registration_bulk_upload_success_partial'
- USER_REGISTRATION_BULK_UPLOAD_SUCCESS_ALL = 'user_registration_bulk_upload_success_all'
- USER_ADD_SSO_EMAIL_OSF4I = 'user_add_sso_email_osf4i'
- USER_WELCOME_OSF4I = 'user_welcome_osf4i'
- USER_ARCHIVE_JOB_EXCEEDED = 'user_archive_job_exceeded'
- USER_ARCHIVE_JOB_COPY_ERROR = 'user_archive_job_copy_error'
- USER_ARCHIVE_JOB_FILE_NOT_FOUND = 'user_archive_job_file_not_found'
- USER_FILE_UPDATED = 'user_file_updated'
- USER_FILE_OPERATION_SUCCESS = 'user_file_operation_success'
- USER_FILE_OPERATION_FAILED = 'user_file_operation_failed'
- USER_PASSWORD_RESET = 'user_password_reset'
- USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_CREATE = 'user_external_login_confirm_email_create'
- USER_EXTERNAL_LOGIN_CONFIRM_EMAIL_LINK = 'user_external_login_email_confirm_link'
- USER_CONFIRM_MERGE = 'user_confirm_merge'
- USER_CONFIRM_EMAIL = 'user_confirm_email'
- USER_INITIAL_CONFIRM_EMAIL = 'user_initial_confirm_email'
- USER_INVITE_DEFAULT = 'user_invite_default'
- USER_FORWARD_INVITE = 'user_forward_invite'
- USER_FORWARD_INVITE_REGISTERED = 'user_forward_invite_registered'
- USER_INVITE_DRAFT_REGISTRATION = 'user_invite_draft_registration'
- USER_INVITE_OSF_PREPRINT = 'user_invite_osf_preprint'
- USER_ARCHIVE_JOB_UNCAUGHT_ERROR = 'user_archive_job_uncaught_error'
- USER_INSTITUTIONAL_ACCESS_REQUEST = 'user_institutional_access_request'
- USER_CAMPAIGN_CONFIRM_PREPRINTS_BRANDED = 'user_campaign_confirm_preprint_branded'
- USER_CAMPAIGN_CONFIRM_PREPRINTS_OSF = 'user_campaign_confirm_preprint_osf'
- USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE = 'user_campaign_confirm_email_agu_conference'
- USER_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE_2023 = 'user_campaign_confirm_email_agu_conference_2023'
- USER_CAMPAIGN_CONFIRM_EMAIL_REGISTRIES_OSF = 'user_campaign_confirm_email_registries_osf'
- USER_CAMPAIGN_CONFIRM_EMAIL_ERPC = 'user_campaign_confirm_email_erpc'
- USER_DIGEST = 'user_digest'
- USER_NO_LOGIN = 'user_no_login'
- DIGEST_REVIEWS_MODERATORS = 'digest_reviews_moderators'
- USER_NO_ADDON = 'user_no_addon'
- USER_SPAM_FILES_DETECTED = 'user_spam_files_detected'
- USER_CROSSREF_DOI_PENDING = 'user_crossref_doi_pending'
-
- # Node notifications
- NODE_FILE_UPDATED = 'node_file_updated'
- NODE_FILES_UPDATED = 'node_files_updated'
- NODE_AFFILIATION_CHANGED = 'node_affiliation_changed'
- NODE_REQUEST_ACCESS_SUBMITTED = 'node_request_access_submitted'
- NODE_REQUEST_ACCESS_DENIED = 'node_request_access_denied'
- NODE_FORK_COMPLETED = 'node_fork_completed'
- NODE_FORK_FAILED = 'node_fork_failed'
- NODE_INSTITUTIONAL_ACCESS_REQUEST = 'node_institutional_access_request'
- NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST = 'node_contributor_added_access_request'
- NODE_CONTRIBUTOR_ADDED_DEFAULT = 'node_contributor_added_default'
- NODE_PENDING_EMBARGO_ADMIN = 'node_pending_embargo_admin'
- NODE_PENDING_EMBARGO_NON_ADMIN = 'node_pending_embargo_non_admin'
- NODE_PENDING_RETRACTION_NON_ADMIN = 'node_pending_retraction_non_admin'
- NODE_PENDING_RETRACTION_ADMIN = 'node_pending_retraction_admin'
- NODE_PENDING_REGISTRATION_NON_ADMIN = 'node_pending_registration_non_admin'
- NODE_PENDING_REGISTRATION_ADMIN = 'node_pending_registration_admin'
- NODE_PENDING_EMBARGO_TERMINATION_NON_ADMIN = 'node_pending_embargo_termination_non_admin'
- NODE_PENDING_EMBARGO_TERMINATION_ADMIN = 'node_pending_embargo_termination_admin'
- NODE_SCHEMA_RESPONSE_REJECTED = 'node_schema_response_rejected'
- NODE_SCHEMA_RESPONSE_APPROVED = 'node_schema_response_approved'
- NODE_SCHEMA_RESPONSE_SUBMITTED = 'node_schema_response_submitted'
- NODE_SCHEMA_RESPONSE_INITIATED = 'node_schema_response_initiated'
- NODE_WITHDRAWAl_REQUEST_APPROVED = 'node_withdrawal_request_approved'
- NODE_WITHDRAWAl_REQUEST_REJECTED = 'node_withdrawal_request_rejected'
- NODE_NEW_PUBLIC_PROJECT = 'node_new_public_project'
-
- FILE_UPDATED = 'file_updated'
- FILE_ADDED = 'file_added'
- FILE_REMOVED = 'file_removed'
- ADDON_FILE_COPIED = 'addon_file_copied'
- ADDON_FILE_RENAMED = 'addon_file_renamed'
- ADDON_FILE_MOVED = 'addon_file_moved'
- ADDON_FILE_REMOVED = 'addon_file_removed'
- FOLDER_CREATED = 'folder_created'
-
- # Provider notifications
- PROVIDER_NEW_PENDING_SUBMISSIONS = 'provider_new_pending_submissions'
- PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS = 'provider_new_pending_withdraw_requests'
- PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION = 'provider_reviews_submission_confirmation'
- PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION = 'provider_reviews_resubmission_confirmation'
- PROVIDER_CONFIRM_EMAIL_MODERATION = 'provider_confirm_email_moderation'
- PROVIDER_MODERATOR_ADDED = 'provider_moderator_added'
- PROVIDER_USER_INVITE_PREPRINT = 'provider_user_invite_preprint'
-
- # Preprint notifications
- PREPRINT_REQUEST_WITHDRAWAL_APPROVED = 'preprint_request_withdrawal_approved'
- PREPRINT_REQUEST_WITHDRAWAL_DECLINED = 'preprint_request_withdrawal_declined'
- PREPRINT_CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF = 'preprint_contributor_added_preprint_node_from_osf'
- PREPRINT_CONTRIBUTOR_ADDED_DEFAULT = 'preprint_contributor_added_default'
-
- # Collections Submission notifications
- COLLECTION_SUBMISSION_REMOVED_ADMIN = 'collection_submission_removed_admin'
- COLLECTION_SUBMISSION_REMOVED_MODERATOR = 'collection_submission_removed_moderator'
- COLLECTION_SUBMISSION_REMOVED_PRIVATE = 'collection_submission_removed_private'
- COLLECTION_SUBMISSION_SUBMITTED = 'collection_submission_submitted'
- COLLECTION_SUBMISSION_ACCEPTED = 'collection_submission_accepted'
- COLLECTION_SUBMISSION_REJECTED = 'collection_submission_rejected'
- COLLECTION_SUBMISSION_CANCEL = 'collection_submission_cancel'
-
- REGISTRATION_BULK_UPLOAD_FAILURE_DUPLICATES = 'registration_bulk_upload_failure_duplicates'
-
- DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT = 'draft_registration_contributor_added_default'
-
- @ttl_cached_property(ttl=settings.TTL_CACHE_LIFETIME)
- def instance(self):
- obj, created = NotificationType.objects.get_or_create(name=self.value)
- return obj
-
@property
def is_digest_type(self):
digest_types = {
# User types
- NotificationType.Type.USER_NO_ADDON.value,
+ NotificationTypeEnum.USER_NO_ADDON.value,
# File types
- NotificationType.Type.ADDON_FILE_COPIED.value,
- NotificationType.Type.ADDON_FILE_MOVED.value,
- NotificationType.Type.ADDON_FILE_RENAMED.value,
- NotificationType.Type.FILE_ADDED.value,
- NotificationType.Type.FILE_REMOVED.value,
- NotificationType.Type.FILE_UPDATED.value,
- NotificationType.Type.FOLDER_CREATED.value,
- NotificationType.Type.NODE_FILE_UPDATED.value,
- NotificationType.Type.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
# Review types
- NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
- NotificationType.Type.REVIEWS_SUBMISSION_STATUS.value,
+ NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
}
return self.name in digest_types
@@ -226,7 +225,6 @@ def emit(
used.
"""
from osf.models.notification_subscription import NotificationSubscription
- from osf.models.provider import AbstractProvider
if is_digest != self.is_digest_type:
sentry.log_message(f'NotificationType.emit called with is_digest={is_digest} for '
@@ -234,11 +232,7 @@ def emit(
'is_digest value will be overridden.')
is_digest = self.is_digest_type
- # use concrete model for AbstractProvider to specifically get the provider content type
- if isinstance(subscribed_object, AbstractProvider):
- content_type = ContentType.objects.get_for_model(subscribed_object, for_concrete_model=False) if subscribed_object else None
- else:
- content_type = ContentType.objects.get_for_model(subscribed_object) if subscribed_object else None
+ content_type = ContentType.objects.get_for_model(subscribed_object) if subscribed_object else None
if message_frequency is None:
message_frequency = self.get_group_frequency_or_default(user, subscribed_object, content_type)
@@ -296,33 +290,33 @@ def get_group_frequency_or_default(self, user, subscribed_object, content_type):
from osf.models import NotificationSubscription, AbstractNode
_global_file_updated = [
- NotificationType.Type.USER_FILE_UPDATED.value,
- NotificationType.Type.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,
+ NotificationTypeEnum.USER_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.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,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION.value,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.value,
+ NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.value,
]
_node_file_updated = [
- NotificationType.Type.NODE_FILE_UPDATED.value,
- NotificationType.Type.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,
+ NotificationTypeEnum.NODE_FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_UPDATED.value,
+ NotificationTypeEnum.FILE_ADDED.value,
+ NotificationTypeEnum.FILE_REMOVED.value,
+ NotificationTypeEnum.ADDON_FILE_COPIED.value,
+ NotificationTypeEnum.ADDON_FILE_RENAMED.value,
+ NotificationTypeEnum.ADDON_FILE_MOVED.value,
+ NotificationTypeEnum.ADDON_FILE_REMOVED.value,
+ NotificationTypeEnum.FOLDER_CREATED.value,
]
if self.name in _global_file_updated and content_type != ContentType.objects.get_for_model(AbstractNode):
diff --git a/osf/models/preprint.py b/osf/models/preprint.py
index 9938a0e7147..ccf48331ea9 100644
--- a/osf/models/preprint.py
+++ b/osf/models/preprint.py
@@ -20,7 +20,7 @@
from framework.auth import Auth
from framework.exceptions import PermissionsError, UnpublishedPendingPreprintVersionExists
from framework.auth import oauth_scopes
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .subject import Subject
from .tag import Tag
@@ -1071,28 +1071,22 @@ def _add_creator_as_contributor(self):
def _send_preprint_confirmation(self, auth):
# Send creator confirmation email
recipient = self.creator
- NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.instance.emit(
+ NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION.instance.emit(
subscribed_object=self.provider,
user=recipient,
event_context={
- 'domain': settings.DOMAIN,
'user_fullname': recipient.fullname,
'referrer_fullname': recipient.fullname,
'reviewable_title': self.title,
'no_future_emails': self.provider.allow_submissions,
'reviewable_absolute_url': self.absolute_url,
'reviewable_provider_name': self.provider.name,
- 'reviewable_provider__id': self.provider._id,
'workflow': self.provider.reviews_workflow,
'provider_url': f'{self.provider.domain or settings.DOMAIN}preprints/'
f'{(self.provider._id if not self.provider.domain else '').strip('/')}',
- 'provider_contact_email': self.provider.email_contact or settings.OSF_CONTACT_EMAIL,
- 'provider_support_email': self.provider.email_support or settings.OSF_SUPPORT_EMAIL,
'is_creator': True,
'provider_name': 'OSF Preprints' if self.provider.name == 'Open Science Framework' else self.provider.name,
- 'logo': settings.OSF_PREPRINTS_LOGO if self.provider._id == 'osf' else self.provider._id,
'document_type': self.provider.preprint_word,
- 'notify_comment': not self.provider.reviews_comments_private
},
)
diff --git a/osf/models/provider.py b/osf/models/provider.py
index 977ff662b42..92681173240 100644
--- a/osf/models/provider.py
+++ b/osf/models/provider.py
@@ -14,7 +14,7 @@
from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase
from framework import sentry
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .base import BaseModel, TypedObjectIDMixin
from .mixins import ReviewProviderMixin
from .brand import Brand
@@ -253,7 +253,7 @@ def setup_share_source(self, provider_home_page):
class CollectionProvider(AbstractProvider):
DEFAULT_SUBSCRIPTIONS = [
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
]
class Meta:
@@ -295,8 +295,8 @@ class RegistrationProvider(AbstractProvider):
STATE_FIELD_NAME = 'moderation_state'
DEFAULT_SUBSCRIPTIONS = [
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS,
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS,
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS,
]
diff --git a/osf/models/registrations.py b/osf/models/registrations.py
index 7426121de98..e1d819b43bf 100644
--- a/osf/models/registrations.py
+++ b/osf/models/registrations.py
@@ -23,7 +23,7 @@
from osf.exceptions import NodeStateError, DraftRegistrationStateError
from osf.external.internet_archive.tasks import archive_to_ia, update_ia_metadata
from osf.metrics import RegistriesModerationMetrics
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .action import RegistrationAction
from .archive import ArchiveJob
from .contributor import DraftRegistrationContributor
@@ -1336,7 +1336,7 @@ def create_from_node(
node=None,
data=None,
provider=None,
- notification_type=NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type=NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
):
if not provider:
provider = RegistrationProvider.get_default()
@@ -1381,7 +1381,7 @@ def create_from_node(
draft,
contributor=contributor.user,
auth=Auth(user) if user != contributor.user else None,
- notification_type=notification_type if contributor.user.is_confirmed else NotificationType.Type.USER_INVITE_DRAFT_REGISTRATION,
+ notification_type=notification_type if contributor.user.is_confirmed else NotificationTypeEnum.USER_INVITE_DRAFT_REGISTRATION,
permissions=contributor.permission
)
diff --git a/osf/models/sanctions.py b/osf/models/sanctions.py
index 8855852f1a1..5fa5d304d22 100644
--- a/osf/models/sanctions.py
+++ b/osf/models/sanctions.py
@@ -19,7 +19,7 @@
from osf.utils import tokens
from osf.utils.machines import ApprovalsMachine
from osf.utils.workflows import ApprovalStates, SanctionTypes
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
VIEW_PROJECT_URL_TEMPLATE = osf_settings.DOMAIN + '{node_id}/'
@@ -462,8 +462,8 @@ class Embargo(SanctionCallbackMixin, EmailApprovableSanction):
DISPLAY_NAME = 'Embargo'
SHORT_NAME = 'embargo'
- AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_EMBARGO_ADMIN
- NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_EMBARGO_NON_ADMIN
+ AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_EMBARGO_ADMIN
+ NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_EMBARGO_NON_ADMIN
VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
@@ -662,8 +662,8 @@ class Retraction(EmailApprovableSanction):
DISPLAY_NAME = 'Retraction'
SHORT_NAME = 'retraction'
- AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_RETRACTION_ADMIN
- NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_RETRACTION_NON_ADMIN
+ AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_RETRACTION_ADMIN
+ NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_RETRACTION_NON_ADMIN
VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
@@ -800,8 +800,8 @@ class RegistrationApproval(SanctionCallbackMixin, EmailApprovableSanction):
DISPLAY_NAME = 'Approval'
SHORT_NAME = 'registration_approval'
- AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_REGISTRATION_ADMIN
- NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_REGISTRATION_NON_ADMIN
+ AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_REGISTRATION_ADMIN
+ NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_REGISTRATION_NON_ADMIN
VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
@@ -980,8 +980,8 @@ class EmbargoTerminationApproval(EmailApprovableSanction):
DISPLAY_NAME = 'Embargo Termination Request'
SHORT_NAME = 'embargo_termination_approval'
- AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_EMBARGO_TERMINATION_ADMIN
- NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationType.Type.NODE_PENDING_EMBARGO_TERMINATION_NON_ADMIN
+ AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_EMBARGO_TERMINATION_ADMIN
+ NON_AUTHORIZER_NOTIFY_EMAIL_TYPE = NotificationTypeEnum.NODE_PENDING_EMBARGO_TERMINATION_NON_ADMIN
VIEW_URL_TEMPLATE = VIEW_PROJECT_URL_TEMPLATE
APPROVE_URL_TEMPLATE = osf_settings.DOMAIN + 'token_action/{node_id}/?token={token}'
diff --git a/osf/models/schema_response.py b/osf/models/schema_response.py
index 0ea5239ee5d..c68084f1601 100644
--- a/osf/models/schema_response.py
+++ b/osf/models/schema_response.py
@@ -9,7 +9,7 @@
from framework.exceptions import PermissionsError
from osf.exceptions import PreviousSchemaResponseError, SchemaResponseStateError, SchemaResponseUpdateError
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .base import BaseModel, ObjectIDMixin
from .metaschema import RegistrationSchemaBlock
from .schema_response_block import SchemaResponseBlock
@@ -488,10 +488,10 @@ def _notify_users(self, event, event_initiator):
)
notification_type = {
- 'create': NotificationType.Type.NODE_SCHEMA_RESPONSE_INITIATED.instance,
- 'submit': NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED.instance,
- 'accept': NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED.instance,
- 'reject': NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED.instance,
+ 'create': NotificationTypeEnum.NODE_SCHEMA_RESPONSE_INITIATED.instance,
+ 'submit': NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED.instance,
+ 'accept': NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED.instance,
+ 'reject': NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED.instance,
}.get(event)
if not notification_type:
return
diff --git a/osf/models/user.py b/osf/models/user.py
index ecf720739d9..c49eeb41814 100644
--- a/osf/models/user.py
+++ b/osf/models/user.py
@@ -67,7 +67,7 @@
from website.project import new_bookmark_collection
from website.util.metrics import OsfSourceTags, unregistered_created_source_tag
from importlib import import_module
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf.utils.requests import string_type_request_headers
@@ -249,6 +249,7 @@ class OSFUser(DirtyFieldsMixin, GuidMixin, BaseModel, AbstractBaseUser, Permissi
# }
email_last_sent = NonNaiveDateTimeField(null=True, blank=True)
+ no_login_email_last_sent = NonNaiveDateTimeField(null=True, blank=True)
change_password_last_attempt = NonNaiveDateTimeField(null=True, blank=True)
# Logs number of times user attempted to change their password where their
# old password was invalid
@@ -1096,14 +1097,13 @@ def set_password(self, raw_password, notify=True):
raise ChangePasswordError(['Password cannot be the same as your email address'])
super().set_password(raw_password)
if had_existing_password and notify:
- NotificationType.Type.USER_PASSWORD_RESET.instance.emit(
+ NotificationTypeEnum.USER_PASSWORD_RESET.instance.emit(
subscribed_object=self,
user=self,
message_frequency='instantly',
event_context={
'domain': website_settings.DOMAIN,
'user_fullname': self.fullname,
- 'can_change_preferences': False,
'osf_contact_email': website_settings.OSF_CONTACT_EMAIL
}
)
diff --git a/osf/models/user_message.py b/osf/models/user_message.py
index 10ea735b61e..34ec44e6b72 100644
--- a/osf/models/user_message.py
+++ b/osf/models/user_message.py
@@ -3,7 +3,7 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from .base import BaseModel, ObjectIDMixin
from website import settings
@@ -32,7 +32,7 @@ def get_notification_type(cls: Type['MessageTypes'], message_type: str) -> str:
str: The email template string for the specified message type.
"""
return {
- cls.INSTITUTIONAL_REQUEST: NotificationType.Type.USER_INSTITUTIONAL_ACCESS_REQUEST
+ cls.INSTITUTIONAL_REQUEST: NotificationTypeEnum.USER_INSTITUTIONAL_ACCESS_REQUEST
}[message_type]
diff --git a/osf/utils/machines.py b/osf/utils/machines.py
index d13d85075f4..eb7e93d450b 100644
--- a/osf/utils/machines.py
+++ b/osf/utils/machines.py
@@ -6,7 +6,7 @@
from framework.auth import Auth
from osf.exceptions import InvalidTransitionError
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from osf.models.preprintlog import PreprintLog
from osf.models.action import ReviewAction, NodeRequestAction, PreprintRequestAction
from osf.utils import permissions
@@ -192,7 +192,7 @@ def notify_withdraw(self, ev):
if context.get('requester_fullname', None):
context['is_requester'] = requester == contributor
- NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_APPROVED.instance.emit(
+ NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_APPROVED.instance.emit(
user=contributor,
subscribed_object=self.machineable,
event_context={
@@ -234,7 +234,7 @@ def save_changes(self, ev):
make_curator = self.machineable.request_type == NodeRequestTypes.INSTITUTIONAL_REQUEST.value
visible = False if make_curator else ev.kwargs.get('visible', True)
if self.machineable.request_type in (NodeRequestTypes.ACCESS.value, NodeRequestTypes.INSTITUTIONAL_REQUEST.value):
- notification_type = NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ notification_type = NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
else:
notification_type = None
@@ -265,7 +265,7 @@ def notify_submit(self, ev):
if not self.machineable.request_type == NodeRequestTypes.INSTITUTIONAL_REQUEST.value:
for admin in self.machineable.target.get_users_with_perm(permissions.ADMIN):
- NotificationType.Type.NODE_REQUEST_ACCESS_SUBMITTED.instance.emit(
+ NotificationTypeEnum.NODE_REQUEST_ACCESS_SUBMITTED.instance.emit(
user=admin,
subscribed_object=self.machineable,
event_context={
@@ -286,7 +286,7 @@ def notify_accept_reject(self, ev):
if ev.event.name == DefaultTriggers.REJECT.value:
context = self.get_context()
- NotificationType.Type.NODE_REQUEST_ACCESS_DENIED.instance.emit(
+ NotificationTypeEnum.NODE_REQUEST_ACCESS_DENIED.instance.emit(
user=self.machineable.creator,
subscribed_object=self.machineable,
event_context=context
@@ -345,7 +345,7 @@ def notify_accept_reject(self, ev):
context['comment'] = self.action.comment
context['contributor_fullname'] = self.machineable.creator.fullname
- NotificationType.Type.PREPRINT_REQUEST_WITHDRAWAL_DECLINED.instance.emit(
+ NotificationTypeEnum.PREPRINT_REQUEST_WITHDRAWAL_DECLINED.instance.emit(
user=self.machineable.creator,
subscribed_object=self.machineable,
event_context=context
diff --git a/osf/utils/notifications.py b/osf/utils/notifications.py
index 8808ef3b243..0d279479155 100644
--- a/osf/utils/notifications.py
+++ b/osf/utils/notifications.py
@@ -1,6 +1,6 @@
from django.utils import timezone
-from osf.models.notification_type import NotificationType
+from osf.models.notification_type import NotificationTypeEnum
from website.reviews import signals as reviews_signals
from website.settings import DOMAIN, OSF_SUPPORT_EMAIL, OSF_CONTACT_EMAIL
from osf.utils.workflows import RegistrationModerationTriggers
@@ -50,7 +50,7 @@ def notify_submit(resource, user, *args, **kwargs):
context=context,
recipients=recipients,
resource=resource,
- notification_type=NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ notification_type=NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
)
reviews_signals.reviews_email_submit_moderators_notifications.send(
timestamp=timezone.now(),
@@ -71,7 +71,7 @@ def notify_resubmit(resource, user, *args, **kwargs):
reviews_signals.reviews_email_submit.send(
recipients=recipients,
context=context,
- notification_type=NotificationType.Type.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION,
+ notification_type=NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION,
resource=resource,
)
reviews_signals.reviews_email_submit_moderators_notifications.send(
@@ -94,7 +94,7 @@ def notify_accept_reject(resource, user, action, states, *args, **kwargs):
reviews_signals.reviews_email.send(
creator=user,
context=context,
- template=NotificationType.Type.REVIEWS_SUBMISSION_STATUS,
+ template=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS,
action=action
)
@@ -110,7 +110,7 @@ def notify_edit_comment(resource, user, action, states, *args, **kwargs):
reviews_signals.reviews_email.send(
creator=user,
context=context,
- template=NotificationType.Type.REVIEWS_SUBMISSION_STATUS,
+ template=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS,
action=action
)
@@ -118,21 +118,14 @@ def notify_edit_comment(resource, user, action, states, *args, **kwargs):
def notify_reject_withdraw_request(resource, action, *args, **kwargs):
context = get_email_template_context(resource)
context['requester_fullname'] = action.creator.fullname
- context['referrer_fullname'] = action.creator.fullname
- context['force_withdrawal'] = False
context['notify_comment'] = not resource.provider.reviews_comments_private
- context['reviewable_withdrawal_justification'] = resource.withdrawal_justification
for contributor in resource.contributors.all():
- context['user_fullname'] = contributor.fullname
context['contributor_fullname'] = contributor.fullname
- context['is_requester'] = action.creator == contributor
context['comment'] = action.comment
- NotificationType.Type.NODE_WITHDRAWAl_REQUEST_REJECTED.instance.emit(
+ NotificationTypeEnum.NODE_WITHDRAWAl_REQUEST_REJECTED.instance.emit(
user=contributor,
event_context={
- 'is_requester': contributor == action.creator,
- 'ever_public': getattr(resource, 'ever_public', resource.is_public),
**context
},
)
@@ -163,7 +156,7 @@ def notify_withdraw_registration(resource, action, *args, **kwargs):
context['user_fullname'] = contributor.fullname
context['is_requester'] = resource.retraction.initiated_by == contributor
- NotificationType.Type.NODE_WITHDRAWAl_REQUEST_APPROVED.instance.emit(
+ NotificationTypeEnum.NODE_WITHDRAWAl_REQUEST_APPROVED.instance.emit(
user=contributor,
event_context=context
)
diff --git a/osf_tests/management_commands/test_migrate_notifications.py b/osf_tests/management_commands/test_migrate_notifications.py
index 1ea906d0d17..0bcecbdd37f 100644
--- a/osf_tests/management_commands/test_migrate_notifications.py
+++ b/osf_tests/management_commands/test_migrate_notifications.py
@@ -11,6 +11,7 @@
)
from osf.models import (
NotificationType,
+ NotificationTypeEnum,
NotificationSubscription,
)
from osf.management.commands.migrate_notifications import (
@@ -99,7 +100,7 @@ def test_migrate_provider_subscription(self, users, provider, provider2):
self.create_legacy_sub(event_name='global_reviews', users=users, provider=RegistrationProvider.get_default())
migrate_legacy_notification_subscriptions()
subs = NotificationSubscription.objects.filter(
- notification_type__name=NotificationType.Type.REVIEWS_SUBMISSION_STATUS
+ notification_type__name=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
)
assert subs.count() == 9
for obj in [provider, provider2, RegistrationProvider.get_default()]:
@@ -108,7 +109,7 @@ def test_migrate_provider_subscription(self, users, provider, provider2):
def test_migrate_node_subscription(self, users, user, node):
migrate_legacy_notification_subscriptions()
- nt = NotificationType.objects.get(name=NotificationType.Type.NODE_FILE_UPDATED)
+ nt = NotificationType.objects.get(name=NotificationTypeEnum.NODE_FILE_UPDATED)
assert nt.object_content_type == ContentType.objects.get_for_model(Node)
subs = NotificationSubscription.objects.filter(notification_type=nt)
assert subs.count() == 1
@@ -130,7 +131,7 @@ def test_idempotent_migration(self, users, user, node, provider):
user=user,
object_id=node.id,
content_type=ContentType.objects.get_for_model(node.__class__),
- notification_type__name=NotificationType.Type.NODE_FILE_UPDATED
+ notification_type__name=NotificationTypeEnum.NODE_FILE_UPDATED
)
def test_migrate_all_subscription_types(self, users, user, provider, provider2, node):
@@ -158,7 +159,7 @@ def test_migrate_all_subscription_types(self, users, user, provider, provider2,
node_ct = ContentType.objects.get_for_model(node.__class__)
assert NotificationSubscription.objects.filter(
- notification_type=NotificationType.Type.NODE_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.NODE_FILE_UPDATED.instance,
content_type=node_ct,
object_id=node.id
).exists()
@@ -201,7 +202,7 @@ def test_migrate_batch_with_valid_and_invalid(self, users, user, node, provider)
)
migrate_legacy_notification_subscriptions()
assert NotificationSubscription.objects.filter(
- notification_type__name=NotificationType.Type.REVIEWS_SUBMISSION_STATUS
+ notification_type__name=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
).count() == 3
def test_migrate_subscription_frequencies_none(self, user, django_db_blocker):
@@ -214,7 +215,7 @@ def test_migrate_subscription_frequencies_none(self, user, django_db_blocker):
migrate_legacy_notification_subscriptions()
- nt = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ nt = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
subs = NotificationSubscription.objects.filter(notification_type=nt)
assert subs.count() == 1
assert subs.get().message_frequency == 'none'
@@ -228,7 +229,7 @@ def test_migrate_subscription_frequencies_transactional(self, user, django_db_bl
migrate_legacy_notification_subscriptions()
- nt = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ nt = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
subs = NotificationSubscription.objects.filter(
notification_type=nt,
content_type=ContentType.objects.get_for_model(user.__class__),
@@ -246,7 +247,7 @@ def test_migrate_global_subscription_frequencies_daily(self, user, django_db_blo
migrate_legacy_notification_subscriptions()
- nt = NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ nt = NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
subs = NotificationSubscription.objects.filter(
notification_type=nt,
content_type=ContentType.objects.get_for_model(user.__class__),
@@ -264,7 +265,7 @@ def test_migrate_node_subscription_frequencies_daily(self, user, node, django_db
migrate_legacy_notification_subscriptions()
- nt = NotificationType.objects.get(name=NotificationType.Type.NODE_FILE_UPDATED)
+ nt = NotificationType.objects.get(name=NotificationTypeEnum.NODE_FILE_UPDATED)
subs = NotificationSubscription.objects.filter(
user=user,
notification_type=nt,
@@ -283,7 +284,7 @@ def test_node_subscription_copy_group_frequency(self, user, node, django_db_bloc
migrate_legacy_notification_subscriptions()
- NotificationType.Type.FILE_UPDATED.instance.emit(
+ NotificationTypeEnum.FILE_UPDATED.instance.emit(
user=user,
subscribed_object=node,
event_context={
@@ -294,7 +295,7 @@ def test_node_subscription_copy_group_frequency(self, user, node, django_db_bloc
nt = NotificationSubscription.objects.get(
user=user,
- notification_type__name=NotificationType.Type.FILE_UPDATED,
+ notification_type__name=NotificationTypeEnum.FILE_UPDATED,
content_type=ContentType.objects.get_for_model(node),
object_id=node.id,
)
@@ -309,7 +310,7 @@ def test_user_subscription_copy_group_frequency(self, user, node, django_db_bloc
migrate_legacy_notification_subscriptions()
- NotificationType.Type.FILE_UPDATED.instance.emit(
+ NotificationTypeEnum.FILE_UPDATED.instance.emit(
user=user,
subscribed_object=user,
event_context={
@@ -320,7 +321,7 @@ def test_user_subscription_copy_group_frequency(self, user, node, django_db_bloc
nt = NotificationSubscription.objects.get(
user=user,
- notification_type__name=NotificationType.Type.FILE_UPDATED,
+ notification_type__name=NotificationTypeEnum.FILE_UPDATED,
content_type=ContentType.objects.get_for_model(user),
object_id=user.id,
)
@@ -335,7 +336,7 @@ def test_provider_subscription_copy_group_frequency(self, user, node, provider):
migrate_legacy_notification_subscriptions()
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
user=user,
subscribed_object=provider,
event_context={
@@ -346,8 +347,8 @@ def test_provider_subscription_copy_group_frequency(self, user, node, provider):
nt = NotificationSubscription.objects.get(
user=user,
- notification_type__name=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS,
- content_type=ContentType.objects.get_for_model(provider, for_concrete_model=False),
+ notification_type__name=NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS,
+ content_type=ContentType.objects.get_for_model(provider),
object_id=provider.id,
)
assert nt.message_frequency == 'none'
diff --git a/osf_tests/test_archiver.py b/osf_tests/test_archiver.py
index 1e4ece0b016..1987c1088ee 100644
--- a/osf_tests/test_archiver.py
+++ b/osf_tests/test_archiver.py
@@ -20,7 +20,7 @@
from website.archiver import listeners
from website.archiver.tasks import * # noqa: F403
-from osf.models import Guid, RegistrationSchema, Registration, NotificationType
+from osf.models import Guid, RegistrationSchema, Registration, NotificationTypeEnum
from osf.models.archive import ArchiveTarget, ArchiveJob
from osf.models.base import generate_object_id
from osf.utils.migrations import map_schema_to_schemablocks
@@ -735,8 +735,8 @@ def test_handle_archive_fail(self):
{}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_ARCHIVE_JOB_COPY_ERROR
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_ARCHIVE_JOB_COPY_ERROR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_ARCHIVE_JOB_COPY_ERROR
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_ARCHIVE_JOB_COPY_ERROR
assert notifications['emits'][0]['kwargs']['destination_address'] == settings.OSF_SUPPORT_EMAIL
assert notifications['emits'][1]['kwargs']['user'] == self.user
self.dst.reload()
@@ -752,8 +752,8 @@ def test_handle_archive_fail_copy(self):
{}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_ARCHIVE_JOB_COPY_ERROR
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_ARCHIVE_JOB_COPY_ERROR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_ARCHIVE_JOB_COPY_ERROR
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_ARCHIVE_JOB_COPY_ERROR
assert notifications['emits'][0]['kwargs']['destination_address'] == settings.OSF_SUPPORT_EMAIL
assert notifications['emits'][1]['kwargs']['user'] == self.user
@@ -767,8 +767,8 @@ def test_handle_archive_fail_size(self):
{}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_ARCHIVE_JOB_EXCEEDED
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_ARCHIVE_JOB_EXCEEDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_ARCHIVE_JOB_EXCEEDED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_ARCHIVE_JOB_EXCEEDED
assert notifications['emits'][0]['kwargs']['destination_address'] == settings.OSF_SUPPORT_EMAIL
assert notifications['emits'][1]['kwargs']['user'] == self.user
@@ -782,8 +782,8 @@ def test_handle_archive_uncaught_error(self):
{}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_ARCHIVE_JOB_UNCAUGHT_ERROR
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_ARCHIVE_JOB_UNCAUGHT_ERROR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_ARCHIVE_JOB_UNCAUGHT_ERROR
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_ARCHIVE_JOB_UNCAUGHT_ERROR
assert notifications['emits'][0]['kwargs']['destination_address'] == settings.OSF_SUPPORT_EMAIL
assert notifications['emits'][1]['kwargs']['user'] == self.user
diff --git a/osf_tests/test_collection.py b/osf_tests/test_collection.py
index 5eba83ac530..1dd1b3473b9 100644
--- a/osf_tests/test_collection.py
+++ b/osf_tests/test_collection.py
@@ -5,7 +5,7 @@
from framework.auth import Auth
-from osf.models import Collection, NotificationType
+from osf.models import Collection, NotificationTypeEnum
from osf.exceptions import NodeStateError
from tests.utils import capture_notifications
from website.views import find_bookmark_collection
@@ -134,7 +134,7 @@ def test_node_removed_from_collection_on_privacy_change_notify(self, auth, provi
with capture_notifications() as notifications:
provider_collected_node.set_privacy('private', auth=auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_PRIVATE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_PRIVATE
@mock.patch('osf.models.node.Node.check_privacy_change_viability', mock.Mock()) # mocks the storage usage limits
def test_node_removed_from_collection_on_privacy_change_no_provider(self, auth, collected_node, bookmark_collection):
diff --git a/osf_tests/test_collection_submission.py b/osf_tests/test_collection_submission.py
index 478a437f5a5..2b661f00873 100644
--- a/osf_tests/test_collection_submission.py
+++ b/osf_tests/test_collection_submission.py
@@ -8,7 +8,7 @@
from osf_tests.factories import NodeFactory, CollectionFactory, CollectionProviderFactory
-from osf.models import CollectionSubmission, NotificationType
+from osf.models import CollectionSubmission, NotificationTypeEnum
from osf.utils.workflows import CollectionSubmissionStates
from framework.exceptions import PermissionsError
from api_tests.utils import UserRoles
@@ -165,8 +165,8 @@ def test_notify_contributors_pending(self, node, moderated_collection):
)
collection_submission.save()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert collection_submission.state == CollectionSubmissionStates.PENDING
def test_notify_moderators_pending(self, node, moderated_collection):
@@ -179,8 +179,8 @@ def test_notify_moderators_pending(self, node, moderated_collection):
)
collection_submission.save()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert collection_submission.state == CollectionSubmissionStates.PENDING
@pytest.mark.parametrize('user_role', [UserRoles.UNAUTHENTICATED, UserRoles.NONCONTRIB])
@@ -201,7 +201,7 @@ def test_notify_moderated_accepted(self, node, moderated_collection_submission):
with capture_notifications() as notifications:
moderated_collection_submission.accept(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED
assert moderated_collection_submission.state == CollectionSubmissionStates.ACCEPTED
@@ -224,7 +224,7 @@ def test_notify_moderated_rejected(self, node, moderated_collection_submission):
with capture_notifications() as notifications:
moderated_collection_submission.reject(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REJECTED
assert moderated_collection_submission.state == CollectionSubmissionStates.REJECTED
@@ -253,7 +253,7 @@ def test_notify_moderated_removed_moderator(self, node, moderated_collection_sub
with capture_notifications() as notifications:
moderated_collection_submission.remove(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_MODERATOR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_MODERATOR
assert moderated_collection_submission.state == CollectionSubmissionStates.REMOVED
@@ -264,8 +264,8 @@ def test_notify_moderated_removed_admin(self, node, moderated_collection_submiss
with capture_notifications() as notifications:
moderated_collection_submission.remove(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 2
- assert notifications['emits'][1]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
assert moderated_collection_submission.state == CollectionSubmissionStates.REMOVED
@@ -350,8 +350,8 @@ def test_notify_moderated_removed_admin(self, node, unmoderated_collection_submi
with capture_notifications() as notifications:
unmoderated_collection_submission.remove(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
- assert notifications['emits'][1]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
assert unmoderated_collection_submission.state == CollectionSubmissionStates.REMOVED
def test_resubmit_success(self, node, unmoderated_collection_submission):
@@ -454,7 +454,7 @@ def test_notify_moderated_accepted(self, node, hybrid_moderated_collection_submi
with capture_notifications() as notifications:
hybrid_moderated_collection_submission.accept(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_ACCEPTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_ACCEPTED
assert hybrid_moderated_collection_submission.state == CollectionSubmissionStates.ACCEPTED
@pytest.mark.parametrize('user_role', [UserRoles.UNAUTHENTICATED, UserRoles.NONCONTRIB])
@@ -476,7 +476,7 @@ def test_notify_moderated_rejected(self, node, hybrid_moderated_collection_submi
with capture_notifications() as notifications:
hybrid_moderated_collection_submission.reject(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REJECTED
assert hybrid_moderated_collection_submission.state == CollectionSubmissionStates.REJECTED
@pytest.mark.parametrize('user_role', UserRoles.excluding(*[UserRoles.ADMIN_USER, UserRoles.MODERATOR]))
@@ -504,7 +504,7 @@ def test_notify_moderated_removed_moderator(self, node, hybrid_moderated_collect
with capture_notifications() as notifications:
hybrid_moderated_collection_submission.remove(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_MODERATOR
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_MODERATOR
assert hybrid_moderated_collection_submission.state == CollectionSubmissionStates.REMOVED
def test_notify_moderated_removed_admin(self, node, hybrid_moderated_collection_submission):
@@ -514,8 +514,8 @@ def test_notify_moderated_removed_admin(self, node, hybrid_moderated_collection_
with capture_notifications() as notifications:
hybrid_moderated_collection_submission.remove(user=moderator, comment='Test Comment')
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
- assert notifications['emits'][1]['type'] == NotificationType.Type.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.COLLECTION_SUBMISSION_REMOVED_ADMIN
assert hybrid_moderated_collection_submission.state == CollectionSubmissionStates.REMOVED
diff --git a/osf_tests/test_institution.py b/osf_tests/test_institution.py
index b0575d02fe6..039b0ce04dd 100644
--- a/osf_tests/test_institution.py
+++ b/osf_tests/test_institution.py
@@ -4,7 +4,7 @@
import pytest
from addons.osfstorage.models import Region
-from osf.models import Institution, InstitutionStorageRegion, NotificationType
+from osf.models import Institution, InstitutionStorageRegion, NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
InstitutionFactory,
@@ -157,8 +157,8 @@ def test_send_deactivation_email_call_count(self):
with capture_notifications() as notifications:
institution._send_deactivation_email()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INSTITUTION_DEACTIVATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_INSTITUTION_DEACTIVATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INSTITUTION_DEACTIVATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_INSTITUTION_DEACTIVATION
def test_send_deactivation_email_call_args(self):
institution = InstitutionFactory()
@@ -168,7 +168,7 @@ def test_send_deactivation_email_call_args(self):
with capture_notifications() as notifications:
institution._send_deactivation_email()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INSTITUTION_DEACTIVATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INSTITUTION_DEACTIVATION
def test_deactivate_inactive_institution_noop(self):
institution = InstitutionFactory()
diff --git a/osf_tests/test_institutional_admin_contributors.py b/osf_tests/test_institutional_admin_contributors.py
index 8f4c7fad1c1..ad9f7075832 100644
--- a/osf_tests/test_institutional_admin_contributors.py
+++ b/osf_tests/test_institutional_admin_contributors.py
@@ -2,7 +2,7 @@
from unittest import mock
-from osf.models import Contributor, NotificationType
+from osf.models import Contributor, NotificationTypeEnum
from osf_tests.factories import (
AuthUserFactory,
ProjectFactory,
@@ -142,7 +142,7 @@ def test_requested_permissions_or_default(self, app, project, institutional_admi
auth=mock.ANY,
permissions=permissions.ADMIN, # `requested_permissions` should take precedence
visible=True,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
make_curator=False,
)
@@ -168,7 +168,7 @@ def test_permissions_override_requested_permissions(self, app, project, institut
auth=mock.ANY,
permissions=permissions.ADMIN, # `requested_permissions` should take precedence
visible=True,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
make_curator=False,
)
@@ -194,6 +194,6 @@ def test_requested_permissions_is_used(self, app, project, institutional_admin):
auth=mock.ANY,
permissions=permissions.ADMIN, # `requested_permissions` should take precedence
visible=True,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST,
make_curator=False,
)
diff --git a/osf_tests/test_merging_users.py b/osf_tests/test_merging_users.py
index f0d1c1069ca..e505b5580e3 100644
--- a/osf_tests/test_merging_users.py
+++ b/osf_tests/test_merging_users.py
@@ -19,7 +19,7 @@
)
from importlib import import_module
from django.conf import settings as django_conf_settings
-from osf.models import UserSessionMap, NotificationType
+from osf.models import UserSessionMap, NotificationTypeEnum
from tests.utils import run_celery_tasks, capture_notifications
from waffle.testutils import override_flag
from osf.features import ENABLE_GV
@@ -300,5 +300,5 @@ def test_send_confirm_email_emits_merge_notification(self):
with capture_notifications() as notifications:
send_confirm_email(merger, target_email)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_CONFIRM_MERGE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_CONFIRM_MERGE
assert notifications['emits'][0]['kwargs']['destination_address'] == target_email
diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py
index 6b52539c491..e78876b1fed 100644
--- a/osf_tests/test_node.py
+++ b/osf_tests/test_node.py
@@ -34,7 +34,7 @@
NodeRelation,
Registration,
DraftRegistration,
- CollectionSubmission, NotificationType
+ CollectionSubmission, NotificationTypeEnum
)
from addons.wiki.models import WikiPage, WikiVersion
@@ -2150,7 +2150,7 @@ def test_set_privacy(self, node, auth):
with capture_notifications() as notifications:
node.set_privacy('public', auth=auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_NEW_PUBLIC_PROJECT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_NEW_PUBLIC_PROJECT
assert node.logs.first().action == NodeLog.MADE_PUBLIC
assert last_logged_before_method_call != node.last_logged
node.save()
@@ -2171,7 +2171,7 @@ def test_set_privacy_sends_mail(self, node, auth):
node.set_privacy('private', auth=auth)
node.set_privacy('public', auth=auth, meeting_creation=False)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_NEW_PUBLIC_PROJECT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_NEW_PUBLIC_PROJECT
def test_set_privacy_can_not_cancel_pending_embargo_for_registration(self, node, user, auth):
registration = RegistrationFactory(project=node)
diff --git a/osf_tests/test_registration_moderation_notifications.py b/osf_tests/test_registration_moderation_notifications.py
index ca16b4eb993..5516344a2b1 100644
--- a/osf_tests/test_registration_moderation_notifications.py
+++ b/osf_tests/test_registration_moderation_notifications.py
@@ -6,7 +6,7 @@
from notifications.tasks import send_users_digest_email
from osf.management.commands.populate_notification_types import populate_notification_types
from osf.migrations import update_provider_auth_groups
-from osf.models import Brand, NotificationSubscription, NotificationType
+from osf.models import Brand, NotificationSubscription, NotificationTypeEnum
from osf.models.action import RegistrationAction
from osf.utils.notifications import (
notify_submit,
@@ -135,11 +135,11 @@ def test_submit_notifications(self, registration, moderator, admin, contrib, pro
notify_submit(registration, admin)
assert len(notification['emits']) == 3
- assert notification['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notification['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
assert notification['emits'][0]['kwargs']['user'] == admin
- assert notification['emits'][1]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notification['emits'][1]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
assert notification['emits'][1]['kwargs']['user'] == contrib
- assert notification['emits'][2]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notification['emits'][2]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert NotificationSubscription.objects.count() == 7
digest = NotificationSubscription.objects.last()
assert digest.user == moderator
diff --git a/osf_tests/test_reviewable.py b/osf_tests/test_reviewable.py
index f3627d1faf2..954abd6ee1f 100644
--- a/osf_tests/test_reviewable.py
+++ b/osf_tests/test_reviewable.py
@@ -1,7 +1,7 @@
from unittest import mock
import pytest
-from osf.models import Preprint, NotificationType
+from osf.models import Preprint, NotificationTypeEnum
from osf.utils.workflows import DefaultStates
from osf_tests.factories import PreprintFactory, AuthUserFactory
from tests.utils import capture_notifications
@@ -48,7 +48,7 @@ def test_reject_resubmission_sends_emails(self):
with capture_notifications() as notifications:
preprint.run_submit(user)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
assert preprint.machine_state == DefaultStates.PENDING.value
assert not user.notification_subscriptions.exists()
@@ -59,5 +59,5 @@ def test_reject_resubmission_sends_emails(self):
with capture_notifications() as notifications:
preprint.run_submit(user) # Resubmission alerts users and moderators
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION
assert preprint.machine_state == DefaultStates.PENDING.value
diff --git a/osf_tests/test_schema_responses.py b/osf_tests/test_schema_responses.py
index 958150aee76..c087c6f514e 100644
--- a/osf_tests/test_schema_responses.py
+++ b/osf_tests/test_schema_responses.py
@@ -3,7 +3,7 @@
from api.providers.workflows import Workflows
from framework.exceptions import PermissionsError
from osf.exceptions import PreviousSchemaResponseError, SchemaResponseStateError, SchemaResponseUpdateError
-from osf.models import RegistrationSchema, RegistrationSchemaBlock, SchemaResponseBlock, NotificationType
+from osf.models import RegistrationSchema, RegistrationSchemaBlock, SchemaResponseBlock, NotificationTypeEnum
from osf.models import schema_response # import module for mocking purposes
from osf.utils.workflows import ApprovalStates, SchemaResponseTriggers
from osf_tests.factories import AuthUserFactory, ProjectFactory, RegistrationFactory, RegistrationProviderFactory
@@ -259,7 +259,7 @@ def test_create_from_previous_response_notification(
initiator=admin_user
)
assert len(notifications['emits']) == len(notification_recipients)
- assert all(notification['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_INITIATED
+ assert all(notification['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_INITIATED
for notification in notifications['emits'])
assert all(notification['kwargs']['user'].username in notification_recipients for notification in notifications['emits'])
@@ -588,9 +588,9 @@ def test_submit_response_notification(
with capture_notifications() as notifications:
revised_response.submit(user=admin_user, required_approvers=[admin_user])
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_SUBMITTED
def test_no_submit_notification_on_initial_response(self, initial_response, admin_user):
initial_response.approvals_state_machine.set_state(ApprovalStates.IN_PROGRESS)
@@ -752,7 +752,7 @@ def test_reject_response_notification(
with capture_notifications() as notifications:
revised_response.reject(user=admin_user)
assert len(notifications['emits']) == 3
- assert all(notification['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
+ assert all(notification['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
for notification in notifications['emits'])
def test_no_reject_notification_on_initial_response(self, initial_response, admin_user):
@@ -861,9 +861,9 @@ def test_accept_notification_sent_on_admin_approval(self, revised_response, admi
revised_response.approve(user=admin_user)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == moderator
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert notifications['emits'][1]['kwargs']['user'] == admin_user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
def test_moderators_notified_on_admin_approval(self, revised_response, admin_user, moderator):
revised_response.approvals_state_machine.set_state(ApprovalStates.UNAPPROVED)
@@ -874,9 +874,9 @@ def test_moderators_notified_on_admin_approval(self, revised_response, admin_use
revised_response.approve(user=admin_user)
assert len(notifications['emits']) == 2
assert notifications['emits'][0]['kwargs']['user'] == moderator
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS
assert notifications['emits'][1]['kwargs']['user'] == admin_user
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
def test_no_moderator_notification_on_admin_approval_of_initial_response(
self, initial_response, admin_user):
@@ -914,9 +914,9 @@ def test_moderator_accept_notification(
with capture_notifications() as notifications:
revised_response.accept(user=moderator)
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_APPROVED
def test_no_moderator_accept_notification_on_initial_response(
self, initial_response, moderator):
@@ -954,9 +954,9 @@ def test_moderator_reject_notification(
with capture_notifications() as notifications:
revised_response.reject(user=moderator)
assert len(notifications['emits']) == 3
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
- assert notifications['emits'][1]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
- assert notifications['emits'][2]['type'] == NotificationType.Type.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
+ assert notifications['emits'][2]['type'] == NotificationTypeEnum.NODE_SCHEMA_RESPONSE_REJECTED
def test_no_moderator_reject_notification_on_initial_response(
self, initial_response, moderator):
diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py
index a2fc5b0e92a..6bccb40e9dd 100644
--- a/osf_tests/test_user.py
+++ b/osf_tests/test_user.py
@@ -32,7 +32,7 @@
PreprintContributor,
DraftRegistrationContributor,
UserSessionMap,
- NotificationType,
+ NotificationTypeEnum,
)
from osf.models.institution_affiliation import get_user_by_institution_identity
from addons.github.tests.factories import GitHubAccountFactory
@@ -946,7 +946,7 @@ def test_set_password_notify_default(self, user):
user.save()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PASSWORD_RESET
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PASSWORD_RESET
def test_set_password_no_notify(self, user):
old_password = 'password'
diff --git a/scripts/disable_removed_beat_tasks.py b/scripts/disable_removed_beat_tasks.py
new file mode 100644
index 00000000000..9a455716e85
--- /dev/null
+++ b/scripts/disable_removed_beat_tasks.py
@@ -0,0 +1,24 @@
+from framework.celery_tasks import app as celery_app
+from website import settings
+from django.db.models import Q
+from django_celery_beat.models import PeriodicTask, PeriodicTasks
+
+
+@celery_app.task(name='scripts.disable_removed_beat_tasks')
+def disable_removed_beat_tasks():
+ """
+ Disable django-celery-beat PeriodicTask entries that no longer exist
+ and re-enable those that do exist, based on the current CeleryConfig.beat_schedule.
+ """
+
+ beat_schedule_keys = settings.CeleryConfig.beat_schedule.keys() if hasattr(settings.CeleryConfig, 'beat_schedule') else None
+ if not beat_schedule_keys:
+ return
+
+ desired_task_names = set(beat_schedule_keys) | {'celery.backend_cleanup'} # Built-in backend cleanup task
+
+ qs_disable = PeriodicTask.objects.filter(~Q(name__in=desired_task_names))
+ qs_disable.update(enabled=False)
+ qs_enable = PeriodicTask.objects.filter(name__in=desired_task_names)
+ qs_enable.update(enabled=True)
+ PeriodicTasks.update_changed() # Notify django-celery-beat of the changes
diff --git a/scripts/osfstorage/usage_audit.py b/scripts/osfstorage/usage_audit.py
index d3f555aab75..49003c56fdc 100644
--- a/scripts/osfstorage/usage_audit.py
+++ b/scripts/osfstorage/usage_audit.py
@@ -19,7 +19,7 @@
from framework.celery_tasks import app as celery_app
-from osf.models import TrashedFile, Node, NotificationType
+from osf.models import TrashedFile, Node, NotificationTypeEnum
from website.app import init_app
from website.settings.defaults import GBs
@@ -110,7 +110,7 @@ def main(send_email=False):
if lines:
if send_email:
logger.info('Sending email...')
- NotificationType.Type.EMPTY.instance.emit(
+ NotificationTypeEnum.EMPTY.instance.emit(
destination_address='support+scripts@osf.io',
event_context={
'body': '\n'.join(lines),
diff --git a/scripts/populate_notification_subscriptions.py b/scripts/populate_notification_subscriptions.py
deleted file mode 100644
index 557b9f2a47d..00000000000
--- a/scripts/populate_notification_subscriptions.py
+++ /dev/null
@@ -1,113 +0,0 @@
-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={
- '_is_digest': True,
- '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={
- '_is_digest': True,
- '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/scripts/remove_after_use/merge_notification_subscription_provider_ct.py b/scripts/remove_after_use/merge_notification_subscription_provider_ct.py
new file mode 100644
index 00000000000..50da93b2669
--- /dev/null
+++ b/scripts/remove_after_use/merge_notification_subscription_provider_ct.py
@@ -0,0 +1,31 @@
+import django
+django.setup()
+
+from website.app import init_app
+init_app(routes=False)
+
+from django.contrib.contenttypes.models import ContentType
+from framework.celery_tasks import app as celery_app
+from osf.models import NotificationSubscription
+
+
+@celery_app.task(name='scripts.remove_after_use.merge_notification_subscription_provider_ct')
+def merge_notification_subscription_provider_ct():
+
+ abstract_provider_ct = ContentType.objects.get_by_natural_key('osf', 'abstractprovider')
+
+ provider_ct_list = [
+ ContentType.objects.get_by_natural_key('osf', 'preprintprovider'),
+ ContentType.objects.get_by_natural_key('osf', 'registrationprovider'),
+ ContentType.objects.get_by_natural_key('osf', 'collectionprovider'),
+ ]
+ subscriptions = NotificationSubscription.objects.filter(
+ content_type__in=provider_ct_list
+ )
+ subscriptions.update(
+ content_type=abstract_provider_ct
+ )
+
+
+if __name__ == '__main__':
+ merge_notification_subscription_provider_ct.delay()
diff --git a/scripts/remove_after_use/populate_notification_subscriptions_node_file_updated.py b/scripts/remove_after_use/populate_notification_subscriptions_node_file_updated.py
new file mode 100644
index 00000000000..61625ce6b1f
--- /dev/null
+++ b/scripts/remove_after_use/populate_notification_subscriptions_node_file_updated.py
@@ -0,0 +1,128 @@
+import django
+django.setup()
+
+from website.app import init_app
+init_app(routes=False)
+
+from datetime import datetime
+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, Coalesce
+from osf.models import Node, NotificationSubscription, NotificationTypeEnum
+
+
+@celery_app.task(name='scripts.remove_after_use.populate_notification_subscriptions_node_file_updated')
+def populate_notification_subscriptions_node_file_updated(batch_size: int = 1000):
+ print('---Starting NODE_FILE_UPDATED subscriptions population script----')
+ global_start = datetime.now()
+
+ node_file_nt = NotificationTypeEnum.NODE_FILE_UPDATED
+
+ node_ct = ContentType.objects.get_for_model(Node)
+
+ node_notifications_sq = (
+ NotificationSubscription.objects.filter(
+ content_type=node_ct,
+ notification_type=node_file_nt.instance,
+ object_id=Cast(OuterRef('pk'), CharField()),
+ ).values(
+ 'object_id'
+ ).annotate(
+ cnt=Count('id')
+ ).values('cnt')[:1]
+ )
+
+ nodes_qs = (
+ Node.objects
+ .filter(is_deleted=False)
+ .annotate(
+ contributors_count=Count('_contributors', distinct=True),
+ notifications_count=Coalesce(
+ Subquery(
+ node_notifications_sq,
+ output_field=IntegerField(),
+ ),
+ 0
+ ),
+ ).exclude(contributors_count=F('notifications_count'))
+ ).iterator(chunk_size=batch_size)
+
+ items_to_create = []
+ total_created = 0
+ batch_start = datetime.now()
+ count_nodes = 0
+ count_contributors = 0
+ for node in nodes_qs:
+ count_nodes += 1
+ for contributor in node.contributors.all():
+ count_contributors += 1
+ items_to_create.append(
+ NotificationSubscription(
+ notification_type=node_file_nt.instance,
+ user=contributor,
+ content_type=node_ct,
+ object_id=node.id,
+ _is_digest=True,
+ message_frequency='none',
+ )
+ )
+ if len(items_to_create) >= batch_size:
+ print(f'Creating batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ items_to_create = []
+ except Exception as exeption:
+ print(f"Error during bulk_create: {exeption}")
+ continue
+ finally:
+ items_to_create.clear()
+ batch_end = datetime.now()
+ print(f'Batch took {batch_end - batch_start}')
+
+ if count_contributors % batch_size == 0:
+ print(f'Processed {count_nodes} nodes with {count_contributors} contributors, created {total_created} subscriptions')
+ batch_start = datetime.now()
+
+ if items_to_create:
+ final_batch_start = datetime.now()
+ print(f'Creating final batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ except Exception as exeption:
+ print(f"Error during bulk_create: {exeption}")
+ final_batch_end = datetime.now()
+ print(f'Final batch took {final_batch_end - final_batch_start}')
+
+ global_end = datetime.now()
+ print(f'Total time for NODE_FILE_UPDATED subscription population: {global_end - global_start}')
+ print(f'Created {total_created} subscriptions.')
+ print('----Creation finished----')
+
+@celery_app.task(name='scripts.remove_after_use.update_notification_subscriptions_node_file_updated')
+def update_notification_subscriptions_node_file_updated():
+ print('---Starting NODE_FILE_UPDATED subscriptions update script----')
+
+ node_file_nt = NotificationTypeEnum.NODE_FILE_UPDATED
+
+ updated_start = datetime.now()
+ updated = (
+ NotificationSubscription.objects.filter(
+ notification_type__name=node_file_nt,
+ _is_digest=False,
+ )
+ .update(_is_digest=True)
+ )
+ updated_end = datetime.now()
+ print(f'Updated {updated} subscriptions. Took time: {updated_end - updated_start}')
+ print('Update finished.')
diff --git a/scripts/remove_after_use/populate_notification_subscriptions_user_global_file_updated.py b/scripts/remove_after_use/populate_notification_subscriptions_user_global_file_updated.py
new file mode 100644
index 00000000000..651143d6f8a
--- /dev/null
+++ b/scripts/remove_after_use/populate_notification_subscriptions_user_global_file_updated.py
@@ -0,0 +1,111 @@
+import django
+django.setup()
+
+from website.app import init_app
+init_app(routes=False)
+
+from django.utils import timezone
+from dateutil.relativedelta import relativedelta
+from datetime import datetime
+from framework.celery_tasks import app as celery_app
+from django.contrib.contenttypes.models import ContentType
+from osf.models import OSFUser, NotificationSubscription, NotificationTypeEnum
+
+@celery_app.task(name='scripts.remove_after_use.populate_notification_subscriptions_user_global_file_updated')
+def populate_notification_subscriptions_user_global_file_updated(per_last_years: int | None= None, batch_size: int = 1000):
+ print('---Starting USER_FILE_UPDATED subscriptions population script----')
+ global_start = datetime.now()
+
+ user_file_updated_nt = NotificationTypeEnum.USER_FILE_UPDATED
+ user_ct = ContentType.objects.get_for_model(OSFUser)
+ if per_last_years:
+ from_date = timezone.now() - relativedelta(years=per_last_years)
+ user_qs = (OSFUser.objects
+ .filter(date_last_login__gte=from_date)
+ .exclude(subscriptions__notification_type__name=user_file_updated_nt)
+ .distinct('id')
+ .order_by('id')
+ .iterator(chunk_size=batch_size)
+ )
+ else:
+ user_qs = (OSFUser.objects
+ .exclude(subscriptions__notification_type__name=user_file_updated_nt)
+ .distinct('id')
+ .order_by('id')
+ .iterator(chunk_size=batch_size)
+ )
+
+ items_to_create = []
+ total_created = 0
+
+ batch_start = datetime.now()
+ for count, user in enumerate(user_qs, 1):
+ items_to_create.append(
+ NotificationSubscription(
+ notification_type=user_file_updated_nt.instance,
+ user=user,
+ content_type=user_ct,
+ object_id=user.id,
+ _is_digest=True,
+ message_frequency='none',
+ )
+ )
+ if len(items_to_create) >= batch_size:
+ print(f'Creating batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ except Exception as e:
+ print(f'Error during bulk_create: {e}')
+ finally:
+ items_to_create.clear()
+ batch_end = datetime.now()
+ print(f'Batch took {batch_end - batch_start}')
+
+ if count % batch_size == 0:
+ print(f'Processed {count}, created {total_created}')
+ batch_start = datetime.now()
+
+ if items_to_create:
+ final_batch_start = datetime.now()
+ print(f'Creating final batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ except Exception as e:
+ print(f'Error during bulk_create: {e}')
+ final_batch_end = datetime.now()
+ print(f'Final batch took {final_batch_end - final_batch_start}')
+
+ global_end = datetime.now()
+ print(f'Total time for USER_FILE_UPDATED subscription population: {global_end - global_start}')
+ print(f'Created {total_created} subscriptions.')
+ print('----Creation finished----')
+
+@celery_app.task(name='scripts.remove_after_use.update_notification_subscriptions_user_global_file_updated')
+def update_notification_subscriptions_user_global_file_updated():
+ print('---Starting USER_FILE_UPDATED subscriptions updating script----')
+
+ user_file_updated_nt = NotificationTypeEnum.USER_FILE_UPDATED
+
+ update_start = datetime.now()
+ updated = (
+ NotificationSubscription.objects
+ .filter(
+ notification_type__name=user_file_updated_nt,
+ _is_digest=False,
+ )
+ .update(_is_digest=True)
+ )
+ update_end = datetime.now()
+
+ print(f'Updated {updated} subscriptions. Took time: {update_end - update_start}')
+ print('Update finished.')
diff --git a/scripts/remove_after_use/populate_notification_subscriptions_user_global_reviews.py b/scripts/remove_after_use/populate_notification_subscriptions_user_global_reviews.py
new file mode 100644
index 00000000000..edfca287d0d
--- /dev/null
+++ b/scripts/remove_after_use/populate_notification_subscriptions_user_global_reviews.py
@@ -0,0 +1,104 @@
+import django
+django.setup()
+
+from website.app import init_app
+init_app(routes=False)
+
+from django.utils import timezone
+from dateutil.relativedelta import relativedelta
+from datetime import datetime
+from framework.celery_tasks import app as celery_app
+from django.contrib.contenttypes.models import ContentType
+from osf.models import OSFUser, NotificationSubscription, NotificationTypeEnum
+
+
+@celery_app.task(name='scripts.remove_after_use.populate_notification_subscriptions_user_global_reviews')
+def populate_notification_subscriptions_user_global_reviews(per_last_years: int | None = None, batch_size: int = 1000):
+ print('---Starting REVIEWS_SUBMISSION_STATUS subscriptions population script----')
+ global_start = datetime.now()
+
+ review_nt = NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
+ user_ct = ContentType.objects.get_for_model(OSFUser)
+ if per_last_years:
+ from_date = timezone.now() - relativedelta(years=per_last_years)
+ user_qs = OSFUser.objects.filter(date_last_login__gte=from_date).exclude(
+ subscriptions__notification_type__name=review_nt.instance
+ ).distinct('id')
+ else:
+ user_qs = OSFUser.objects.exclude(
+ subscriptions__notification_type__name=review_nt.instance
+ ).distinct('id')
+
+ items_to_create = []
+ total_created = 0
+
+ batch_start = datetime.now()
+ for count, user in enumerate(user_qs, 1):
+ items_to_create.append(
+ NotificationSubscription(
+ notification_type=review_nt.instance,
+ user=user,
+ content_type=user_ct,
+ object_id=user.id,
+ _is_digest=True,
+ message_frequency='none',
+ )
+ )
+ if len(items_to_create) >= batch_size:
+ print(f'Creating batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ except Exception as e:
+ print(f'Error during bulk_create: {e}')
+ finally:
+ items_to_create.clear()
+ batch_end = datetime.now()
+ print(f'Batch took {batch_end - batch_start}')
+
+ if count % batch_size == 0:
+ print(f'Processed {count}, created {total_created}')
+ batch_start = datetime.now()
+
+ if items_to_create:
+ final_batch_start = datetime.now()
+ print(f'Creating final batch of {len(items_to_create)} subscriptions...')
+ try:
+ NotificationSubscription.objects.bulk_create(
+ items_to_create,
+ batch_size=batch_size,
+ ignore_conflicts=True,
+ )
+ total_created += len(items_to_create)
+ except Exception as e:
+ print(f'Error during bulk_create: {e}')
+ final_batch_end = datetime.now()
+ print(f'Final batch took {final_batch_end - final_batch_start}')
+
+ global_end = datetime.now()
+ print(f'Total time for REVIEWS_SUBMISSION_STATUS subscription population: {global_end - global_start}')
+ print(f'Created {total_created} subscriptions.')
+ print('----Creation finished----')
+
+@celery_app.task(name='scripts.remove_after_use.update_notification_subscriptions_user_global_reviews')
+def update_notification_subscriptions_user_global_reviews():
+ print('---Starting REVIEWS_SUBMISSION_STATUS subscriptions updating script----')
+
+ review_nt = NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS
+
+ updated_start = datetime.now()
+ updated = (
+ NotificationSubscription.objects.filter(
+ notification_type__name=review_nt,
+ _is_digest=False,
+ )
+ .update(_is_digest=True)
+ )
+ updated_end = datetime.now()
+
+ print(f'Updated {updated} subscriptions. Took time: {updated_end - updated_start}')
+ print('Update finished.')
diff --git a/scripts/stuck_registration_audit.py b/scripts/stuck_registration_audit.py
index 36eca5e52ab..b9cdb53c27c 100644
--- a/scripts/stuck_registration_audit.py
+++ b/scripts/stuck_registration_audit.py
@@ -14,7 +14,7 @@
from framework.auth import Auth
from framework.celery_tasks import app as celery_app
from osf.management.commands import force_archive as fa
-from osf.models import Registration, NotificationType
+from osf.models import Registration, NotificationTypeEnum
from website.settings import ADDONS_REQUESTED
from scripts import utils as scripts_utils
@@ -95,13 +95,12 @@ def main():
dict_writer.writeheader()
dict_writer.writerows(broken_registrations)
- NotificationType.Type.DESK_ARCHIVE_REGISTRATION_STUCK.instance.emit(
+ NotificationTypeEnum.DESK_ARCHIVE_REGISTRATION_STUCK.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
'broken_registrations_count': len(broken_registrations),
'attachment_name': filename,
'attachement_content': output.getvalue(),
- 'can_change_preferences': False
}
)
diff --git a/scripts/tests/test_deactivate_requested_accounts.py b/scripts/tests/test_deactivate_requested_accounts.py
index d2adf6f76fe..fdc6233b920 100644
--- a/scripts/tests/test_deactivate_requested_accounts.py
+++ b/scripts/tests/test_deactivate_requested_accounts.py
@@ -1,6 +1,6 @@
import pytest
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from osf_tests.factories import ProjectFactory, AuthUserFactory
from osf.management.commands.deactivate_requested_accounts import deactivate_requested_accounts
@@ -30,7 +30,7 @@ def test_deactivate_user_with_no_content(self, user_requested_deactivation):
with capture_notifications() as notifications:
deactivate_requested_accounts(dry_run=False)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_REQUEST_DEACTIVATION_COMPLETE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_REQUEST_DEACTIVATION_COMPLETE
user_requested_deactivation.reload()
assert user_requested_deactivation.requested_deactivation
@@ -42,7 +42,7 @@ def test_deactivate_user_with_content(self, user_requested_deactivation_with_nod
with capture_notifications() as notifications:
deactivate_requested_accounts(dry_run=False)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_REQUEST_DEACTIVATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_REQUEST_DEACTIVATION
user_requested_deactivation_with_node.reload()
assert user_requested_deactivation_with_node.requested_deactivation
diff --git a/scripts/triggered_mails.py b/scripts/triggered_mails.py
index 4b56d12c5df..389a82c257f 100644
--- a/scripts/triggered_mails.py
+++ b/scripts/triggered_mails.py
@@ -2,11 +2,11 @@
import uuid
from django.db import transaction
-from django.db.models import Q, Exists, OuterRef
+from django.db.models import Q, F
from django.utils import timezone
from framework.celery_tasks import app as celery_app
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationTypeEnum
from website.app import init_app
from website import settings
from osf import features
@@ -60,31 +60,38 @@ def find_inactive_users_without_enqueued_or_sent_no_login():
Match your original inactivity rules, but exclude users who already have a no_login EmailTask
either pending, started, retrying, or already sent successfully.
"""
+ now = timezone.now()
- # Subquery: Is there already a not-yet-failed/aborted EmailTask for this user with our prefix?
- existing_no_login = EmailTask.objects.filter(
- user_id=OuterRef('pk'),
- task_id__startswith=NO_LOGIN_PREFIX,
- status__in=['PENDING', 'STARTED', 'RETRY', 'SUCCESS'],
- )
cutoff_query = Q(date_last_login__gte=settings.NO_LOGIN_EMAIL_CUTOFF - settings.NO_LOGIN_WAIT_TIME) if settings.NO_LOGIN_EMAIL_CUTOFF else Q()
base_q = OSFUser.objects.filter(
cutoff_query,
is_active=True,
).filter(
Q(
- date_last_login__lt=timezone.now() - settings.NO_LOGIN_WAIT_TIME,
+ date_last_login__lt=now - settings.NO_LOGIN_WAIT_TIME,
# NOT tagged osf4m
) & ~Q(tags__name='osf4m')
|
Q(
- date_last_login__lt=timezone.now() - settings.NO_LOGIN_OSF4M_WAIT_TIME,
+ date_last_login__lt=now - settings.NO_LOGIN_OSF4M_WAIT_TIME,
tags__name='osf4m'
)
).distinct()
- # Exclude users who already have a task for this email type
- return base_q.annotate(_has_task=Exists(existing_no_login)).filter(_has_task=False)
+ # Exclude users who have already received a no-login email recently
+ base_q = base_q.filter(
+ Q(no_login_email_last_sent__isnull=True) |
+ (
+ Q(no_login_email_last_sent__lt=now - settings.NO_LOGIN_WAIT_TIME) &
+ Q(no_login_email_last_sent__lt=F('date_last_login'))
+ )
+ )
+ # Exlude users who already have a pending/started/retrying EmailTask for no-login
+ base_q = base_q.exclude(
+ emailtask__task_id__startswith=NO_LOGIN_PREFIX,
+ emailtask__status__in=['PENDING', 'STARTED', 'RETRY']
+ )
+ return base_q
@celery_app.task(name='scripts.triggered_no_login_email')
@@ -124,7 +131,7 @@ def send_no_login_email(email_task_id: int):
email_task.save()
logger.warning(f'EmailTask {email_task.id}: user {user.id} is not active')
return
- NotificationType.Type.USER_NO_LOGIN.instance.emit(
+ NotificationTypeEnum.USER_NO_LOGIN.instance.emit(
user=user,
event_context={
'user_fullname': user.fullname,
@@ -133,6 +140,8 @@ def send_no_login_email(email_task_id: int):
)
email_task.status = 'SUCCESS'
email_task.save()
+ user.no_login_email_last_sent = timezone.now()
+ user.save()
except Exception as exc: # noqa: BLE001
logger.exception(f'EmailTask {email_task.id}: error while sending')
diff --git a/tests/test_add_contributiors_subscriptions.py b/tests/test_add_contributiors_subscriptions.py
index 93460b71e8c..123c1f28fd6 100644
--- a/tests/test_add_contributiors_subscriptions.py
+++ b/tests/test_add_contributiors_subscriptions.py
@@ -1,6 +1,6 @@
import pytest
from framework.auth import Auth
-from osf.models import NotificationType, NotificationSubscription
+from osf.models import NotificationTypeEnum, NotificationSubscription
from osf_tests.factories import ProjectFactory, UserFactory
from tests.utils import capture_notifications
from framework.auth import register_unconfirmed
@@ -39,7 +39,7 @@ def test_only_one_subscription_for_registered_user(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.NODE_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.NODE_FILE_UPDATED
subs = NotificationSubscription.objects.filter(
user=user,
@@ -51,7 +51,7 @@ def test_only_one_subscription_for_registered_user(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.USER_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.USER_FILE_UPDATED
def test_only_one_subscription_for_unregistered_user(self):
"""Adding the same unregistered contributor multiple times creates only one subscription."""
@@ -91,7 +91,7 @@ def test_only_one_subscription_for_unregistered_user(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.NODE_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.NODE_FILE_UPDATED
subs = NotificationSubscription.objects.filter(
user=unreg_user,
@@ -103,7 +103,7 @@ def test_only_one_subscription_for_unregistered_user(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.USER_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.USER_FILE_UPDATED
def test_only_one_subscription_for_creator(self):
"""Ensure the project creator only has one NotificationSubscription for their own node."""
@@ -134,7 +134,7 @@ def test_only_one_subscription_for_creator(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.NODE_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.NODE_FILE_UPDATED
subs = NotificationSubscription.objects.filter(
user=creator,
@@ -146,7 +146,7 @@ def test_only_one_subscription_for_creator(self):
f"found {subs.count()}"
)
sub = subs.first()
- assert sub.notification_type.name == NotificationType.Type.USER_FILE_UPDATED
+ assert sub.notification_type.name == NotificationTypeEnum.USER_FILE_UPDATED
def test_unregistered_contributor_then_registered_user_only_one_subscription(self):
"""When an unregistered contributor later registers, their subscriptions merge correctly."""
@@ -178,7 +178,7 @@ def test_unregistered_contributor_then_registered_user_only_one_subscription(sel
assert subs_node.count() == 1, (
f"Expected one NODE_FILE_UPDATED subscription after registration, found {subs_node.count()}"
)
- assert subs_node.first().notification_type.name == NotificationType.Type.NODE_FILE_UPDATED
+ assert subs_node.first().notification_type.name == NotificationTypeEnum.NODE_FILE_UPDATED
subs_user = NotificationSubscription.objects.filter(
user=registered_user,
@@ -188,7 +188,7 @@ def test_unregistered_contributor_then_registered_user_only_one_subscription(sel
assert subs_user.count() == 1, (
f"Expected one USER_FILE_UPDATED subscription after registration, found {subs_user.count()}"
)
- assert subs_user.first().notification_type.name == NotificationType.Type.USER_FILE_UPDATED
+ assert subs_user.first().notification_type.name == NotificationTypeEnum.USER_FILE_UPDATED
def test_contributor_removed_then_readded_only_one_subscription(self):
"""Removing a contributor and re-adding them should not duplicate subscriptions."""
@@ -216,7 +216,7 @@ def test_contributor_removed_then_readded_only_one_subscription(self):
assert subs_node.count() == 1, (
f"Expected one NODE_FILE_UPDATED subscription after re-adding, found {subs_node.count()}"
)
- assert subs_node.first().notification_type.name == NotificationType.Type.NODE_FILE_UPDATED
+ assert subs_node.first().notification_type.name == NotificationTypeEnum.NODE_FILE_UPDATED
subs_user = NotificationSubscription.objects.filter(
user=user,
@@ -226,4 +226,4 @@ def test_contributor_removed_then_readded_only_one_subscription(self):
assert subs_user.count() == 1, (
f"Expected one USER_FILE_UPDATED subscription after re-adding, found {subs_user.count()}"
)
- assert subs_user.first().notification_type.name == NotificationType.Type.USER_FILE_UPDATED
+ assert subs_user.first().notification_type.name == NotificationTypeEnum.USER_FILE_UPDATED
diff --git a/tests/test_adding_contributor_views.py b/tests/test_adding_contributor_views.py
index 26207af9360..6dabbdd17cc 100644
--- a/tests/test_adding_contributor_views.py
+++ b/tests/test_adding_contributor_views.py
@@ -8,7 +8,7 @@
from framework import auth
from framework.auth import Auth
from framework.exceptions import HTTPError
-from osf.models import NodeRelation, NotificationType
+from osf.models import NodeRelation, NotificationTypeEnum
from osf.utils import permissions
from osf_tests.factories import (
fake_email,
@@ -266,7 +266,7 @@ def test_email_sent_when_reg_user_is_added(self):
project.add_contributors(contributors, auth=self.auth, notification_type=None)
project.save()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
def test_contributor_added_email_sent_to_unreg_user(self):
unreg_user = UnregUserFactory()
@@ -305,17 +305,17 @@ def test_notify_contributor_email_does_not_send_before_throttle_expires(self):
notify_added_contributor(
project,
contributor,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT,
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT,
auth=auth
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
# 2nd call does not send email because throttle period has not expired
notify_added_contributor(
project,
contributor,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT,
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT,
auth=auth
)
@@ -327,23 +327,23 @@ def test_notify_contributor_email_sends_after_throttle_expires(self):
notify_added_contributor(
project,
contributor,
- NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT,
+ NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT,
auth,
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
time.sleep(2) # throttle period expires
with capture_notifications() as notifications:
notify_added_contributor(
project,
contributor,
- NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT,
+ NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT,
auth,
throttle=1
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
def test_add_contributor_to_fork_sends_email(self):
contributor = UserFactory()
@@ -352,7 +352,7 @@ def test_add_contributor_to_fork_sends_email(self):
fork.add_contributor(contributor, auth=Auth(self.creator))
fork.save()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
def test_add_contributor_to_template_sends_email(self):
contributor = UserFactory()
@@ -361,11 +361,11 @@ def test_add_contributor_to_template_sends_email(self):
template.add_contributor(
contributor,
auth=Auth(self.creator),
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
)
template.save()
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_ACCESS_REQUEST
def test_creating_fork_does_not_email_creator(self):
with capture_notifications():
@@ -512,7 +512,7 @@ def test_send_claim_email_to_given_email(self):
with capture_notifications() as notifications:
send_claim_email(email=given_email, unclaimed_user=unreg_user, node=project)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
def test_send_claim_email_to_referrer(self):
project = ProjectFactory()
@@ -528,8 +528,8 @@ def test_send_claim_email_to_referrer(self):
send_claim_email(email=real_email, unclaimed_user=unreg_user, node=project)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_FORWARD_INVITE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE
def test_send_claim_email_before_throttle_expires(self):
project = ProjectFactory()
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 2ea8cedefbf..07e19d7a706 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -24,7 +24,7 @@
from framework.auth import Auth
from framework.auth.decorators import must_be_logged_in
from framework.sessions import get_session
-from osf.models import OSFUser, NotificationType
+from osf.models import OSFUser, NotificationTypeEnum
from osf.utils import permissions
from tests.utils import capture_notifications
from website import settings
@@ -165,7 +165,7 @@ def test_password_change_sends_email(self):
user.set_password('killerqueen')
user.save()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PASSWORD_RESET
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PASSWORD_RESET
@mock.patch('framework.auth.utils.requests.post')
def test_validate_recaptcha_success(self, req_post):
@@ -210,13 +210,13 @@ def test_sign_up_twice_sends_two_confirmation_emails_only(self):
with capture_notifications() as notifications:
self.app.post(url, json=sign_up_data)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_NO_ADDON
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_INITIAL_CONFIRM_EMAIL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_NO_ADDON
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_INITIAL_CONFIRM_EMAIL
with capture_notifications() as notifications:
self.app.post(url, json=sign_up_data)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INITIAL_CONFIRM_EMAIL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INITIAL_CONFIRM_EMAIL
class TestAuthObject(OsfTestCase):
diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py
index 8e8cc5fafb1..608edc06519 100644
--- a/tests/test_auth_views.py
+++ b/tests/test_auth_views.py
@@ -25,7 +25,7 @@
)
from framework.auth.exceptions import InvalidTokenError
from framework.auth.views import login_and_register_handler
-from osf.models import OSFUser, NotableDomain, NotificationType
+from osf.models import OSFUser, NotableDomain, NotificationTypeEnum
from osf_tests.factories import (
fake_email,
AuthUserFactory,
@@ -326,7 +326,7 @@ def test_resend_confirmation(self):
self.app.put(url, json={'id': self.user._id, 'email': header}, auth=self.user.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_CONFIRM_EMAIL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_CONFIRM_EMAIL
self.user.reload()
assert token != self.user.get_confirmation_token(email)
@@ -505,7 +505,7 @@ def test_resend_confirmation_does_not_send_before_throttle_expires(self):
with capture_notifications() as notifications:
self.app.put(url, json={'id': self.user._id, 'email': header}, auth=self.user.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_CONFIRM_EMAIL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_CONFIRM_EMAIL
# 2nd call does not send email because throttle period has not expired
res = self.app.put(url, json={'id': self.user._id, 'email': header}, auth=self.user.auth)
assert res.status_code == 400
diff --git a/tests/test_claim_views.py b/tests/test_claim_views.py
index af3038c537c..031eaa6ac70 100644
--- a/tests/test_claim_views.py
+++ b/tests/test_claim_views.py
@@ -10,7 +10,7 @@
from framework.flask import redirect
from osf.models import (
OSFUser,
- Tag, NotificationType,
+ Tag, NotificationTypeEnum,
)
from osf_tests.factories import (
fake_email,
@@ -98,8 +98,8 @@ def test_claim_user_already_registered_redirects_to_claim_user_registered(self):
}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_FORWARD_INVITE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE
# set unregistered record email since we are mocking send_claim_email()
unclaimed_record = unregistered_user.get_unclaimed_record(self.project._primary_key)
@@ -147,8 +147,8 @@ def test_claim_user_already_registered_secondary_email_redirects_to_claim_user_r
}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_FORWARD_INVITE
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE
# set unregistered record email since we are mocking send_claim_email()
unclaimed_record = unregistered_user.get_unclaimed_record(self.project._primary_key)
@@ -236,8 +236,8 @@ def test_send_claim_registered_email_before_throttle_expires(self):
node=self.project,
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORWARD_INVITE_REGISTERED
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION_REGISTERED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION_REGISTERED
# second call raises error because it was called before throttle period
with pytest.raises(HTTPError):
send_claim_registered_email(
@@ -433,7 +433,7 @@ def test_claim_user_post_returns_fullname(self):
)
assert res.json['fullname'] == self.given_name
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INVITE_DEFAULT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INVITE_DEFAULT
def test_claim_user_post_if_email_is_different_from_given_email(self):
email = fake_email() # email that is different from the one the referrer gave
@@ -446,9 +446,9 @@ def test_claim_user_post_if_email_is_different_from_given_email(self):
}
)
assert len(notifications['emits']) == 2
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_PENDING_VERIFICATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_PENDING_VERIFICATION
assert notifications['emits'][0]['kwargs']['user'].username == self.given_email
- assert notifications['emits'][1]['type'] == NotificationType.Type.USER_FORWARD_INVITE
+ assert notifications['emits'][1]['type'] == NotificationTypeEnum.USER_FORWARD_INVITE
assert notifications['emits'][1]['kwargs']['destination_address'] == email
def test_claim_url_with_bad_token_returns_400(self):
diff --git a/tests/test_events.py b/tests/test_events.py
index fa79515e021..b73e55882ba 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -2,7 +2,7 @@
from django.contrib.contenttypes.models import ContentType
-from osf.models import NotificationType
+from osf.models import NotificationType, NotificationTypeEnum
from tests.utils import capture_notifications
from notifications.file_event_notifications import (
event_registry,
@@ -121,7 +121,7 @@ def setUp(self):
self.sub = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.sub.save()
self.event = event_registry['file_updated'](self.user_2, self.project, 'file_updated', payload=file_payload)
@@ -135,7 +135,7 @@ def test_file_updated(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.FILE_UPDATED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.FILE_UPDATED
class TestFileAdded(OsfTestCase):
@@ -147,7 +147,7 @@ def setUp(self):
self.project_subscription = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.project_subscription.save()
self.user2 = factories.UserFactory()
@@ -162,7 +162,7 @@ def test_file_added(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.FILE_ADDED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.FILE_ADDED
class TestFileRemoved(OsfTestCase):
@@ -174,7 +174,7 @@ def setUp(self):
self.project_subscription = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_REMOVED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_REMOVED)
)
self.project_subscription.object_id = self.project.id
self.project_subscription.content_type = ContentType.objects.get_for_model(self.project)
@@ -185,12 +185,12 @@ def setUp(self):
)
def test_info_formed_correct_file(self):
- assert NotificationType.Type.FILE_UPDATED == self.event.event_type
+ assert NotificationTypeEnum.FILE_UPDATED == self.event.event_type
assert f'removed file "{materialized.lstrip("/")}".' == self.event.html_message
assert f'removed file "{materialized.lstrip("/")}".' == self.event.text_message
def test_info_formed_correct_folder(self):
- assert NotificationType.Type.FILE_UPDATED == self.event.event_type
+ assert NotificationTypeEnum.FILE_UPDATED == self.event.event_type
self.event.payload['metadata']['materialized'] += '/'
assert f'removed folder "{materialized.lstrip("/")}/".' == self.event.html_message
assert f'removed folder "{materialized.lstrip("/")}/".' == self.event.text_message
@@ -199,7 +199,7 @@ def test_file_removed(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.FILE_REMOVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.FILE_REMOVED
class TestFolderCreated(OsfTestCase):
@@ -210,7 +210,7 @@ def setUp(self):
self.project = factories.ProjectFactory()
self.project_subscription = factories.NotificationSubscriptionFactory(
user=self.user,
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED),
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED),
)
self.project_subscription.save()
self.user2 = factories.UserFactory()
@@ -219,7 +219,7 @@ def setUp(self):
)
def test_info_formed_correct(self):
- assert NotificationType.Type.FILE_UPDATED == self.event.event_type
+ assert NotificationTypeEnum.FILE_UPDATED == self.event.event_type
assert 'created folder "Three/".' == self.event.html_message
assert 'created folder "Three/".' == self.event.text_message
@@ -227,7 +227,7 @@ def test_folder_added(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.FOLDER_CREATED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.FOLDER_CREATED
class TestFolderFileRenamed(OsfTestCase):
@@ -242,7 +242,7 @@ def setUp(self):
user=self.user_2,
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.USER_FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.USER_FILE_UPDATED)
)
self.sub.save()
@@ -295,21 +295,21 @@ def setUp(self):
self.sub = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.sub.save()
# for private node
self.private_sub = factories.NotificationSubscriptionFactory(
object_id=self.private_node.id,
content_type=ContentType.objects.get_for_model(self.private_node),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.private_sub.save()
# for file subscription
self.file_sub = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.NODE_FILES_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.NODE_FILE_UPDATED)
)
self.file_sub.save()
@@ -327,7 +327,7 @@ def test_user_performing_action_no_email(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_MOVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_MOVED
assert notifications['emits'][0]['kwargs']['user'] == self.user_2
def test_perform_store_called_once(self):
@@ -336,7 +336,7 @@ def test_perform_store_called_once(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_MOVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_MOVED
def test_perform_store_one_of_each(self):
# Move Event: Tests that store_emails is called 3 times, one in
@@ -357,7 +357,7 @@ def test_perform_store_one_of_each(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_MOVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_MOVED
def test_remove_user_sent_once(self):
# Move Event: Tests removed user is removed once. Regression
@@ -368,7 +368,7 @@ def test_remove_user_sent_once(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_MOVED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_MOVED
class TestFileCopied(OsfTestCase):
@@ -393,21 +393,21 @@ def setUp(self):
self.sub = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.sub.save()
# for private node
self.private_sub = factories.NotificationSubscriptionFactory(
object_id=self.private_node.id,
content_type=ContentType.objects.get_for_model(self.private_node),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.FILE_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.FILE_UPDATED)
)
self.private_sub.save()
# for file subscription
self.file_sub = factories.NotificationSubscriptionFactory(
object_id=self.project.id,
content_type=ContentType.objects.get_for_model(self.project),
- notification_type=NotificationType.objects.get(name=NotificationType.Type.NODE_FILES_UPDATED)
+ notification_type=NotificationType.objects.get(name=NotificationTypeEnum.NODE_FILE_UPDATED)
)
self.file_sub.save()
@@ -439,7 +439,7 @@ def test_copied_one_of_each(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_COPIED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_COPIED
def test_user_performing_action_no_email(self):
# Move Event: Makes sure user who performed the action is not
@@ -449,7 +449,7 @@ def test_user_performing_action_no_email(self):
with capture_notifications() as notifications:
self.event.perform()
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.ADDON_FILE_COPIED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.ADDON_FILE_COPIED
class TestSubscriptionManipulations(OsfTestCase):
diff --git a/tests/test_forgot_password.py b/tests/test_forgot_password.py
index 935b641d120..3e8bacaf1a1 100644
--- a/tests/test_forgot_password.py
+++ b/tests/test_forgot_password.py
@@ -1,6 +1,6 @@
from urllib.parse import quote_plus
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from tests.base import OsfTestCase
from osf_tests.factories import (
AuthUserFactory,
@@ -50,7 +50,7 @@ def test_can_receive_reset_password_email(self):
res = form.submit(self.app)
# check mail was sent
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORGOT_PASSWORD
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORGOT_PASSWORD
# check http 200 response
assert res.status_code == 200
# check request URL is /forgotpassword
@@ -152,7 +152,7 @@ def test_can_receive_reset_password_email(self):
# check mail was sent
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORGOT_PASSWORD_INSTITUTION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_FORGOT_PASSWORD_INSTITUTION
# check http 200 response
assert res.status_code == 200
# check request URL is /forgotpassword
diff --git a/tests/test_misc_views.py b/tests/test_misc_views.py
index 56c804f794f..3d30905ab31 100644
--- a/tests/test_misc_views.py
+++ b/tests/test_misc_views.py
@@ -21,7 +21,7 @@
Comment,
OSFUser,
SpamStatus,
- NodeRelation, NotificationType,
+ NodeRelation, NotificationTypeEnum,
)
from osf.utils import permissions
from osf_tests.factories import (
@@ -423,7 +423,7 @@ def test_external_login_confirm_email_get_link(self):
with capture_notifications() as notifications:
res = self.app.get(url)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_EXTERNAL_LOGIN_LINK_SUCCESS
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_EXTERNAL_LOGIN_LINK_SUCCESS
assert res.status_code == 302, 'redirects to cas login'
assert 'You should be redirected automatically' in str(res.html)
assert '/login?service=' in res.location
diff --git a/tests/test_preprints.py b/tests/test_preprints.py
index 205d356c65b..e14d8920487 100644
--- a/tests/test_preprints.py
+++ b/tests/test_preprints.py
@@ -26,7 +26,7 @@
from addons.base import views
from admin_tests.utilities import setup_view
from api.preprints.views import PreprintContributorDetail
-from osf.models import Tag, Preprint, PreprintLog, PreprintContributor, NotificationType
+from osf.models import Tag, Preprint, PreprintLog, PreprintContributor, NotificationTypeEnum
from osf.exceptions import PreprintStateError, ValidationError, ValidationValueError
from osf_tests.factories import (
ProjectFactory,
@@ -2014,12 +2014,12 @@ def test_creator_gets_email(self):
with capture_notifications() as notifications:
self.preprint.set_published(True, auth=Auth(self.user), save=True)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
with capture_notifications() as notifications:
self.preprint_branded.set_published(True, auth=Auth(self.user), save=True)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION
class TestPreprintOsfStorage(OsfTestCase):
def setUp(self):
diff --git a/tests/test_registrations/test_retractions.py b/tests/test_registrations/test_retractions.py
index 520a21a809a..481f4be7420 100644
--- a/tests/test_registrations/test_retractions.py
+++ b/tests/test_registrations/test_retractions.py
@@ -22,7 +22,7 @@
InvalidSanctionApprovalToken, InvalidSanctionRejectionToken,
NodeStateError,
)
-from osf.models import Contributor, Retraction, NotificationType
+from osf.models import Contributor, Retraction, NotificationTypeEnum
from osf.utils import permissions
from tests.utils import capture_notifications
@@ -804,7 +804,7 @@ def test_POST_retraction_does_not_send_email_to_unregistered_admins(self):
auth=self.user.auth,
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_PENDING_RETRACTION_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_PENDING_RETRACTION_ADMIN
def test_POST_pending_embargo_returns_HTTPError_HTTPOK(self):
self.registration.embargo_registration(
@@ -900,7 +900,7 @@ def test_valid_POST_calls_send_mail_with_username(self):
auth=self.user.auth,
)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.NODE_PENDING_RETRACTION_ADMIN
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.NODE_PENDING_RETRACTION_ADMIN
def test_non_contributor_GET_approval_returns_HTTPError_FORBIDDEN(self):
non_contributor = AuthUserFactory()
diff --git a/tests/test_resend_confirmation.py b/tests/test_resend_confirmation.py
index 95c1d1d431d..9b1fcdf74ce 100644
--- a/tests/test_resend_confirmation.py
+++ b/tests/test_resend_confirmation.py
@@ -1,4 +1,4 @@
-from osf.models import NotificationType
+from osf.models import NotificationTypeEnum
from tests.base import OsfTestCase
from osf_tests.factories import (
UserFactory,
@@ -34,7 +34,7 @@ def test_can_receive_resend_confirmation_email(self):
res = form.submit(self.app)
# check email, request and response
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_INITIAL_CONFIRM_EMAIL
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_INITIAL_CONFIRM_EMAIL
assert res.status_code == 200
assert res.request.path == self.post_url
diff --git a/tests/test_spam_mixin.py b/tests/test_spam_mixin.py
index 91dc6387181..ca3e25a9cda 100644
--- a/tests/test_spam_mixin.py
+++ b/tests/test_spam_mixin.py
@@ -10,7 +10,7 @@
from tests.base import DbTestCase
from osf_tests.factories import UserFactory, CommentFactory, ProjectFactory, PreprintFactory, RegistrationFactory, AuthUserFactory
-from osf.models import NotableDomain, SpamStatus, NotificationType
+from osf.models import NotableDomain, SpamStatus, NotificationTypeEnum
from tests.utils import capture_notifications
from website import settings
@@ -27,7 +27,7 @@ def test_throttled_autoban():
proj.save()
projects.append(proj)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.USER_SPAM_BANNED
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.USER_SPAM_BANNED
user.reload()
assert user.is_disabled
for project in projects:
diff --git a/tests/test_triggered_mails.py b/tests/test_triggered_mails.py
index c482338ccff..33562878f8a 100644
--- a/tests/test_triggered_mails.py
+++ b/tests/test_triggered_mails.py
@@ -3,14 +3,14 @@
from unittest import mock
from waffle.testutils import override_switch
from osf import features
-
+from website import settings
from django.utils import timezone
from tests.base import OsfTestCase
from tests.utils import run_celery_tasks, capture_notifications
from osf_tests.factories import UserFactory
-from osf.models import EmailTask, NotificationType
+from osf.models import EmailTask, NotificationTypeEnum
from scripts.triggered_mails import (
find_inactive_users_without_enqueued_or_sent_no_login,
@@ -22,7 +22,7 @@
def _inactive_time():
"""Make a timestamp that is definitely 'inactive' regardless of threshold settings."""
# Very conservative: 12 weeks ago
- return timezone.now() - timedelta(weeks=12)
+ return timezone.now() - settings.NO_LOGIN_WAIT_TIME
def _recent_time():
@@ -87,7 +87,7 @@ def test_trigger_no_login_mail_failure_marks_task_failure(self):
# Force the emit call to raise to exercise failure branch
with mock.patch.object(
- NotificationType.Type.USER_NO_LOGIN.instance,
+ NotificationTypeEnum.USER_NO_LOGIN.instance,
'emit',
side_effect=RuntimeError('kaboom'),
), run_celery_tasks():
@@ -114,25 +114,56 @@ def test_finder_returns_two_inactive_when_none_queued(self):
assert ids == {u1.id, u2.id}
def test_finder_excludes_users_with_existing_task(self):
- """Inactive users but one already has a no_login EmailTask -> excluded."""
+ """Inactive users but one already has a no_login_email_last_sent -> excluded."""
u1 = UserFactory(fullname='Jalen Hurts')
u2 = UserFactory(fullname='Jason Kelece')
u1.date_last_login = _inactive_time()
u2.date_last_login = _inactive_time()
- u1.save()
- u2.save()
-
- # Pretend u2 already had this email flow (SUCCESS qualifies for exclusion)
EmailTask.objects.create(
- task_id=f"{NO_LOGIN_PREFIX}existing-success",
- user=u2,
- status='SUCCESS',
+ user=u2, task_id=f'{NO_LOGIN_PREFIX}uuid4', status='PENDING',
)
+ u1.save()
+ u2.save()
users = list(find_inactive_users_without_enqueued_or_sent_no_login())
ids = {u.id for u in users}
assert ids == {u1.id} # u2 excluded because of existing task
+ def test_finder_excludes_users_with_recent_no_login_email(self):
+ """Inactive users but one already has a no_login_email_last_sent -> excluded."""
+ u1 = UserFactory(fullname='Jalen Hurts')
+ u2 = UserFactory(fullname='Jason Kelece')
+ u1.date_last_login = _inactive_time()
+ u2.date_last_login = _inactive_time()
+ u2.no_login_email_last_sent = timezone.now()
+ u1.save()
+ u2.save()
+
+ users = list(find_inactive_users_without_enqueued_or_sent_no_login())
+ ids = {u.id for u in users}
+ assert ids == {u1.id} # u2 excluded because of recent email
+
+ def test_finder_excludes_users_logged_in_before_no_login_email_last_sent(self):
+ """Inactive users but one already has a no_login_email_last_sent -> excluded."""
+ u1 = UserFactory(fullname='Jalen Hurts')
+ u2 = UserFactory(fullname='Jason Kelece')
+ u1.date_last_login = _inactive_time()
+ u2.date_last_login = _inactive_time()
+ u2.no_login_email_last_sent = timezone.now() - settings.NO_LOGIN_WAIT_TIME # old enough to be eligible if not for the login
+ u1.save()
+ u2.save()
+
+ users = list(find_inactive_users_without_enqueued_or_sent_no_login())
+ ids = {u.id for u in users}
+ assert ids == {u1.id} # u2 excluded because last login was before email was sent
+
+ u2.date_last_login = timezone.now() - settings.NO_LOGIN_WAIT_TIME
+ u2.save()
+
+ users = list(find_inactive_users_without_enqueued_or_sent_no_login())
+ ids = {u.id for u in users}
+ assert ids == {u1.id, u2.id} # u2 included again because they logged in after the email was sent
+
@override_switch(features.ENABLE_NO_LOGIN_EMAILS, active=False)
def test_disable_task(self):
u1 = UserFactory(fullname='Jalen Hurts')
diff --git a/tests/test_user_profile_view.py b/tests/test_user_profile_view.py
index fd5b424f4b8..8fa505df378 100644
--- a/tests/test_user_profile_view.py
+++ b/tests/test_user_profile_view.py
@@ -9,7 +9,7 @@
from addons.github.tests.factories import GitHubAccountFactory
from framework.celery_tasks import handlers
from osf.external.spam import tasks as spam_tasks
-from osf.models import NotableDomain, NotificationType
+from osf.models import NotableDomain, NotificationTypeEnum
from osf_tests.factories import (
fake_email,
ApiOAuth2ApplicationFactory,
@@ -728,7 +728,7 @@ def test_user_cannot_request_account_export_before_throttle_expires(self):
with capture_notifications() as notifications:
self.app.post(url, auth=self.user.auth)
assert len(notifications['emits']) == 1
- assert notifications['emits'][0]['type'] == NotificationType.Type.DESK_REQUEST_EXPORT
+ assert notifications['emits'][0]['type'] == NotificationTypeEnum.DESK_REQUEST_EXPORT
res = self.app.post(url, auth=self.user.auth)
assert res.status_code == 400
diff --git a/tests/utils.py b/tests/utils.py
index 9e6e3db07cd..9f0aba2f4cf 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -545,7 +545,7 @@ def _safe_obj_id(obj: Any) -> Optional[str]:
@contextlib.contextmanager
def assert_notification(
*,
- type, # NotificationType, NotificationType.Type, or str
+ type, # NotificationType, NotificationTypeEnum, or str
user: Any = None, # optional user object to match
subscribed_object: Any = None, # optional object (e.g., node) to match
times: int = 1, # exact number of emits expected
@@ -555,7 +555,7 @@ def assert_notification(
):
"""
Usage:
- with assert_notification(type=NotificationType.Type.NODE_FORK_COMPLETED, user=self.user):
+ with assert_notification(type=NotificationTypeEnum.NODE_FORK_COMPLETED, user=self.user):
"""
expected_type = _notif_type_name(type)
diff --git a/website/archiver/utils.py b/website/archiver/utils.py
index 0eafd0460b3..f124388423c 100644
--- a/website/archiver/utils.py
+++ b/website/archiver/utils.py
@@ -27,44 +27,37 @@ def normalize_unicode_filenames(filename):
def send_archiver_size_exceeded_mails(src, user, stat_result, url):
- from osf.models.notification_type import NotificationType
+ from osf.models.notification_type import NotificationTypeEnum
- NotificationType.Type.DESK_ARCHIVE_JOB_EXCEEDED.instance.emit(
+ NotificationTypeEnum.DESK_ARCHIVE_JOB_EXCEEDED.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
subscribed_object=src,
event_context={
'user_fullname': user.fullname,
'user__id': user._id,
- 'src__id': src._id,
'src_url': src.url,
'src_title': src.title,
'stat_result': stat_result,
'url': url,
'max_archive_size': MAX_ARCHIVE_SIZE / 1024 ** 3,
- 'can_change_preferences': False,
}
)
- NotificationType.Type.USER_ARCHIVE_JOB_EXCEEDED.instance.emit(
+ NotificationTypeEnum.USER_ARCHIVE_JOB_EXCEEDED.instance.emit(
user=user,
subscribed_object=user,
event_context={
- 'user_fullname': user.fullname,
- 'user__id': user._id,
'src_title': src.title,
'src_url': src.url,
- 'max_archive_size': MAX_ARCHIVE_SIZE / 1024 ** 3,
- 'can_change_preferences': False,
}
)
def send_archiver_copy_error_mails(src, user, results, url):
- from osf.models.notification_type import NotificationType
+ from osf.models.notification_type import NotificationTypeEnum
- NotificationType.Type.DESK_ARCHIVE_JOB_COPY_ERROR.instance.emit(
+ NotificationTypeEnum.DESK_ARCHIVE_JOB_COPY_ERROR.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
- 'domain': settings.DOMAIN,
'user_fullname': user.fullname,
'user__id': user._id,
'src__id': src._id,
@@ -72,37 +65,34 @@ def send_archiver_copy_error_mails(src, user, results, url):
'src_title': src.title,
'results': results,
'url': url,
- 'can_change_preferences': False,
}
)
- NotificationType.Type.USER_ARCHIVE_JOB_COPY_ERROR.instance.emit(
+ NotificationTypeEnum.USER_ARCHIVE_JOB_COPY_ERROR.instance.emit(
user=user,
event_context={
'domain': settings.DOMAIN,
- 'user_fullname': user.fullname,
- 'user__id': user._id,
- 'src__id': src._id,
'src_url': src.url,
'src_title': src.title,
- 'results': results,
- 'can_change_preferences': False,
+
}
)
def send_archiver_file_not_found_mails(src, user, results, url):
- from osf.models.notification_type import NotificationType
+ from osf.models.notification_type import NotificationTypeEnum
- NotificationType.Type.DESK_ARCHIVE_JOB_FILE_NOT_FOUND.instance.emit(
+ NotificationTypeEnum.DESK_ARCHIVE_JOB_FILE_NOT_FOUND.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
- 'user': user.id,
- 'src': src._id,
+ 'domain': settings.DOMAIN,
+ 'src': src,
+ 'src_title': src.title,
+ 'user__id': user._id,
+ 'user_fullname': user.fullname,
+ 'src__id': src._id,
'results': results,
- 'url': url,
- 'can_change_preferences': False,
}
)
- NotificationType.Type.USER_ARCHIVE_JOB_FILE_NOT_FOUND.instance.emit(
+ NotificationTypeEnum.USER_ARCHIVE_JOB_FILE_NOT_FOUND.instance.emit(
user=user,
event_context={
'user': user.id,
@@ -110,39 +100,29 @@ def send_archiver_file_not_found_mails(src, user, results, url):
'src_title': src.title,
'src_url': src.url,
'results': results,
- 'can_change_preferences': False,
}
)
def send_archiver_uncaught_error_mails(src, user, results, url):
- from osf.models.notification_type import NotificationType
+ from osf.models.notification_type import NotificationTypeEnum
- NotificationType.Type.DESK_ARCHIVE_JOB_UNCAUGHT_ERROR.instance.emit(
+ NotificationTypeEnum.DESK_ARCHIVE_JOB_UNCAUGHT_ERROR.instance.emit(
destination_address=settings.OSF_SUPPORT_EMAIL,
event_context={
'user_fullname': user.fullname,
'user__id': user._id,
- 'user_username': user.username,
'src_title': src.title,
- 'src__id': src._id,
'src_url': src.url,
- 'src': src._id,
- 'results': [str(error) for error in results],
'url': url,
- 'can_change_preferences': False,
+ 'src__id': src._id,
+ 'results': results,
}
)
- NotificationType.Type.USER_ARCHIVE_JOB_UNCAUGHT_ERROR.instance.emit(
+ NotificationTypeEnum.USER_ARCHIVE_JOB_UNCAUGHT_ERROR.instance.emit(
user=user,
event_context={
- 'user_fullname': user.fullname,
- 'user__id': user._id,
'src_title': src.title,
- 'src__id': src._id,
'src_url': src.url,
- 'src': src._id,
- 'results': [str(error) for error in results],
- 'can_change_preferences': False,
}
)
diff --git a/website/mailchimp_utils.py b/website/mailchimp_utils.py
index 7e92a59d275..764247c1482 100644
--- a/website/mailchimp_utils.py
+++ b/website/mailchimp_utils.py
@@ -8,7 +8,7 @@
from framework.celery_tasks.handlers import queued_task
from framework.auth.signals import user_confirmed
from osf.exceptions import OSFError
-from osf.models import OSFUser, NotificationSubscription, NotificationType
+from osf.models import OSFUser, NotificationSubscription, NotificationTypeEnum
from website import settings
@@ -123,16 +123,22 @@ def subscribe_on_confirm(user):
# Subscribe user to default notification subscriptions
NotificationSubscription.objects.get_or_create(
user=user,
- notification_type=NotificationType.Type.REVIEWS_SUBMISSION_STATUS.instance,
+ notification_type=NotificationTypeEnum.REVIEWS_SUBMISSION_STATUS.instance,
content_type=ContentType.objects.get_for_model(user),
object_id=user.id,
- defaults={'message_frequency': 'instantly'},
+ defaults={
+ '_is_digest': True,
+ 'message_frequency': 'instantly',
+ },
)
NotificationSubscription.objects.get_or_create(
user=user,
- notification_type=NotificationType.Type.USER_FILE_UPDATED.instance,
+ notification_type=NotificationTypeEnum.USER_FILE_UPDATED.instance,
content_type=ContentType.objects.get_for_model(user),
object_id=user.id,
- defaults={'message_frequency': 'instantly'},
+ defaults={
+ '_is_digest': True,
+ 'message_frequency': 'instantly',
+ },
)
diff --git a/website/notifications/__init__.py b/website/notifications/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/website/notifications/constants.py b/website/notifications/constants.py
deleted file mode 100644
index ce3c9db4315..00000000000
--- a/website/notifications/constants.py
+++ /dev/null
@@ -1,39 +0,0 @@
-NODE_SUBSCRIPTIONS_AVAILABLE = {
- 'file_updated': 'Files updated'
-}
-
-# Note: if the subscription starts with 'global_', it will be treated like a default
-# subscription. If no notification type has been assigned, the user subscription
-# will default to 'email_transactional'.
-USER_SUBSCRIPTIONS_AVAILABLE = {
- 'global_file_updated': 'Files updated',
- 'global_reviews': 'Preprint submissions updated'
-}
-
-PROVIDER_SUBSCRIPTIONS_AVAILABLE = {
- 'new_pending_submissions': 'New preprint submissions for moderators to review.'
-}
-
-# Note: the python value None mean inherit from parent
-NOTIFICATION_TYPES = {
- 'email_transactional': 'Email when a change occurs',
- 'email_digest': 'Daily email digest of all changes to this project',
- 'none': 'None'
-}
-
-# Formatted file provider names for notification emails
-PROVIDERS = {
- 'osfstorage': 'OSF Storage',
- 'boa': 'Boa',
- 'box': 'Box',
- 'dataverse': 'Dataverse',
- 'dropbox': 'Dropbox',
- 'figshare': 'figshare',
- 'github': 'GitHub',
- 'gitlab': 'GitLab',
- 'bitbucket': 'Bitbucket',
- 'googledrive': 'Google Drive',
- 'owncloud': 'ownCloud',
- 'onedrive': 'Microsoft OneDrive',
- 's3': 'Amazon S3'
-}
diff --git a/website/notifications/emails.py b/website/notifications/emails.py
deleted file mode 100644
index e61c64e660a..00000000000
--- a/website/notifications/emails.py
+++ /dev/null
@@ -1,243 +0,0 @@
-from django.apps import apps
-
-from babel import dates, core, Locale
-
-from osf.models import AbstractNode, NotificationDigest, NotificationSubscription
-from osf.utils.permissions import ADMIN, READ
-from website import mails
-from website.notifications import constants
-from website.notifications import utils
-from website.util import web_url_for
-
-
-def notify(event, user, node, timestamp, **context):
- """Retrieve appropriate ***subscription*** and passe user list
-
- :param event: event that triggered the notification
- :param user: user who triggered notification
- :param node: instance of Node
- :param timestamp: time event happened
- :param context: optional variables specific to templates
- target_user: used with comment_replies
- :return: List of user ids notifications were sent to
- """
- sent_users = []
- # The user who the current comment is a reply to
- target_user = context.get('target_user', None)
- exclude = context.get('exclude', [])
- # do not notify user who initiated the emails
- exclude.append(user._id)
-
- event_type = utils.find_subscription_type(event)
- if target_user and event_type in constants.USER_SUBSCRIPTIONS_AVAILABLE:
- # global user
- subscriptions = get_user_subscriptions(target_user, event_type)
- else:
- # local project user
- subscriptions = compile_subscriptions(node, event_type, event)
-
- for notification_type in subscriptions:
- if notification_type == 'none' or not subscriptions[notification_type]:
- continue
- # Remove excluded ids from each notification type
- subscriptions[notification_type] = [guid for guid in subscriptions[notification_type] if guid not in exclude]
-
- # If target, they get a reply email and are removed from the general email
- if target_user and target_user._id in subscriptions[notification_type]:
- subscriptions[notification_type].remove(target_user._id)
- store_emails([target_user._id], notification_type, 'comment_replies', user, node, timestamp, **context)
- sent_users.append(target_user._id)
-
- if subscriptions[notification_type]:
- store_emails(subscriptions[notification_type], notification_type, event_type, user, node, timestamp, **context)
- sent_users.extend(subscriptions[notification_type])
- return sent_users
-
-def notify_mentions(event, user, node, timestamp, **context):
- OSFUser = apps.get_model('osf', 'OSFUser')
- recipient_ids = context.get('new_mentions', [])
- recipients = OSFUser.objects.filter(guids___id__in=recipient_ids)
- sent_users = notify_global_event(event, user, node, timestamp, recipients, context=context)
- return sent_users
-
-def notify_global_event(event, sender_user, node, timestamp, recipients, template=None, context=None):
- event_type = utils.find_subscription_type(event)
- sent_users = []
- if not context:
- context = {}
-
- for recipient in recipients:
- subscriptions = get_user_subscriptions(recipient, event_type)
- context['is_creator'] = recipient == node.creator
- if node.provider:
- context['has_psyarxiv_chronos_text'] = node.has_permission(recipient, ADMIN) and 'psyarxiv' in node.provider.name.lower()
- for notification_type in subscriptions:
- if (notification_type != 'none' and subscriptions[notification_type] and recipient._id in subscriptions[notification_type]):
- store_emails([recipient._id], notification_type, event, sender_user, node, timestamp, template=template, **context)
- sent_users.append(recipient._id)
-
- return sent_users
-
-
-def store_emails(recipient_ids, notification_type, event, user, node, timestamp, abstract_provider=None, template=None, **context):
- """Store notification emails
-
- Emails are sent via celery beat as digests
- :param recipient_ids: List of user ids to send mail to.
- :param notification_type: from constants.Notification_types
- :param event: event that triggered notification
- :param user: user who triggered the notification
- :param node: instance of Node
- :param timestamp: time event happened
- :param context:
- :return: --
- """
- OSFUser = apps.get_model('osf', 'OSFUser')
-
- if notification_type == 'none':
- return
-
- # If `template` is not specified, default to using a template with name `event`
- template = f'{template or event}.html.mako'
-
- # user whose action triggered email sending
- context['user'] = user
- node_lineage_ids = get_node_lineage(node) if node else []
-
- for recipient_id in recipient_ids:
- if recipient_id == user._id:
- continue
- recipient = OSFUser.load(recipient_id)
- if recipient.is_disabled:
- continue
- context['localized_timestamp'] = localize_timestamp(timestamp, recipient)
- context['recipient'] = recipient
- message = mails.render_message(template, **context)
- digest = NotificationDigest(
- timestamp=timestamp,
- send_type=notification_type,
- event=event,
- user=recipient,
- message=message,
- node_lineage=node_lineage_ids,
- provider=abstract_provider
- )
- digest.save()
-
-
-def compile_subscriptions(node, event_type, event=None, level=0):
- """Recurse through node and parents for subscriptions.
-
- :param node: current node
- :param event_type: Generally node_subscriptions_available
- :param event: Particular event such a file_updated that has specific file subs
- :param level: How deep the recursion is
- :return: a dict of notification types with lists of users.
- """
- subscriptions = check_node(node, event_type)
- if event:
- subscriptions = check_node(node, event) # Gets particular event subscriptions
- parent_subscriptions = compile_subscriptions(node, event_type, level=level + 1) # get node and parent subs
- elif getattr(node, 'parent_id', False):
- parent_subscriptions = \
- compile_subscriptions(AbstractNode.load(node.parent_id), event_type, level=level + 1)
- else:
- parent_subscriptions = check_node(None, event_type)
- for notification_type in parent_subscriptions:
- p_sub_n = parent_subscriptions[notification_type]
- p_sub_n.extend(subscriptions[notification_type])
- for nt in subscriptions:
- if notification_type != nt:
- p_sub_n = list(set(p_sub_n).difference(set(subscriptions[nt])))
- if level == 0:
- p_sub_n, removed = utils.separate_users(node, p_sub_n)
- parent_subscriptions[notification_type] = p_sub_n
- return parent_subscriptions
-
-
-def check_node(node, event):
- """Return subscription for a particular node and event."""
- node_subscriptions = {key: [] for key in constants.NOTIFICATION_TYPES}
- if node:
- subscription = NotificationSubscription.load(utils.to_subscription_key(node._id, event))
- for notification_type in node_subscriptions:
- users = getattr(subscription, notification_type, [])
- if users:
- for user in users.exclude(date_disabled__isnull=False):
- if node.has_permission(user, READ):
- node_subscriptions[notification_type].append(user._id)
- return node_subscriptions
-
-
-def get_user_subscriptions(user, event):
- if user.is_disabled:
- return {}
- user_subscription = NotificationSubscription.load(utils.to_subscription_key(user._id, event))
- if user_subscription:
- return {key: list(getattr(user_subscription, key).all().values_list('guids___id', flat=True)) for key in constants.NOTIFICATION_TYPES}
- else:
- return {key: [user._id] if (event in constants.USER_SUBSCRIPTIONS_AVAILABLE and key == 'email_transactional') else [] for key in constants.NOTIFICATION_TYPES}
-
-
-def get_node_lineage(node):
- """ Get a list of node ids in order from the node to top most project
- e.g. [parent._id, node._id]
- """
- from osf.models import Preprint
- lineage = [node._id]
- if isinstance(node, Preprint):
- return lineage
-
- while node.parent_id:
- node = node.parent_node
- lineage = [node._id] + lineage
-
- return lineage
-
-
-def get_settings_url(uid, user):
- if uid == user._id:
- return web_url_for('user_notifications', _absolute=True)
-
- node = AbstractNode.load(uid)
- assert node, 'get_settings_url received an invalid Node id'
- return node.web_url_for('node_setting', _guid=True, _absolute=True)
-
-def fix_locale(locale):
- """Attempt to fix a locale to have the correct casing, e.g. de_de -> de_DE
-
- This is NOT guaranteed to return a valid locale identifier.
- """
- try:
- language, territory = locale.split('_', 1)
- except ValueError:
- return locale
- else:
- return '_'.join([language, territory.upper()])
-
-def localize_timestamp(timestamp, user):
- try:
- user_timezone = dates.get_timezone(user.timezone)
- except LookupError:
- user_timezone = dates.get_timezone('Etc/UTC')
-
- try:
- user_locale = Locale(user.locale)
- except core.UnknownLocaleError:
- user_locale = Locale('en')
-
- # Do our best to find a valid locale
- try:
- user_locale.date_formats
- except OSError: # An IOError will be raised if locale's casing is incorrect, e.g. de_de vs. de_DE
- # Attempt to fix the locale, e.g. de_de -> de_DE
- try:
- user_locale = Locale(fix_locale(user.locale))
- user_locale.date_formats
- except (core.UnknownLocaleError, OSError):
- user_locale = Locale('en')
-
- formatted_date = dates.format_date(timestamp, format='full', locale=user_locale)
- formatted_time = dates.format_time(timestamp, format='short', tzinfo=user_timezone, locale=user_locale)
-
- return f'{formatted_time} on {formatted_date}'
diff --git a/website/notifications/utils.py b/website/notifications/utils.py
deleted file mode 100644
index b9a9b6d10e3..00000000000
--- a/website/notifications/utils.py
+++ /dev/null
@@ -1,479 +0,0 @@
-import collections
-
-from django.apps import apps
-from django.db.models import Q
-
-from osf.utils.permissions import READ
-from website.notifications import constants
-from website.notifications.exceptions import InvalidSubscriptionError
-from website.project import signals
-
-class NotificationsDict(dict):
- def __init__(self):
- super().__init__()
- self.update(messages=[], children=collections.defaultdict(NotificationsDict))
-
- def add_message(self, keys, messages):
- """
- :param keys: ordered list of project ids from parent to node (e.g. ['parent._id', 'node._id'])
- :param messages: built email message for an event that occurred on the node
- :return: nested dict with project/component ids as the keys with the message at the appropriate level
- """
- d_to_use = self
-
- for key in keys:
- d_to_use = d_to_use['children'][key]
-
- if not isinstance(messages, list):
- messages = [messages]
-
- d_to_use['messages'].extend(messages)
-
-
-def find_subscription_type(subscription):
- """Find subscription type string within specific subscription.
- Essentially removes extraneous parts of the string to get the type.
- """
- subs_available = list(constants.USER_SUBSCRIPTIONS_AVAILABLE.keys())
- subs_available.extend(list(constants.NODE_SUBSCRIPTIONS_AVAILABLE.keys()))
- for available in subs_available:
- if available in subscription:
- return available
-
-
-def to_subscription_key(uid, event):
- """Build the Subscription primary key for the given guid and event"""
- return f'{uid}_{event}'
-
-
-def from_subscription_key(key):
- parsed_key = key.split('_', 1)
- return {
- 'uid': parsed_key[0],
- 'event': parsed_key[1]
- }
-
-
-@signals.contributor_removed.connect
-def remove_contributor_from_subscriptions(node, user):
- """ Remove contributor from node subscriptions unless the user is an
- admin on any of node's parent projects.
- """
- Preprint = apps.get_model('osf.Preprint')
- DraftRegistration = apps.get_model('osf.DraftRegistration')
- # Preprints don't have subscriptions at this time
- if isinstance(node, Preprint):
- return
- if isinstance(node, DraftRegistration):
- return
-
- # If user still has permissions through being a contributor or group member, or has
- # admin perms on a parent, don't remove their subscription
- if not (node.is_contributor_or_group_member(user)) and user._id not in node.admin_contributor_or_group_member_ids:
- node_subscriptions = get_all_node_subscriptions(user, node)
- for subscription in node_subscriptions:
- subscription.objects.filter(
- user=user,
- ).delete()
-
-def separate_users(node, user_ids):
- """Separates users into ones with permissions and ones without given a list.
- :param node: Node to separate based on permissions
- :param user_ids: List of ids, will also take and return User instances
- :return: list of subbed, list of removed user ids
- """
- OSFUser = apps.get_model('osf.OSFUser')
- removed = []
- subbed = []
- for user_id in user_ids:
- try:
- user = OSFUser.load(user_id)
- except TypeError:
- user = user_id
- if node.has_permission(user, READ):
- subbed.append(user_id)
- else:
- removed.append(user_id)
- return subbed, removed
-
-
-def users_to_remove(source_event, source_node, new_node):
- """Find users that do not have permissions on new_node.
- :param source_event: such as _file_updated
- :param source_node: Node instance where a subscription currently resides
- :param new_node: Node instance where a sub or new sub will be.
- :return: Dict of notification type lists with user_ids
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- removed_users = {key: [] for key in constants.NOTIFICATION_TYPES}
- if source_node == new_node:
- return removed_users
- old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event))
- old_node_sub = NotificationSubscription.load(to_subscription_key(source_node._id,
- '_'.join(source_event.split('_')[-2:])))
- if not old_sub and not old_node_sub:
- return removed_users
- for notification_type in constants.NOTIFICATION_TYPES:
- users = []
- if hasattr(old_sub, notification_type):
- users += list(getattr(old_sub, notification_type).values_list('guids___id', flat=True))
- if hasattr(old_node_sub, notification_type):
- users += list(getattr(old_node_sub, notification_type).values_list('guids___id', flat=True))
- subbed, removed_users[notification_type] = separate_users(new_node, users)
- return removed_users
-
-
-def move_subscription(remove_users, source_event, source_node, new_event, new_node):
- """Moves subscription from old_node to new_node
- :param remove_users: dictionary of lists of users to remove from the subscription
- :param source_event: A specific guid event _file_updated
- :param source_node: Instance of Node
- :param new_event: A specific guid event
- :param new_node: Instance of Node
- :return: Returns a NOTIFICATION_TYPES list of removed users without permissions
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- if source_node == new_node:
- return
- old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event))
- if not old_sub:
- return
- elif old_sub:
- old_sub._id = to_subscription_key(new_node._id, new_event)
- old_sub.event_name = new_event
- old_sub.owner = new_node
- new_sub = old_sub
- new_sub.save()
- # Remove users that don't have permission on the new node.
- for notification_type in constants.NOTIFICATION_TYPES:
- if new_sub:
- for user_id in remove_users[notification_type]:
- related_manager = getattr(new_sub, notification_type, None)
- subscriptions = related_manager.all() if related_manager else []
- if user_id in subscriptions:
- new_sub.delete()
-
-
-def get_configured_projects(user):
- """Filter all user subscriptions for ones that are on parent projects
- and return the node objects.
- :param user: OSFUser object
- :return: list of node objects for projects with no parent
- """
- configured_projects = set()
- user_subscriptions = get_all_user_subscriptions(user, extra=(
- ~Q(node__type='osf.collection') &
- Q(node__is_deleted=False)
- ))
-
- for subscription in user_subscriptions:
- # If the user has opted out of emails skip
- node = subscription.subscribed_object
-
- if subscription.message_frequency == 'none':
- continue
-
- root = node.root
-
- if not root.is_deleted:
- configured_projects.add(root)
-
- return sorted(configured_projects, key=lambda n: n.title.lower())
-
-
-def check_project_subscriptions_are_all_none(user, node):
- node_subscriptions = get_all_node_subscriptions(user, node)
- for s in node_subscriptions:
- if not s.none.filter(id=user.id).exists():
- return False
- return True
-
-
-def get_all_user_subscriptions(user, extra=None):
- """ Get all Subscription objects that the user is subscribed to"""
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- queryset = NotificationSubscription.objects.filter(
- Q(none=user.pk) |
- Q(email_digest=user.pk) |
- Q(email_transactional=user.pk)
- ).distinct()
- return queryset.filter(extra) if extra else queryset
-
-
-def get_all_node_subscriptions(user, node, user_subscriptions=None):
- """ Get all Subscription objects for a node that the user is subscribed to
- :param user: OSFUser object
- :param node: Node object
- :param user_subscriptions: all Subscription objects that the user is subscribed to
- :return: list of Subscription objects for a node that the user is subscribed to
- """
- if not user_subscriptions:
- user_subscriptions = get_all_user_subscriptions(user)
- return user_subscriptions.filter(user__isnull=True, node=node)
-
-
-def format_data(user, nodes):
- """ Format subscriptions data for project settings page
- :param user: OSFUser object
- :param nodes: list of parent project node objects
- :return: treebeard-formatted data
- """
- items = []
-
- user_subscriptions = get_all_user_subscriptions(user)
- for node in nodes:
- assert node, f'{node._id} is not a valid Node.'
-
- can_read = node.has_permission(user, READ)
- can_read_children = node.has_permission_on_children(user, READ)
-
- if not can_read and not can_read_children:
- continue
-
- children = node.get_nodes(**{'is_deleted': False, 'is_node_link': False})
- children_tree = []
- # List project/node if user has at least READ permissions (contributor or admin viewer) or if
- # user is contributor on a component of the project/node
-
- if can_read:
- node_sub_available = list(constants.NODE_SUBSCRIPTIONS_AVAILABLE.keys())
- subscriptions = get_all_node_subscriptions(
- user,
- node,
- user_subscriptions=user_subscriptions).filter(notification_type__name__in=node_sub_available)
-
- for subscription in subscriptions:
- index = node_sub_available.index(getattr(subscription, 'event_name'))
- children_tree.append(serialize_event(user, subscription=subscription,
- node=node, event_description=node_sub_available.pop(index)))
- for node_sub in node_sub_available:
- children_tree.append(serialize_event(user, node=node, event_description=node_sub))
- children_tree.sort(key=lambda s: s['event']['title'])
-
- children_tree.extend(format_data(user, children))
-
- item = {
- 'node': {
- 'id': node._id,
- 'url': node.url if can_read else '',
- 'title': node.title if can_read else 'Private Project',
- },
- 'children': children_tree,
- 'kind': 'folder' if not node.parent_node or not node.parent_node.has_permission(user, READ) else 'node',
- 'nodeType': node.project_or_component,
- 'category': node.category,
- 'permissions': {
- 'view': can_read,
- },
- }
-
- items.append(item)
-
- return items
-
-
-def format_user_subscriptions(user):
- """ Format user-level subscriptions (e.g. comment replies across the OSF) for user settings page"""
- user_subs_available = list(constants.USER_SUBSCRIPTIONS_AVAILABLE.keys())
- subscriptions = [
- serialize_event(
- user, subscription,
- event_description=user_subs_available.pop(user_subs_available.index(getattr(subscription, 'event_name')))
- )
- for subscription in get_all_user_subscriptions(user)
- if subscription is not None and getattr(subscription, 'event_name') in user_subs_available
- ]
- subscriptions.extend([serialize_event(user, event_description=sub) for sub in user_subs_available])
- return subscriptions
-
-
-def format_file_subscription(user, node_id, path, provider):
- """Format a single file event"""
- AbstractNode = apps.get_model('osf.AbstractNode')
- node = AbstractNode.load(node_id)
- wb_path = path.lstrip('/')
- for subscription in get_all_node_subscriptions(user, node):
- if wb_path in getattr(subscription, 'event_name'):
- return serialize_event(user, subscription, node)
- return serialize_event(user, node=node, event_description='file_updated')
-
-
-all_subs = constants.NODE_SUBSCRIPTIONS_AVAILABLE.copy()
-all_subs.update(constants.USER_SUBSCRIPTIONS_AVAILABLE)
-
-def serialize_event(user, subscription=None, node=None, event_description=None):
- """
- :param user: OSFUser object
- :param subscription: Subscription object, use if parsing particular subscription
- :param node: Node object, use if node is known
- :param event_description: use if specific subscription is known
- :return: treebeard-formatted subscription event
- """
- if not event_description:
- event_description = getattr(subscription, 'event_name')
- # Looks at only the types available. Deals with pre-pending file names.
- for sub_type in all_subs:
- if sub_type in event_description:
- event_type = sub_type
- else:
- event_type = event_description
- if node and node.parent_node:
- notification_type = 'adopt_parent'
- elif event_type.startswith('global_'):
- notification_type = 'email_transactional'
- else:
- notification_type = 'none'
- if subscription:
- for n_type in constants.NOTIFICATION_TYPES:
- if getattr(subscription, n_type).filter(id=user.id).exists():
- notification_type = n_type
- return {
- 'event': {
- 'title': event_description,
- 'description': all_subs[event_type],
- 'notificationType': notification_type,
- 'parent_notification_type': get_parent_notification_type(node, event_type, user)
- },
- 'kind': 'event',
- 'children': []
- }
-
-
-def get_parent_notification_type(node, event, user):
- """
- Given an event on a node (e.g. comment on node 'xyz'), find the user's notification
- type on the parent project for the same event.
- :param obj node: event owner (Node or User object)
- :param str event: notification event (e.g. 'comment_replies')
- :param obj user: OSFUser object
- :return: str notification type (e.g. 'email_transactional')
- """
- AbstractNode = apps.get_model('osf.AbstractNode')
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
-
- if node and isinstance(node, AbstractNode) and node.parent_node and node.parent_node.has_permission(user, READ):
- parent = node.parent_node
- key = to_subscription_key(parent._id, event)
- try:
- subscription = NotificationSubscription.objects.get(_id=key)
- except NotificationSubscription.DoesNotExist:
- return get_parent_notification_type(parent, event, user)
-
- for notification_type in constants.NOTIFICATION_TYPES:
- if getattr(subscription, notification_type).filter(id=user.id).exists():
- return notification_type
- else:
- return get_parent_notification_type(parent, event, user)
- else:
- return None
-
-
-def get_global_notification_type(global_subscription, user):
- """
- Given a global subscription (e.g. NotificationSubscription object with event_type
- 'global_file_updated'), find the user's notification type.
- :param obj global_subscription: NotificationSubscription object
- :param obj user: OSFUser object
- :return: str notification type (e.g. 'email_transactional')
- """
- for notification_type in constants.NOTIFICATION_TYPES:
- # TODO Optimize me
- if getattr(global_subscription, notification_type).filter(id=user.id).exists():
- return notification_type
-
-
-def check_if_all_global_subscriptions_are_none(user):
- # This function predates comment mentions, which is a global_ notification that cannot be disabled
- # Therefore, an actual check would never return True.
- # If this changes, an optimized query would look something like:
- # not NotificationSubscription.objects.filter(Q(event_name__startswith='global_') & (Q(email_digest=user.pk)|Q(email_transactional=user.pk))).exists()
- return False
-
-
-def subscribe_user_to_global_notifications(user):
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- notification_type = 'email_transactional'
- user_events = constants.USER_SUBSCRIPTIONS_AVAILABLE
- for user_event in user_events:
- user_event_id = to_subscription_key(user._id, user_event)
-
- # get_or_create saves on creation
- subscription, created = NotificationSubscription.objects.get_or_create(_id=user_event_id, user=user, event_name=user_event)
- subscription.add_user_to_subscription(user, notification_type)
- subscription.save()
-
-
-def subscribe_user_to_notifications(node, user):
- """ Update the notification settings for the creator or contributors
- :param user: User to subscribe to notifications
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- Preprint = apps.get_model('osf.Preprint')
- DraftRegistration = apps.get_model('osf.DraftRegistration')
- if isinstance(node, Preprint):
- raise InvalidSubscriptionError('Preprints are invalid targets for subscriptions at this time.')
-
- if isinstance(node, DraftRegistration):
- raise InvalidSubscriptionError('DraftRegistrations are invalid targets for subscriptions at this time.')
-
- if node.is_collection:
- raise InvalidSubscriptionError('Collections are invalid targets for subscriptions')
-
- if node.is_deleted:
- raise InvalidSubscriptionError('Deleted Nodes are invalid targets for subscriptions')
-
- if getattr(node, 'is_registration', False):
- raise InvalidSubscriptionError('Registrations are invalid targets for subscriptions')
-
- events = constants.NODE_SUBSCRIPTIONS_AVAILABLE
- notification_type = 'email_transactional'
- target_id = node._id
-
- if user.is_registered:
- for event in events:
- event_id = to_subscription_key(target_id, event)
- global_event_id = to_subscription_key(user._id, 'global_' + event)
- global_subscription = NotificationSubscription.load(global_event_id)
-
- subscription = NotificationSubscription.load(event_id)
-
- # If no subscription for component and creator is the user, do not create subscription
- # If no subscription exists for the component, this means that it should adopt its
- # parent's settings
- if not (node and node.parent_node and not subscription and node.creator == user):
- if not subscription:
- subscription = NotificationSubscription(_id=event_id, owner=node, event_name=event)
- # Need to save here in order to access m2m fields
- subscription.save()
- if global_subscription:
- global_notification_type = get_global_notification_type(global_subscription, user)
- subscription.add_user_to_subscription(user, global_notification_type)
- else:
- subscription.add_user_to_subscription(user, notification_type)
- subscription.save()
-
-
-def format_user_and_project_subscriptions(user):
- """ Format subscriptions data for user settings page. """
- return [
- {
- 'node': {
- 'id': user._id,
- 'title': 'Default Notification Settings',
- 'help': 'These are default settings for new projects you create ' +
- 'or are added to. Modifying these settings will not ' +
- 'modify settings on existing projects.'
- },
- 'kind': 'heading',
- 'children': format_user_subscriptions(user)
- },
- {
- 'node': {
- 'id': '',
- 'title': 'Project Notifications',
- 'help': 'These are settings for each of your projects. Modifying ' +
- 'these settings will only modify the settings for the selected project.'
- },
- 'kind': 'heading',
- 'children': format_data(user, get_configured_projects(user))
- }]
diff --git a/website/notifications/views.py b/website/notifications/views.py
deleted file mode 100644
index 700594f69d6..00000000000
--- a/website/notifications/views.py
+++ /dev/null
@@ -1,540 +0,0 @@
-from django.contrib.contenttypes.models import ContentType
-from rest_framework import status as http_status
-
-from flask import request
-
-from framework import sentry
-from framework.auth.decorators import must_be_logged_in
-from framework.exceptions import HTTPError
-
-from osf.models import AbstractNode, Registration, Node
-
-NOTIFICATION_TYPES = {}
-USER_SUBSCRIPTIONS_AVAILABLE = {}
-NODE_SUBSCRIPTIONS_AVAILABLE = {}
-from website.project.decorators import must_be_valid_project
-import collections
-
-from django.apps import apps
-from django.db.models import Q
-
-from osf.models import NotificationSubscription
-from osf.utils.permissions import READ
-
-
-class NotificationsDict(dict):
- def __init__(self):
- super().__init__()
- self.update(messages=[], children=collections.defaultdict(NotificationsDict))
-
- def add_message(self, keys, messages):
- """
- :param keys: ordered list of project ids from parent to node (e.g. ['parent._id', 'node._id'])
- :param messages: built email message for an event that occurred on the node
- :return: nested dict with project/component ids as the keys with the message at the appropriate level
- """
- d_to_use = self
-
- for key in keys:
- d_to_use = d_to_use['children'][key]
-
- if not isinstance(messages, list):
- messages = [messages]
-
- d_to_use['messages'].extend(messages)
-
-
-def find_subscription_type(subscription):
- """Find subscription type string within specific subscription.
- Essentially removes extraneous parts of the string to get the type.
- """
- subs_available = list(USER_SUBSCRIPTIONS_AVAILABLE.keys())
- subs_available.extend(list(NODE_SUBSCRIPTIONS_AVAILABLE.keys()))
- for available in subs_available:
- if available in subscription:
- return available
-
-
-def to_subscription_key(uid, event):
- """Build the Subscription primary key for the given guid and event"""
- return f'{uid}_{event}'
-
-
-def from_subscription_key(key):
- parsed_key = key.split('_', 1)
- return {
- 'uid': parsed_key[0],
- 'event': parsed_key[1]
- }
-
-
-def users_to_remove(source_event, source_node, new_node):
- """Find users that do not have permissions on new_node.
- :param source_event: such as _file_updated
- :param source_node: Node instance where a subscription currently resides
- :param new_node: Node instance where a sub or new sub will be.
- :return: Dict of notification type lists with user_ids
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- removed_users = {key: [] for key in NOTIFICATION_TYPES}
- if source_node == new_node:
- return removed_users
- old_sub = NotificationSubscription.objects.get(
- subscribed_object=source_node,
- notification_type__name=source_event
- )
- for notification_type in NOTIFICATION_TYPES:
- users = []
- if hasattr(old_sub, notification_type):
- users += list(getattr(old_sub, notification_type).values_list('guids___id', flat=True))
- return removed_users
-
-
-def move_subscription(remove_users, source_event, source_node, new_event, new_node):
- """Moves subscription from old_node to new_node
- :param remove_users: dictionary of lists of users to remove from the subscription
- :param source_event: A specific guid event _file_updated
- :param source_node: Instance of Node
- :param new_event: A specific guid event
- :param new_node: Instance of Node
- :return: Returns a NOTIFICATION_TYPES list of removed users without permissions
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- OSFUser = apps.get_model('osf.OSFUser')
- if source_node == new_node:
- return
- old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event))
- if not old_sub:
- return
- elif old_sub:
- old_sub._id = to_subscription_key(new_node._id, new_event)
- old_sub.event_name = new_event
- old_sub.owner = new_node
- new_sub = old_sub
- new_sub.save()
- # Remove users that don't have permission on the new node.
- for notification_type in NOTIFICATION_TYPES:
- if new_sub:
- for user_id in remove_users[notification_type]:
- related_manager = getattr(new_sub, notification_type, None)
- subscriptions = related_manager.all() if related_manager else []
- if user_id in subscriptions:
- user = OSFUser.load(user_id)
- new_sub.remove_user_from_subscription(user)
-
-
-def get_configured_projects(user):
- """Filter all user subscriptions for ones that are on parent projects
- and return the node objects.
- :param user: OSFUser object
- :return: list of node objects for projects with no parent
- """
- configured_projects = set()
- user_subscriptions = get_all_user_subscriptions(user, extra=(
- ~Q(node__type='osf.collection') &
- Q(node__is_deleted=False)
- ))
-
- for subscription in user_subscriptions:
- # If the user has opted out of emails skip
- node = subscription.subscribed_object
-
- if subscription.message_frequency == 'none':
- continue
- if isinstance(node, Node):
- root = node.root
-
- if not root.is_deleted:
- configured_projects.add(root)
-
- return sorted(configured_projects, key=lambda n: n.title.lower())
-
-
-def check_project_subscriptions_are_all_none(user, node):
- node_subscriptions = NotificationSubscription.objects.filter(
- user=user,
- object_id=node.id,
- content_type=ContentType.objects.get_for_model(node).id,
- )
- for s in node_subscriptions:
- if not s.message_frequecy == 'none':
- return False
- return True
-
-
-def get_all_user_subscriptions(user, extra=None):
- """ Get all Subscription objects that the user is subscribed to"""
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- return NotificationSubscription.objects.filter(
- user=user,
- )
-
-
-def get_all_node_subscriptions(user, node, user_subscriptions=None):
- """ Get all Subscription objects for a node that the user is subscribed to
- :param user: OSFUser object
- :param node: Node object
- :param user_subscriptions: all Subscription objects that the user is subscribed to
- :return: list of Subscription objects for a node that the user is subscribed to
- """
- if not user_subscriptions:
- user_subscriptions = get_all_user_subscriptions(user)
- return user_subscriptions.filter(
- object_id=node.id,
- content_type=ContentType.objects.get_for_model(node).id,
- )
-
-
-def format_data(user, nodes):
- """ Format subscriptions data for project settings page
- :param user: OSFUser object
- :param nodes: list of parent project node objects
- :return: treebeard-formatted data
- """
- items = []
-
- user_subscriptions = get_all_user_subscriptions(user)
- for node in nodes:
- assert node, f'{node._id} is not a valid Node.'
-
- can_read = node.has_permission(user, READ)
- can_read_children = node.has_permission_on_children(user, READ)
-
- if not can_read and not can_read_children:
- continue
-
- children = node.get_nodes(**{'is_deleted': False, 'is_node_link': False})
- children_tree = []
- # List project/node if user has at least READ permissions (contributor or admin viewer) or if
- # user is contributor on a component of the project/node
-
- if can_read:
- node_sub_available = list(NODE_SUBSCRIPTIONS_AVAILABLE.keys())
- subscriptions = get_all_node_subscriptions(
- user,
- node,
- user_subscriptions=user_subscriptions
- ).filter(
- notification_type__name__in=node_sub_available
- )
-
- for subscription in subscriptions:
- index = node_sub_available.index(getattr(subscription, 'event_name'))
- children_tree.append(serialize_event(user, subscription=subscription,
- node=node, event_description=node_sub_available.pop(index)))
- for node_sub in node_sub_available:
- children_tree.append(serialize_event(user, node=node, event_description=node_sub))
- children_tree.sort(key=lambda s: s['event']['title'])
-
- children_tree.extend(format_data(user, children))
-
- item = {
- 'node': {
- 'id': node._id,
- 'url': node.url if can_read else '',
- 'title': node.title if can_read else 'Private Project',
- },
- 'children': children_tree,
- 'kind': 'folder' if not node.parent_node or not node.parent_node.has_permission(user, READ) else 'node',
- 'nodeType': node.project_or_component,
- 'category': node.category,
- 'permissions': {
- 'view': can_read,
- },
- }
-
- items.append(item)
-
- return items
-
-
-def format_user_subscriptions(user):
- """ Format user-level subscriptions (e.g. comment replies across the OSF) for user settings page"""
- user_subs_available = list(USER_SUBSCRIPTIONS_AVAILABLE.keys())
- subscriptions = [
- serialize_event(
- user, subscription,
- event_description=user_subs_available.pop(user_subs_available.index(getattr(subscription, 'event_name')))
- )
- for subscription in get_all_user_subscriptions(user)
- if subscription is not None in user_subs_available
- ]
- subscriptions.extend([serialize_event(user, event_description=sub) for sub in user_subs_available])
- return subscriptions
-
-
-def format_file_subscription(user, node_id, path, provider):
- """Format a single file event"""
- AbstractNode = apps.get_model('osf.AbstractNode')
- node = AbstractNode.load(node_id)
- wb_path = path.lstrip('/')
- for subscription in get_all_node_subscriptions(user, node):
- if wb_path in getattr(subscription, 'event_name'):
- return serialize_event(user, subscription, node)
- return serialize_event(user, node=node, event_description='file_updated')
-
-def serialize_event(user, subscription=None, node=None, event_description=None):
- """
- :param user: OSFUser object
- :param subscription: Subscription object, use if parsing particular subscription
- :param node: Node object, use if node is known
- :param event_description: use if specific subscription is known
- :return: treebeard-formatted subscription event
- """
- if not event_description:
- event_description = getattr(subscription, 'event_name')
- # Looks at only the types available. Deals with pre-pending file names.
- for sub_type in {}:
- if sub_type in event_description:
- event_type = sub_type
- else:
- event_type = event_description
- if node and node.parent_node:
- notification_type = 'adopt_parent'
- elif event_type.startswith('global_'):
- notification_type = 'email_transactional'
- else:
- notification_type = 'none'
- if subscription:
- for n_type in {}:
- if getattr(subscription, n_type).filter(id=user.id).exists():
- notification_type = n_type
- return {
- 'event': {
- 'title': event_description,
- 'description': {}[event_type],
- 'notificationType': notification_type,
- 'parent_notification_type': get_parent_notification_type(node, event_type, user)
- },
- 'kind': 'event',
- 'children': []
- }
-
-
-def get_parent_notification_type(node, event, user):
- """
- Given an event on a node (e.g. comment on node 'xyz'), find the user's notification
- type on the parent project for the same event.
- :param obj node: event owner (Node or User object)
- :param str event: notification event (e.g. 'comment_replies')
- :param obj user: OSFUser object
- :return: str notification type (e.g. 'email_transactional')
- """
- AbstractNode = apps.get_model('osf.AbstractNode')
- NotificationSubscriptionLegacy = apps.get_model('osf.NotificationSubscriptionLegacy')
-
- if node and isinstance(node, AbstractNode) and node.parent_node and node.parent_node.has_permission(user, READ):
- parent = node.parent_node
- key = to_subscription_key(parent._id, event)
- try:
- subscription = NotificationSubscriptionLegacy.objects.get(_id=key)
- except NotificationSubscriptionLegacy.DoesNotExist:
- return get_parent_notification_type(parent, event, user)
-
- for notification_type in NOTIFICATION_TYPES:
- if getattr(subscription, notification_type).filter(id=user.id).exists():
- return notification_type
- else:
- return get_parent_notification_type(parent, event, user)
- else:
- return None
-
-
-def get_global_notification_type(global_subscription, user):
- """
- Given a global subscription (e.g. NotificationSubscription object with event_type
- 'global_file_updated'), find the user's notification type.
- :param obj global_subscription: NotificationSubscription object
- :param obj user: OSFUser object
- :return: str notification type (e.g. 'email_transactional')
- """
- for notification_type in NOTIFICATION_TYPES:
- # TODO Optimize me
- if getattr(global_subscription, notification_type).filter(id=user.id).exists():
- return notification_type
-
-
-def check_if_all_global_subscriptions_are_none(user):
- # This function predates comment mentions, which is a global_ notification that cannot be disabled
- # Therefore, an actual check would never return True.
- # If this changes, an optimized query would look something like:
- # not NotificationSubscriptionLegacy.objects.filter(Q(event_name__startswith='global_') & (Q(email_digest=user.pk)|Q(email_transactional=user.pk))).exists()
- return False
-
-
-def subscribe_user_to_global_notifications(user):
- NotificationSubscriptionLegacy = apps.get_model('osf.NotificationSubscriptionLegacy')
- notification_type = 'email_transactional'
- user_events = USER_SUBSCRIPTIONS_AVAILABLE
- for user_event in user_events:
- user_event_id = to_subscription_key(user._id, user_event)
-
- # get_or_create saves on creation
- subscription, created = NotificationSubscriptionLegacy.objects.get_or_create(_id=user_event_id, user=user, event_name=user_event)
- subscription.add_user_to_subscription(user, notification_type)
- subscription.save()
-
-
-class InvalidSubscriptionError:
- pass
-
-
-def subscribe_user_to_notifications(node, user):
- """ Update the notification settings for the creator or contributors
- :param user: User to subscribe to notifications
- """
- NotificationSubscription = apps.get_model('osf.NotificationSubscription')
- Preprint = apps.get_model('osf.Preprint')
- DraftRegistration = apps.get_model('osf.DraftRegistration')
- if isinstance(node, Preprint):
- raise InvalidSubscriptionError('Preprints are invalid targets for subscriptions at this time.')
-
- if isinstance(node, DraftRegistration):
- raise InvalidSubscriptionError('DraftRegistrations are invalid targets for subscriptions at this time.')
-
- if node.is_collection:
- raise InvalidSubscriptionError('Collections are invalid targets for subscriptions')
-
- if node.is_deleted:
- raise InvalidSubscriptionError('Deleted Nodes are invalid targets for subscriptions')
-
- if getattr(node, 'is_registration', False):
- raise InvalidSubscriptionError('Registrations are invalid targets for subscriptions')
-
- events = NODE_SUBSCRIPTIONS_AVAILABLE
-
- if user.is_registered:
- for event in events:
- subscription, _ = NotificationSubscription.objects.get_or_create(
- user=user,
- notification_type__name=event
- )
-
-
-def format_user_and_project_subscriptions(user):
- """ Format subscriptions data for user settings page. """
- return [
- {
- 'node': {
- 'id': user._id,
- 'title': 'Default Notification Settings',
- 'help': 'These are default settings for new projects you create ' +
- 'or are added to. Modifying these settings will not ' +
- 'modify settings on existing projects.'
- },
- 'kind': 'heading',
- 'children': format_user_subscriptions(user)
- },
- {
- 'node': {
- 'id': '',
- 'title': 'Project Notifications',
- 'help': 'These are settings for each of your projects. Modifying ' +
- 'these settings will only modify the settings for the selected project.'
- },
- 'kind': 'heading',
- 'children': format_data(user, get_configured_projects(user))
- }]
-
-
-@must_be_logged_in
-def get_subscriptions(auth):
- return format_user_and_project_subscriptions(auth.user)
-
-
-@must_be_logged_in
-@must_be_valid_project
-def get_node_subscriptions(auth, **kwargs):
- node = kwargs.get('node') or kwargs['project']
- return format_data(auth.user, [node])
-
-
-@must_be_logged_in
-def get_file_subscriptions(auth, **kwargs):
- node_id = request.args.get('node_id')
- path = request.args.get('path')
- provider = request.args.get('provider')
- return format_file_subscription(auth.user, node_id, path, provider)
-
-
-@must_be_logged_in
-def configure_subscription(auth):
- user = auth.user
- json_data = request.get_json()
- target_id = json_data.get('id')
- event = json_data.get('event')
- notification_type = json_data.get('notification_type')
- path = json_data.get('path')
- provider = json_data.get('provider')
-
- if not event or (notification_type not in NOTIFICATION_TYPES and notification_type != 'adopt_parent'):
- raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict(
- message_long='Must provide an event and notification type for subscription.')
- )
-
- node = AbstractNode.load(target_id)
- if 'file_updated' in event and path is not None and provider is not None:
- wb_path = path.lstrip('/')
- event = wb_path + '_file_updated'
- event_id = to_subscription_key(target_id, event)
-
- if not node:
- # if target_id is not a node it currently must be the current user
- if not target_id == user._id:
- sentry.log_message(
- '{!r} attempted to subscribe to either a bad '
- 'id or non-node non-self id, {}'.format(user, target_id)
- )
- raise HTTPError(http_status.HTTP_404_NOT_FOUND)
-
- if notification_type == 'adopt_parent':
- sentry.log_message(
- f'{user!r} attempted to adopt_parent of a none node id, {target_id}'
- )
- raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
- # owner = user
- else:
- if not node.has_permission(user, READ):
- sentry.log_message(f'{user!r} attempted to subscribe to private node, {target_id}')
- raise HTTPError(http_status.HTTP_403_FORBIDDEN)
-
- if isinstance(node, Registration):
- sentry.log_message(
- f'{user!r} attempted to subscribe to registration, {target_id}'
- )
- raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
-
- if notification_type != 'adopt_parent':
- pass
- # owner = node
- else:
- if 'file_updated' in event and len(event) > len('file_updated'):
- pass
- else:
- parent = node.parent_node
- if not parent:
- sentry.log_message(
- '{!r} attempted to adopt_parent of '
- 'the parentless project, {!r}'.format(user, node)
- )
- raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
-
- # If adopt_parent make sure that this subscription is None for the current User
- subscription, _ = NotificationSubscription.objects.get_or_create(
- user=user,
- subscribed_object=node,
- notification_type__name=event
- )
- if not subscription:
- return {} # We're done here
-
- subscription.delete()
- return {}
-
- subscription, _ = NotificationSubscription.objects.get_or_create(
- user=user,
- notification_type__name=event
- )
- subscription.save()
-
- return {'message': f'Successfully subscribed to {notification_type} list on {event_id}'}
diff --git a/website/profile/views.py b/website/profile/views.py
index fb03f52fa5c..37572b00366 100644
--- a/website/profile/views.py
+++ b/website/profile/views.py
@@ -26,7 +26,7 @@
from framework.utils import throttle_period_expired
from osf import features
-from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, OSFUser, NotificationType
+from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, OSFUser, NotificationTypeEnum
from osf.exceptions import BlockedEmailError, OSFError
from osf.utils.requests import string_type_request_headers
from website import mailchimp_utils
@@ -187,13 +187,12 @@ def update_user(auth):
# make sure the new username has already been confirmed
if username and username != user.username and user.emails.filter(address=username).exists():
- NotificationType.Type.USER_PRIMARY_EMAIL_CHANGED.instance.emit(
+ NotificationTypeEnum.USER_PRIMARY_EMAIL_CHANGED.instance.emit(
subscribed_object=user,
user=user,
event_context={
'user_fullname': user.fullname,
'new_address': username,
- 'can_change_preferences': False,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
}
)
@@ -798,13 +797,12 @@ def request_export(auth):
data={'message_long': 'Too many requests. Please wait a while before sending another account export request.',
'error_type': 'throttle_error'})
- NotificationType.Type.DESK_REQUEST_EXPORT.instance.emit(
+ NotificationTypeEnum.DESK_REQUEST_EXPORT.instance.emit(
user=user,
event_context={
'user_username': user.username,
'user_absolute_url': user.absolute_url,
'user__id': user._id,
- 'can_change_preferences': False,
}
)
user.email_last_sent = timezone.now()
diff --git a/website/project/views/contributor.py b/website/project/views/contributor.py
index 6bacb1b158d..1e2edc1cefe 100644
--- a/website/project/views/contributor.py
+++ b/website/project/views/contributor.py
@@ -26,7 +26,7 @@
Preprint,
PreprintProvider,
RecentlyAddedContributor,
- NotificationType
+ NotificationTypeEnum
)
from osf.utils import sanitize
from osf.utils.permissions import ADMIN
@@ -214,7 +214,7 @@ def finalize_invitation(
node,
contributor,
auth,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
):
try:
record = contributor.get_unclaimed_record(node._primary_key)
@@ -436,7 +436,7 @@ def send_claim_registered_email(claimer, unclaimed_user, node, throttle=24 * 360
)
if check_email_throttle(
referrer,
- notification_type=NotificationType.Type.USER_FORWARD_INVITE_REGISTERED,
+ notification_type=NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED,
throttle=throttle
):
raise HTTPError(
@@ -447,29 +447,24 @@ def send_claim_registered_email(claimer, unclaimed_user, node, throttle=24 * 360
)
# Send mail to referrer, telling them to forward verification link to claimer
- NotificationType.Type.USER_FORWARD_INVITE_REGISTERED.instance.emit(
+ NotificationTypeEnum.USER_FORWARD_INVITE_REGISTERED.instance.emit(
user=referrer,
event_context={
'claim_url': claim_url,
'referrer_fullname': referrer.fullname,
'user_fullname': unclaimed_record['name'],
'node_title': node.title,
- 'can_change_preferences': False,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
}
)
# Send mail to claimer, telling them to wait for referrer
- NotificationType.Type.USER_PENDING_VERIFICATION_REGISTERED.instance.emit(
+ NotificationTypeEnum.USER_PENDING_VERIFICATION_REGISTERED.instance.emit(
subscribed_object=claimer,
user=claimer,
event_context={
- 'claim_url': claim_url,
'user_fullname': unclaimed_record['name'],
- 'referrer_username': referrer.username,
'referrer_fullname': referrer.fullname,
'node_title': node.title,
- 'can_change_preferences': False,
- 'osf_contact_email': settings.OSF_CONTACT_EMAIL,
}
)
@@ -479,7 +474,7 @@ def send_claim_email(
node,
notify=True,
throttle=24 * 3600,
- notification_type=NotificationType.Type.NODE_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type=NotificationTypeEnum.NODE_CONTRIBUTOR_ADDED_DEFAULT
):
"""
Send a claim email to an unregistered contributor or the referrer, depending on the scenario.
@@ -508,15 +503,15 @@ def send_claim_email(
match notification_type:
case 'preprint':
if getattr(node.provider, 'is_default', False):
- notification_type = NotificationType.Type.USER_INVITE_OSF_PREPRINT
+ notification_type = NotificationTypeEnum.USER_INVITE_OSF_PREPRINT
logo = settings.OSF_PREPRINTS_LOGO
else:
- notification_type = NotificationType.Type.PROVIDER_USER_INVITE_PREPRINT
+ notification_type = NotificationTypeEnum.PROVIDER_USER_INVITE_PREPRINT
logo = getattr(node.provider, '_id', None)
case 'draft_registration':
- notification_type = NotificationType.Type.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
+ notification_type = NotificationTypeEnum.DRAFT_REGISTRATION_CONTRIBUTOR_ADDED_DEFAULT
case _:
- notification_type = NotificationType.Type.USER_INVITE_DEFAULT
+ notification_type = NotificationTypeEnum.USER_INVITE_DEFAULT
unclaimed_record['claimer_email'] = claimer_email
unclaimed_user.save()
@@ -539,7 +534,7 @@ def send_claim_email(
unclaimed_user.save()
if notify:
- NotificationType.Type.USER_PENDING_VERIFICATION.instance.emit(
+ NotificationTypeEnum.USER_PENDING_VERIFICATION.instance.emit(
subscribed_object=unclaimed_user,
user=unclaimed_user,
event_context={
@@ -547,26 +542,21 @@ def send_claim_email(
'user_fullname': unclaimed_record['name'],
'node_title': node.title,
'logo': logo,
- 'can_change_preferences': False,
+ 'node_absolute_url': node.absolute_url,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
}
)
- notification_type = NotificationType.Type.USER_FORWARD_INVITE
+ notification_type = NotificationTypeEnum.USER_FORWARD_INVITE
claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True)
notification_type.instance.emit(
user=referrer,
destination_address=email,
event_context={
- 'user_fullname': referrer.id,
- 'referrer_name': referrer.fullname,
+ 'user_fullname': unclaimed_record['name'],
'referrer_fullname': referrer.fullname,
- 'fullname': unclaimed_record['name'],
- 'node_url': node.url,
- 'logo': logo,
'claim_url': claim_url,
- 'can_change_preferences': False,
'domain': settings.DOMAIN,
'node_absolute_url': node.absolute_url,
'node_title': node.title,
@@ -626,7 +616,7 @@ def notify_added_contributor(resource, contributor, notification_type, auth=None
logo = settings.OSF_LOGO
if getattr(resource, 'has_linked_published_preprints', None):
- notification_type = NotificationType.Type.PREPRINT_CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF
+ notification_type = NotificationTypeEnum.PREPRINT_CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF
logo = settings.OSF_PREPRINTS_LOGO
throttle = kwargs.get('throttle', settings.CONTRIBUTOR_ADDED_EMAIL_THROTTLE)
@@ -642,6 +632,7 @@ def notify_added_contributor(resource, contributor, notification_type, auth=None
subscribed_object=resource,
event_context={
'user_fullname': contributor.fullname,
+ 'referrer_fullname': referrer_name,
'referrer_text': referrer_name + ' has added you as a contributor' if referrer_name else 'You have been added',
'registry_text': resource.provider.name if resource.provider else 'OSF Registry',
'referrer_name': referrer_name,
@@ -655,7 +646,6 @@ def notify_added_contributor(resource, contributor, notification_type, auth=None
'node_provider__id': getattr(resource.provider, '_id', None),
'node_absolute_url': resource.absolute_url,
'node_has_permission_admin': resource.has_permission(user=contributor, permission='admin'),
- 'can_change_preferences': False,
'logo': logo,
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
'preprint_list': ''.join(f"- {p['absolute_url']}\n" for p in serialize_preprints(resource, user=None)) if isinstance(resource, Node) else '- (none)\n',
diff --git a/website/reviews/listeners.py b/website/reviews/listeners.py
index 0e8f8ee4799..83fc702691b 100644
--- a/website/reviews/listeners.py
+++ b/website/reviews/listeners.py
@@ -29,7 +29,7 @@ def reviews_withdraw_requests_notification_moderators(self, timestamp, context,
context['requester_fullname'] = user.fullname
context['profile_image_url'] = get_profile_image_url(resource.creator)
provider = resource.provider
- from osf.models import NotificationType
+ from osf.models import NotificationTypeEnum
context['message'] = f'has requested withdrawal of "{resource.title}".'
context['reviews_submission_url'] = f'{DOMAIN}{resource._id}?mode=moderator'
@@ -41,7 +41,7 @@ def reviews_withdraw_requests_notification_moderators(self, timestamp, context,
context['recipient_fullname'] = recipient.fullname
context['localized_timestamp'] = str(timestamp)
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
user=recipient,
subscribed_object=provider,
event_context=context,
@@ -56,7 +56,7 @@ def reviews_withdrawal_requests_notification(self, timestamp, context):
context['reviewable_absolute_url'] = preprint.absolute_url
context['reviewable_title'] = preprint.title
context['reviewable__id'] = preprint._id
- from osf.models import NotificationType
+ from osf.models import NotificationTypeEnum
preprint_word = preprint.provider.preprint_word
context['message'] = f'has requested withdrawal of the {preprint_word} "{preprint.title}".'
@@ -69,7 +69,7 @@ def reviews_withdrawal_requests_notification(self, timestamp, context):
context['recipient_fullname'] = recipient.fullname
context['localized_timestamp'] = str(timestamp)
- NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS.instance.emit(
user=recipient,
event_context=context,
subscribed_object=preprint.provider,
@@ -106,7 +106,7 @@ def reviews_submit_notification_moderators(self, timestamp, resource, context):
else:
context['message'] = f'submitted "{resource.title}".'
- from osf.models import NotificationType
+ from osf.models import NotificationTypeEnum
context['requester_contributor_names'] = ''.join(resource.contributors.values_list('fullname', flat=True))
context['localized_timestamp'] = str(timezone.now())
@@ -117,7 +117,7 @@ def reviews_submit_notification_moderators(self, timestamp, resource, context):
context['requester_fullname'] = recipient.fullname
context['is_request_email'] = False
- NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
+ NotificationTypeEnum.PROVIDER_NEW_PENDING_SUBMISSIONS.instance.emit(
user=recipient,
subscribed_object=provider,
event_context=context,
diff --git a/website/routes.py b/website/routes.py
index 80b0d8bec92..d7d6cf3d9bc 100644
--- a/website/routes.py
+++ b/website/routes.py
@@ -56,7 +56,6 @@
from website.registries import views as registries_views
from website.reviews import views as reviews_views
from website.institutions import views as institution_views
-from website.notifications import views as notification_views
from website.ember_osf_web import views as ember_osf_web_views
from website.closed_challenges import views as closed_challenges_views
from website.identifiers import views as identifier_views
@@ -1712,22 +1711,24 @@ def make_url_map(app):
json_renderer,
),
- Rule(
- '/subscriptions/',
- 'get',
- notification_views.get_subscriptions,
- json_renderer,
- ),
+ # Legacy v1 API for notifications, which is no longer used by Angular/Post-NR
+ # Rule(
+ # '/subscriptions/',
+ # 'get',
+ # notification_views.get_subscriptions,
+ # json_renderer,
+ # ),
- Rule(
- [
- '/project//subscriptions/',
- '/project//node//subscriptions/'
- ],
- 'get',
- notification_views.get_node_subscriptions,
- json_renderer,
- ),
+ # Legacy v1 API for notifications, which is no longer used by Angular/Post-NR
+ # Rule(
+ # [
+ # '/project//subscriptions/',
+ # '/project//node//subscriptions/'
+ # ],
+ # 'get',
+ # notification_views.get_node_subscriptions,
+ # json_renderer,
+ # ),
Rule(
[
@@ -1739,12 +1740,13 @@ def make_url_map(app):
json_renderer,
),
- Rule(
- '/subscriptions/',
- 'post',
- notification_views.configure_subscription,
- json_renderer,
- ),
+ # Legacy v1 API for notifications, which is no longer used by Angular/Post-NR
+ # Rule(
+ # '/subscriptions/',
+ # 'post',
+ # notification_views.configure_subscription,
+ # json_renderer,
+ # ),
Rule(
[
diff --git a/website/settings/defaults.py b/website/settings/defaults.py
index d09e583c181..c26ba90b406 100644
--- a/website/settings/defaults.py
+++ b/website/settings/defaults.py
@@ -186,8 +186,9 @@ def parent_dir(path):
NO_ADDON_WAIT_TIME = timedelta(weeks=8) # 2 months for "Link an add-on to your OSF project" email
NO_LOGIN_WAIT_TIME = timedelta(weeks=52) # 1 year for "We miss you at OSF" email
NO_LOGIN_OSF4M_WAIT_TIME = timedelta(weeks=52) # 1 year for "We miss you at OSF" email to users created from OSF4M
+NOTIFICATIONS_CLEANUP_AGE = timedelta(weeks=52) # 1 month to clean up old notifications and email tasks
-# Configuration for "We miss you at OSF" email (`NotificationType.Type.USER_NO_LOGIN`)
+# Configuration for "We miss you at OSF" email (`NotificationTypeEnum.USER_NO_LOGIN`)
# Note: 1) we can gradually increase `MAX_DAILY_NO_LOGIN_EMAILS` to 10000, 100000, etc. or set it to `None` after we
# have verified that users are not spammed by this email after NR release. 2) If we want to clean up database for those
# already sent `USER_NO_LOGIN` emails, we need to adjust the cut-off time to the day we clean the DB.
@@ -435,6 +436,9 @@ class CeleryConfig:
'scripts.populate_new_and_noteworthy_projects',
'website.search.elastic_search',
'scripts.generate_sitemap',
+ 'scripts.remove_after_use.populate_notification_subscriptions_node_file_updated',
+ 'scripts.remove_after_use.populate_notification_subscriptions_user_global_file_updated',
+ 'scripts.remove_after_use.populate_notification_subscriptions_user_global_reviews',
'osf.management.commands.clear_expired_sessions',
'osf.management.commands.delete_withdrawn_or_failed_registration_files',
'osf.management.commands.migrate_pagecounter_data',
@@ -450,7 +454,7 @@ class CeleryConfig:
'osf.management.commands.monthly_reporters_go',
'osf.management.commands.ingest_cedar_metadata_templates',
'osf.metrics.reporters',
- 'scripts.populate_notification_subscriptions',
+ 'scripts.remove_after_use.merge_notification_subscription_provider_ct',
}
med_pri_modules = {
@@ -564,6 +568,9 @@ class CeleryConfig:
'scripts.approve_embargo_terminations',
'scripts.triggered_mails',
'scripts.generate_sitemap',
+ 'scripts.remove_after_use.populate_notification_subscriptions_node_file_updated',
+ 'scripts.remove_after_use.populate_notification_subscriptions_user_global_file_updated',
+ 'scripts.remove_after_use.populate_notification_subscriptions_user_global_reviews',
'scripts.premigrate_created_modified',
'scripts.add_missing_identifiers_to_preprints',
'osf.management.commands.clear_expired_sessions',
@@ -579,7 +586,8 @@ class CeleryConfig:
'osf.management.commands.monthly_reporters_go',
'osf.external.spam.tasks',
'api.share.utils',
- 'scripts.populate_notification_subscriptions',
+ 'scripts.remove_after_use.merge_notification_subscription_provider_ct',
+ 'scripts.disable_removed_beat_tasks',
)
# Modules that need metrics and release requirements
@@ -593,6 +601,10 @@ class CeleryConfig:
# Setting up a scheduler, essentially replaces an independent cron job
# Note: these times must be in UTC
beat_schedule = {
+ 'disable_removed_beat_tasks': {
+ 'task': 'scripts.disable_removed_beat_tasks',
+ 'schedule': crontab(minute=0, hour=0), # Daily 8:00 PM EDT
+ },
'retract_registrations': {
'task': 'scripts.retract_registrations',
'schedule': crontab(minute=0, hour=5), # Daily 12 a.m
@@ -648,6 +660,11 @@ class CeleryConfig:
'schedule': crontab(minute=0, hour=5), # Daily 12 a.m
'kwargs': {'dry_run': False},
},
+ 'notifications_cleanup_task': {
+ 'task': 'notifications.tasks.notifications_cleanup_task',
+ 'schedule': crontab(minute=0, hour=7), # Daily 2 a.m
+ 'kwargs': {'dry_run': False},
+ },
'clear_expired_sessions': {
'task': 'osf.management.commands.clear_expired_sessions',
'schedule': crontab(minute=0, hour=5), # Daily 12 a.m
diff --git a/website/settings/local-ci.py b/website/settings/local-ci.py
index 022a973b35a..b1754ef0191 100644
--- a/website/settings/local-ci.py
+++ b/website/settings/local-ci.py
@@ -81,10 +81,12 @@ class CeleryConfig(defaults.CeleryConfig):
NO_LOGIN_WAIT_TIME = timedelta(weeks=4)
NO_LOGIN_OSF4M_WAIT_TIME = timedelta(weeks=6)
-# Configuration for "We miss you at OSF" email (`NotificationType.Type.USER_NO_LOGIN`)
+# Configuration for "We miss you at OSF" email (`NotificationTypeEnum.USER_NO_LOGIN`)
MAX_DAILY_NO_LOGIN_EMAILS = None
NO_LOGIN_EMAIL_CUTOFF = None
+NOTIFICATIONS_CLEANUP_AGE = timedelta(weeks=4) # 1 month to clean up old notifications and email tasks
+
USE_CDN_FOR_CLIENT_LIBS = False
SENTRY_DSN = None
diff --git a/website/templates/invite_default.html.mako b/website/templates/invite_default.html.mako
index f739f11ad7e..9bf188212ef 100644
--- a/website/templates/invite_default.html.mako
+++ b/website/templates/invite_default.html.mako
@@ -8,7 +8,7 @@
%>
Hello ${user_fullname},
- You have been added by ${referrer_name} as a contributor to the project ${node_title} on the Open Science Framework.
+ You have been added by ${referrer_fullname} as a contributor to the project ${node_title} on the Open Science Framework.
Click here to set a password for your account.
diff --git a/website/templates/node_request_institutional_access_request.html.mako b/website/templates/node_request_institutional_access_request.html.mako
index 8ef9529f0ee..8cfae57cdd0 100644
--- a/website/templates/node_request_institutional_access_request.html.mako
+++ b/website/templates/node_request_institutional_access_request.html.mako
@@ -3,7 +3,7 @@
<%def name="content()">
|
- Hello ${recipient_fullname},
+ Hello ${recipient_username},
${sender_fullname} has requested access to ${node_title}.
|