Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
88103af
[ENG-10083] Add type<-->is_digest "relation" and update subscription …
cslzchen Feb 4, 2026
47e26fd
Add notification subscriptions de-duplication v2 command (#11576)
cslzchen Feb 4, 2026
fa2c953
[ENG-10190][ENG-10214] Permanently update a couple of templates to ma…
Ostap-Zherebetskyi Feb 4, 2026
b98fa97
Update changelog and bump version
cslzchen Feb 9, 2026
c4b83ec
Merge branch 'hotfix/26.2.1'
cslzchen Feb 9, 2026
78eabe7
Merge tag '26.2.1' into develop
cslzchen Feb 9, 2026
28afd65
Exclude spam and deleted Registration from queryset (#11572)
Vlad0n20 Feb 10, 2026
6528598
[ENG-10135] - Fix/eng 10135 (#11587)
Vlad0n20 Feb 13, 2026
4e23670
Merge branch 'hotfix/26.2.2'
adlius Feb 17, 2026
613ff26
Merge tag '26.2.2' into develop
adlius Feb 17, 2026
79c679d
Feature/fair signposting (#11599)
futa-ikeda Feb 24, 2026
247494c
Bump versio no. Add CHANGELOG
adlius Feb 24, 2026
75ecc60
Merge branch 'release/26.3.0'
adlius Feb 24, 2026
c9801c1
Merge tag '26.3.0' into develop
adlius Feb 24, 2026
626b62e
[ENG-10054] feature/ror-migration (#11610)
felliott Feb 26, 2026
194fa0c
bump version & update changelog
felliott Feb 26, 2026
745cb3c
Merge branch 'master' into develop
felliott Feb 26, 2026
32fe971
add command to migrate ror funder names
felliott Mar 1, 2026
0a4e198
Merge pull request #11615 from felliott/hotfix/ror-migration-names
felliott Mar 1, 2026
d4d8c2d
bump version & update changelog
felliott Mar 1, 2026
5466cdc
Merge branch 'hotfix/26.4.1'
felliott Mar 1, 2026
afbbdae
Merge tag '26.4.1' into develop
felliott Mar 1, 2026
31048c7
[ENG-10538] Post-NR Project PR (#11623)
Ostap-Zherebetskyi Mar 13, 2026
0e8f292
Updated cut-off time and added comments for NR settings
cslzchen Mar 13, 2026
991d844
Update changelog and bump version
cslzchen Mar 13, 2026
2972e89
Merge branch 'release/26.5.0'
cslzchen Mar 13, 2026
231bf1b
Merge tag '26.5.0' into develop
cslzchen Mar 13, 2026
cf680a4
Merge remote-tracking branch 'upstream/develop' into feature/pbs-26-2
adlius Mar 18, 2026
a9f99d8
resolve merge conflicts
adlius Mar 18, 2026
04fc20a
respond to CR comments
adlius Mar 19, 2026
631b1c4
fix tests
adlius Mar 19, 2026
f8f4303
fix tests again
adlius Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.

26.5.0 (2026-03-13)
===================

- Notification refactor post-release project (Post-NR)

26.4.1 (2026-03-01)
===================

- Add script to update ROR funder names

26.4.0 (2026-02-26)
===================

- Transition funder ids from CrossRef to ROR

26.3.0 (2026-02-24)
===================

- FAIR Signposting

26.2.1 (2026-02-09)
===================

- Permanently update two notification types
- Link digest to types and fix/log incorrect usage
- Update notifiction dedupe command

26.2.0
======

- TODO: add date and log

26.1.6 (2026-01-14)
===================

Expand Down
16 changes: 9 additions & 7 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
DraftRegistration,
Guid,
FileVersionUserMetadata,
FileVersion, NotificationType
FileVersion, NotificationTypeEnum
)
from osf.metrics import PreprintView, PreprintDownload
from osf.utils import permissions
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1035,9 +1034,12 @@ def persistent_file_download(auth, **kwargs):

query_params = request.args.to_dict()

return redirect(
file.generate_waterbutler_url(**query_params),
code=http_status.HTTP_302_FOUND
return make_response(
'', http_status.HTTP_302_FOUND, {
'Location': file.generate_waterbutler_url(**query_params),
'Link': f'<{settings.DOMAIN}metadata/{id_or_guid}/?format=linkset> ; rel="linkset" ; type="application/linkset",'
f' <{settings.DOMAIN}metadata/{id_or_guid}/?format=linkset-json"> ; rel="linkset-json" ; type="application/linkset+json"',
}
)


Expand Down
10 changes: 3 additions & 7 deletions addons/boa/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions addons/boa/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions addons/osfstorage/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand Down
10 changes: 9 additions & 1 deletion addons/osfstorage/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1539,11 +1539,19 @@ def test_download_file(self):
# Test download works with path
url = base_url.format(file._id)
redirect = self.app.get(url, auth=self.user.auth)
link_header = (f'<{settings.DOMAIN}metadata/{file._id}/?format=linkset> ; rel="linkset" ; type="application/linkset", '
f'<{settings.DOMAIN}metadata/{file._id}/?format=linkset-json"> ; rel="linkset-json" ; type="application/linkset+json"')
assert link_header == redirect.headers['Link']
assert redirect.status_code == 302

# Test download works with guid
url = base_url.format(file.get_guid(create=True)._id)
guid = file.get_guid(create=True)._id
url = base_url.format(guid)
redirect = self.app.get(url, auth=self.user.auth)
link_header = (
f'<{settings.DOMAIN}metadata/{guid}/?format=linkset> ; rel="linkset" ; type="application/linkset", '
f'<{settings.DOMAIN}metadata/{guid}/?format=linkset-json"> ; rel="linkset-json" ; type="application/linkset+json"')
assert link_header == redirect.headers['Link']
assert redirect.status_code == 302

# Test nonexistent file 404's
Expand Down
4 changes: 3 additions & 1 deletion admin/management/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
re_path(r'^ingest_cedar_metadata_templates', views.IngestCedarMetadataTemplates.as_view(), name='ingest_cedar_metadata_templates'),
re_path(r'^bulk_resync', views.BulkResync.as_view(), name='bulk-resync'),
re_path(r'^empty_metadata_dataarchive_registration_bulk_resync', views.EmptyMetadataDataarchiveRegistrationBulkResync.as_view(),
name='empty-metadata-dataarchive-registration-bulk-resync')
name='empty-metadata-dataarchive-registration-bulk-resync'),
re_path(r'^sync_notification_templates', views.SyncNotificationTemplates.as_view(),
name='sync_notification_templates')
]
9 changes: 9 additions & 0 deletions admin/management/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from osf.management.commands.monthly_reporters_go import monthly_reporters_go
from osf.management.commands.fetch_cedar_metadata_templates import ingest_cedar_metadata_templates
from osf.management.commands.sync_doi_metadata import sync_doi_metadata, sync_doi_empty_metadata_dataarchive_registrations
from osf.management.commands.populate_notification_types import populate_notification_types
from scripts.find_spammy_content import manage_spammy_content
from django.urls import reverse
from django.shortcuts import redirect
Expand Down Expand Up @@ -172,3 +173,11 @@ def post(self, request):
})
messages.success(request, 'Resyncing with DataCite! It will take some time.')
return redirect(reverse('management:commands'))


class SyncNotificationTemplates(ManagementCommandPermissionView):

def post(self, request):
populate_notification_types()
messages.success(request, 'Notification templates have been successfully synced.')
return redirect(reverse('management:commands'))
4 changes: 2 additions & 2 deletions admin/providers/views.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions admin/templates/management/commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ <h4><u>Resync empty metadata dataarchive registrations with DataCite</u></h4>
</nav>
</form>
</section>
<section>
<h4><u>Sync Notification Templates</u></h4>
<p>
Use this management command to sync notification templates. Warning: existing templates modifications via django admin will be overridden if they haven't been updated in code. In addition, templates are cached for 2 hours so changes won't be effective immediately.
</p>
<form method="post"
action="{% url 'management:sync_notification_templates'%}">
{% csrf_token %}
<nav>
<input class="btn btn-success" type="submit" value="Run" />
</nav>
</form>
</section>
</div>
</section>
{% endblock %}
5 changes: 2 additions & 3 deletions admin/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
from osf.models.institution import Institution
Expand Down Expand Up @@ -186,12 +186,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:
Expand Down
4 changes: 2 additions & 2 deletions admin_tests/preprints/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down
10 changes: 5 additions & 5 deletions admin_tests/users/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -168,15 +168,15 @@ 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
assert AdminLogEntry.objects.count() == count + 1

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)
Expand Down Expand Up @@ -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

Expand Down
11 changes: 8 additions & 3 deletions api/cedar_metadata_records/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
CedarMetadataRecordsDetailSerializer,
)
from framework.auth.oauth_scopes import CoreScopes

from osf.models import CedarMetadataRecord
from osf.models import CedarMetadataRecord, Node, Registration
from website import settings

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -99,5 +99,10 @@ def get_serializer_class(self):

def get(self, request, *args, **kwargs):
record = self.get_object()
is_referent_project_or_registration = isinstance(record.guid.referent, (Node, Registration))
file_name = f'{record._id}-{record.get_template_name()}-v{record.get_template_version()}.json'
return Response(record.metadata, headers={'Content-Disposition': f'attachment; filename={file_name}'})
headers = {'Content-Disposition': f'attachment; filename={file_name}'}
if is_referent_project_or_registration:
guid_id = record.guid._id
headers['link'] = f'<{settings.DOMAIN}{guid_id}/>; rel="describes"; type="text/html"'
return Response(record.metadata, headers=headers)
4 changes: 2 additions & 2 deletions api/crossref/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading