diff --git a/addons/base/views.py b/addons/base/views.py index 92f930c085b..796f57e3f67 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/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/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..4433e2d3b30 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 @@ -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_CAMPAIGN_CONFIRM_PREPRINTS_OSF name = 'OSF' url_path = 'preprints/' external_url = None else: - confirmation_email_template = NotificationType.Type.USER_CAMPAIGN_CONFIRM_PREPRINTS_BRANDED + confirmation_email_template = NotificationTypeEnum.USER_CAMPAIGN_CONFIRM_PREPRINTS_BRANDED 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_CAMPAIGN_CONFIRM_EMAIL_AGU_CONFERENCE_2023, '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..912d0c55668 100644 --- a/notifications.yaml +++ b/notifications.yaml @@ -455,15 +455,8 @@ notification_types: 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' 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..89ea61e5bcb 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,10 +123,9 @@ 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, } 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_notification_refactor_post_release.py b/osf/migrations/0036_notification_refactor_post_release.py new file mode 100644 index 00000000000..9a8ccb779f5 --- /dev/null +++ b/osf/migrations/0036_notification_refactor_post_release.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.17 on 2026-01-27 21:03 + +from django.db import migrations, models +import osf.utils.fields + +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), + 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..b197b7980fe 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,19 +135,16 @@ 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( + 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, @@ -167,7 +164,7 @@ def _validate_accept(self, event_data): def _notify_accepted(self, event_data): if self.collection.provider: 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={ @@ -211,7 +208,7 @@ def _validate_reject(self, event_data): def _notify_moderated_rejected(self, event_data): 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={ @@ -284,7 +281,7 @@ def _notify_removed(self, event_data): 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 +290,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 +300,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': '', 'is_admin': node.has_permission(contributor, ADMIN), - 'reviews_submission_url': f'{DOMAIN}reviews/registries/{self.guid.referent._id}/{self.guid.referent._id}', - }, ) @@ -388,33 +369,22 @@ 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(), 'remover_fullname': user.fullname if user else '', 'remover_absolute_url': user.get_absolute_url() if user else '', - 'localized_timestamp': str(timezone.now()), '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..369655239ef 100644 --- a/osf/models/notification_type.py +++ b/osf/models/notification_type.py @@ -12,166 +12,164 @@ 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_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_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 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 +224,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 +231,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 +289,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/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..81da30645d3 100644 --- a/scripts/triggered_mails.py +++ b/scripts/triggered_mails.py @@ -6,7 +6,7 @@ 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,29 @@ 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 + return base_q.filter( + Q(no_login_email_last_sent__isnull=True) | + Q(no_login_email_last_sent__lt=now - settings.NO_LOGIN_WAIT_TIME) + ) @celery_app.task(name='scripts.triggered_no_login_email') @@ -124,7 +122,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 +131,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 14afa96b3b0..72c20262c48 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..30f6159ed8c 100644 --- a/tests/test_triggered_mails.py +++ b/tests/test_triggered_mails.py @@ -10,7 +10,7 @@ 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() - timedelta(weeks=52) 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,21 +114,15 @@ 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() + u2.no_login_email_last_sent = timezone.now() 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', - ) - 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 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..8b2e77f8a57 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,7 @@ 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', ) # Modules that need metrics and release requirements @@ -648,6 +655,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}.