From e40af2398e507151aabfa44e19ba8a163259fd06 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Tue, 12 May 2026 21:13:08 +0300 Subject: [PATCH 1/5] ENG-9827 --- api/collections/serializers.py | 18 +++---- api_tests/collections/test_views.py | 79 +++++++++++++++++++++++++---- osf/models/collection_submission.py | 19 +++++++ 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/api/collections/serializers.py b/api/collections/serializers.py index 2f24a2eb56a..948f09b29f1 100644 --- a/api/collections/serializers.py +++ b/api/collections/serializers.py @@ -300,6 +300,8 @@ def update(self, obj, validated_data): obj.grade_levels = validated_data.pop('grade_levels') obj.save() + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): + obj.sync_cedar_metadata() return obj @@ -405,6 +407,8 @@ def update(self, obj, validated_data): obj.grade_levels = validated_data.pop('grade_levels') obj.save() + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): + obj.sync_cedar_metadata() return obj @@ -429,15 +433,12 @@ def create(self, validated_data): raise exceptions.ValidationError('"creator" must be specified.') if not (creator.has_perm('write_collection', collection) or (hasattr(guid.referent, 'has_permission') and guid.referent.has_permission(creator, WRITE))): raise exceptions.PermissionDenied('Must have write permission on either collection or collected object to collect.') - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR) and collection.provider_id: - try: - collection.provider.validate_required_metadata(guid.referent) - except ValidationError as e: - raise InvalidModelValueError(e.message) try: obj = collection.collect_object(guid.referent, creator, **validated_data) except ValidationError as e: raise InvalidModelValueError(e.message) + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): + obj.sync_cedar_metadata() if subjects: auth = get_user_auth(self.context['request']) try: @@ -470,15 +471,12 @@ def create(self, validated_data): raise exceptions.ValidationError('"creator" must be specified.') if not (creator.has_perm('write_collection', collection) or (hasattr(guid.referent, 'has_permission') and guid.referent.has_permission(creator, WRITE))): raise exceptions.PermissionDenied('Must have write permission on either collection or collected object to collect.') - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR) and collection.provider_id: - try: - collection.provider.validate_required_metadata(guid.referent) - except ValidationError as e: - raise InvalidModelValueError(e.message) try: obj = collection.collect_object(guid.referent, creator, **validated_data) except ValidationError as e: raise InvalidModelValueError(e.message) + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): + obj.sync_cedar_metadata() if subjects: auth = get_user_auth(self.context['request']) try: diff --git a/api_tests/collections/test_views.py b/api_tests/collections/test_views.py index 7ae088ab5a7..a4564c7e88d 100644 --- a/api_tests/collections/test_views.py +++ b/api_tests/collections/test_views.py @@ -10,6 +10,7 @@ from api_tests.subjects.mixins import UpdateSubjectsMixin, SubjectsFilterMixin, SubjectsListMixin, \ SubjectsRelationshipMixin from api_tests.utils import disconnected_from_listeners +from tests.utils import capture_notifications from framework.auth.core import Auth from osf import features from osf.models import Collection, VersionedGuidMixin @@ -4450,16 +4451,76 @@ def test_switch_active_no_provider_submission_succeeds(self, app, user_one, proj ) assert res.status_code == 201 - def test_switch_active_missing_cedar_record_submission_fails(self, app, user_one, project, url, payload): + def test_switch_active_no_provider_no_cedar_record_created(self, app, user_one, project, url_no_provider, payload): + from osf.models import CedarMetadataRecord + with mock_update_share(): + with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): + app.post_json_api(url_no_provider, payload(guid=project._id), auth=user_one.auth) + assert not CedarMetadataRecord.objects.filter(guid__in=project.guids.all()).exists() + + def test_switch_active_submission_creates_cedar_record(self, app, user_one, project, url, payload, cedar_template): + from osf.models import CedarMetadataRecord + with capture_notifications(): + with mock_update_share(): + with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): + res = app.post_json_api( + url, + payload(guid=project._id), + auth=user_one.auth, + ) + assert res.status_code == 201 + record = CedarMetadataRecord.objects.filter( + guid__in=project.guids.all(), + template=cedar_template, + is_published=True, + ).first() + assert record is not None + + def test_switch_active_submission_with_custom_fields_syncs_cedar_metadata( + self, app, user_one, project, url, payload, cedar_template, collection): + from osf.models import CedarMetadataRecord + collection.status_choices = ['pending'] + collection.volume_choices = ['1'] + collection.save() + with capture_notifications(): + with mock_update_share(): + with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): + app.post_json_api( + url, + payload(guid=project._id, status='pending', volume='1'), + auth=user_one.auth, + ) + record = CedarMetadataRecord.objects.get(guid__in=project.guids.all(), template=cedar_template) + assert record.metadata['status'] == 'pending' + assert record.metadata['volume'] == '1' + + def test_switch_inactive_submission_does_not_create_cedar_record( + self, app, user_one, project, url, payload, cedar_template): + from osf.models import CedarMetadataRecord + with capture_notifications(): + with mock_update_share(): + with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=False): + res = app.post_json_api(url, payload(guid=project._id), auth=user_one.auth) + assert res.status_code == 201 + assert not CedarMetadataRecord.objects.filter(guid__in=project.guids.all()).exists() + + def test_switch_active_update_syncs_cedar_metadata( + self, app, user_one, project, url, payload, cedar_template, collection, provider): + from osf.models import CedarMetadataRecord + collection.status_choices = ['pending', 'approved'] + collection.save() + with capture_notifications(): + with mock_update_share(): + with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): + res = app.post_json_api(url, payload(guid=project._id, status='pending'), auth=user_one.auth) + assert res.status_code == 201 + + detail_url = f'/{API_BASE}collections/{collection._id}/collected_metadata/{project._id}/' with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): - res = app.post_json_api( - url, - payload(guid=project._id), - auth=user_one.auth, - expect_errors=True, - ) - assert res.status_code == 400 - assert 'CEDAR metadata record' in res.json['errors'][0]['detail'] + app.patch_json_api(detail_url, payload(status='approved'), auth=user_one.auth) + + record = CedarMetadataRecord.objects.get(guid__in=project.guids.all(), template=cedar_template) + assert record.metadata['status'] == 'approved' class TestCollectedMetaSubjectFiltering(SubjectsFilterMixin): diff --git a/osf/models/collection_submission.py b/osf/models/collection_submission.py index f2de5ba6610..b971f1d4658 100644 --- a/osf/models/collection_submission.py +++ b/osf/models/collection_submission.py @@ -22,6 +22,12 @@ logger = logging.getLogger(__name__) +CEDAR_METADATA_FIELDS = [ + 'collected_type', 'status', 'volume', 'issue', + 'program_area', 'school_type', 'study_design', + 'data_type', 'disease', 'grade_levels', +] + class CollectionSubmission(TaxonomizableMixin, BaseModel): primary_identifier_name = 'guid___id' @@ -94,6 +100,19 @@ def is_submitted_by_moderator_contributor(self, event_data): def state(self, new_state): self.machine_state = new_state.value + def sync_cedar_metadata(self): + """Create or update a CedarMetadataRecord from this submission's custom metadata fields.""" + + from osf.models import CedarMetadataRecord + if not (self.collection.provider_id and self.collection.provider.required_metadata_template): + return + template = self.collection.provider.required_metadata_template + metadata = {f: getattr(self, f) for f in CEDAR_METADATA_FIELDS if getattr(self, f, '')} + record, _ = CedarMetadataRecord.objects.get_or_create(guid=self.guid, template=template) + record.metadata = metadata + record.is_published = True + record.save() + def _notify_contributors_pending(self, event_data): user = event_data.kwargs.get('user') for contributor in self.guid.referent.contributors: From 0734205692f0c34201b69a540173739a43629727 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Thu, 28 May 2026 19:33:19 +0200 Subject: [PATCH 2/5] Change metadata creation workflow --- api/collections/serializers.py | 16 ++++-- api_tests/collections/test_views.py | 58 +++++++++----------- osf/models/collection_submission.py | 20 ------- osf/models/provider.py | 23 -------- osf_tests/test_validate_required_metadata.py | 14 ----- 5 files changed, 36 insertions(+), 95 deletions(-) diff --git a/api/collections/serializers.py b/api/collections/serializers.py index 948f09b29f1..8d53d64d7e9 100644 --- a/api/collections/serializers.py +++ b/api/collections/serializers.py @@ -300,8 +300,6 @@ def update(self, obj, validated_data): obj.grade_levels = validated_data.pop('grade_levels') obj.save() - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - obj.sync_cedar_metadata() return obj @@ -407,8 +405,6 @@ def update(self, obj, validated_data): obj.grade_levels = validated_data.pop('grade_levels') obj.save() - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - obj.sync_cedar_metadata() return obj @@ -438,7 +434,11 @@ def create(self, validated_data): except ValidationError as e: raise InvalidModelValueError(e.message) if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - obj.sync_cedar_metadata() + if collection.provider_id: + try: + collection.provider.validate_required_metadata(guid.referent) + except ValidationError as e: + raise InvalidModelValueError(e.message) if subjects: auth = get_user_auth(self.context['request']) try: @@ -476,7 +476,11 @@ def create(self, validated_data): except ValidationError as e: raise InvalidModelValueError(e.message) if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - obj.sync_cedar_metadata() + if collection.provider_id: + try: + collection.provider.validate_required_metadata(guid.referent) + except ValidationError as e: + raise InvalidModelValueError(e.message) if subjects: auth = get_user_auth(self.context['request']) try: diff --git a/api_tests/collections/test_views.py b/api_tests/collections/test_views.py index a4564c7e88d..67334721f41 100644 --- a/api_tests/collections/test_views.py +++ b/api_tests/collections/test_views.py @@ -4451,15 +4451,8 @@ def test_switch_active_no_provider_submission_succeeds(self, app, user_one, proj ) assert res.status_code == 201 - def test_switch_active_no_provider_no_cedar_record_created(self, app, user_one, project, url_no_provider, payload): - from osf.models import CedarMetadataRecord - with mock_update_share(): - with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): - app.post_json_api(url_no_provider, payload(guid=project._id), auth=user_one.auth) - assert not CedarMetadataRecord.objects.filter(guid__in=project.guids.all()).exists() - - def test_switch_active_submission_creates_cedar_record(self, app, user_one, project, url, payload, cedar_template): - from osf.models import CedarMetadataRecord + def test_switch_active_submission_without_cedar_record_fails( + self, app, user_one, project, url, payload, cedar_template): with capture_notifications(): with mock_update_share(): with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): @@ -4467,46 +4460,47 @@ def test_switch_active_submission_creates_cedar_record(self, app, user_one, proj url, payload(guid=project._id), auth=user_one.auth, + expect_errors=True, ) - assert res.status_code == 201 - record = CedarMetadataRecord.objects.filter( - guid__in=project.guids.all(), - template=cedar_template, - is_published=True, - ).first() - assert record is not None + assert res.status_code == 400 - def test_switch_active_submission_with_custom_fields_syncs_cedar_metadata( - self, app, user_one, project, url, payload, cedar_template, collection): + def test_switch_active_submission_with_cedar_record_succeeds( + self, app, user_one, project, url, payload, cedar_template): from osf.models import CedarMetadataRecord - collection.status_choices = ['pending'] - collection.volume_choices = ['1'] - collection.save() + CedarMetadataRecord.objects.create( + guid=project.guids.first(), + template=cedar_template, + metadata={'title': 'Test'}, + is_published=True, + ) with capture_notifications(): with mock_update_share(): with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): - app.post_json_api( + res = app.post_json_api( url, - payload(guid=project._id, status='pending', volume='1'), + payload(guid=project._id), auth=user_one.auth, ) - record = CedarMetadataRecord.objects.get(guid__in=project.guids.all(), template=cedar_template) - assert record.metadata['status'] == 'pending' - assert record.metadata['volume'] == '1' + assert res.status_code == 201 - def test_switch_inactive_submission_does_not_create_cedar_record( + def test_switch_inactive_submission_without_cedar_record_succeeds( self, app, user_one, project, url, payload, cedar_template): - from osf.models import CedarMetadataRecord with capture_notifications(): with mock_update_share(): with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=False): res = app.post_json_api(url, payload(guid=project._id), auth=user_one.auth) assert res.status_code == 201 - assert not CedarMetadataRecord.objects.filter(guid__in=project.guids.all()).exists() - def test_switch_active_update_syncs_cedar_metadata( - self, app, user_one, project, url, payload, cedar_template, collection, provider): + def test_switch_active_update_does_not_alter_cedar_record( + self, app, user_one, project, url, payload, cedar_template, collection): from osf.models import CedarMetadataRecord + original_metadata = {'title': 'Original'} + CedarMetadataRecord.objects.create( + guid=project.guids.first(), + template=cedar_template, + metadata=original_metadata, + is_published=True, + ) collection.status_choices = ['pending', 'approved'] collection.save() with capture_notifications(): @@ -4520,7 +4514,7 @@ def test_switch_active_update_syncs_cedar_metadata( app.patch_json_api(detail_url, payload(status='approved'), auth=user_one.auth) record = CedarMetadataRecord.objects.get(guid__in=project.guids.all(), template=cedar_template) - assert record.metadata['status'] == 'approved' + assert record.metadata == original_metadata class TestCollectedMetaSubjectFiltering(SubjectsFilterMixin): diff --git a/osf/models/collection_submission.py b/osf/models/collection_submission.py index b971f1d4658..86f52ebdce6 100644 --- a/osf/models/collection_submission.py +++ b/osf/models/collection_submission.py @@ -22,13 +22,6 @@ logger = logging.getLogger(__name__) -CEDAR_METADATA_FIELDS = [ - 'collected_type', 'status', 'volume', 'issue', - 'program_area', 'school_type', 'study_design', - 'data_type', 'disease', 'grade_levels', -] - - class CollectionSubmission(TaxonomizableMixin, BaseModel): primary_identifier_name = 'guid___id' @@ -100,19 +93,6 @@ def is_submitted_by_moderator_contributor(self, event_data): def state(self, new_state): self.machine_state = new_state.value - def sync_cedar_metadata(self): - """Create or update a CedarMetadataRecord from this submission's custom metadata fields.""" - - from osf.models import CedarMetadataRecord - if not (self.collection.provider_id and self.collection.provider.required_metadata_template): - return - template = self.collection.provider.required_metadata_template - metadata = {f: getattr(self, f) for f in CEDAR_METADATA_FIELDS if getattr(self, f, '')} - record, _ = CedarMetadataRecord.objects.get_or_create(guid=self.guid, template=template) - record.metadata = metadata - record.is_published = True - record.save() - def _notify_contributors_pending(self, event_data): user = event_data.kwargs.get('user') for contributor in self.guid.referent.contributors: diff --git a/osf/models/provider.py b/osf/models/provider.py index dd32a88ff59..f1dbdec65b3 100644 --- a/osf/models/provider.py +++ b/osf/models/provider.py @@ -1,6 +1,5 @@ import json import requests -from jsonschema import validate as jsonschema_validate, ValidationError as JsonSchemaValidationError from django.apps import apps from django.contrib.postgres import fields @@ -21,7 +20,6 @@ from .brand import Brand from .citation import CitationStyle from .licenses import NodeLicense -from .cedar_metadata import CedarMetadataRecord from .storage import ProviderAssetFile from .subject import Subject from osf.utils.datetime_aware_jsonfield import DateTimeAwareJSONField @@ -259,27 +257,6 @@ def setup_share_source(self, provider_home_page): self.save() - def validate_required_metadata(self, osf_obj): - if not self.required_metadata_template: - return - - record = CedarMetadataRecord.objects.filter( - guid__in=osf_obj.guids.all(), - template=self.required_metadata_template, - is_published=True, - ).first() - - if record is None: - raise ValidationError( - f'Object must have a published CEDAR metadata record for the required template ' - f'"{self.required_metadata_template.schema_name}".' - ) - - try: - jsonschema_validate(record.metadata, self.required_metadata_template.template) - except JsonSchemaValidationError as e: - raise ValidationError(e.message) - class CollectionProvider(AbstractProvider): DEFAULT_SUBSCRIPTIONS = [ diff --git a/osf_tests/test_validate_required_metadata.py b/osf_tests/test_validate_required_metadata.py index 73d4b166815..4e49f2cf42a 100644 --- a/osf_tests/test_validate_required_metadata.py +++ b/osf_tests/test_validate_required_metadata.py @@ -84,20 +84,6 @@ def test_published_valid_record_passes(self, provider, cedar_template, preprint) provider.validate_required_metadata(preprint) - def test_published_invalid_record_raises(self, provider, cedar_template, preprint): - provider.required_metadata_template = cedar_template - provider.save() - - CedarMetadataRecord.objects.create( - guid=preprint.guids.first(), - template=cedar_template, - metadata={'title': 123}, - is_published=True, - ) - - with pytest.raises(ValidationError): - provider.validate_required_metadata(preprint) - def test_record_for_wrong_template_raises(self, provider, cedar_template, preprint): provider.required_metadata_template = cedar_template provider.save() From 598379a3f4bcb7af5b4b0690d75e2ea3d428d8d8 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Thu, 28 May 2026 20:08:15 +0200 Subject: [PATCH 3/5] fix tests --- osf/models/provider.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osf/models/provider.py b/osf/models/provider.py index f1dbdec65b3..ed093d819e3 100644 --- a/osf/models/provider.py +++ b/osf/models/provider.py @@ -165,6 +165,24 @@ def update_or_create_from_json(cls, provider_data, user): related_name='required_by_providers', ) + def validate_required_metadata(self, obj): + """ + Raises ValidationError if obj does not have a published CedarMetadataRecord for + this provider's required_metadata_template. + Does nothing when required_metadata_template is not set. + """ + if not self.required_metadata_template_id: + return + guid = obj.guids.first() + if guid is None or not guid.cedar_metadata_records.filter( + template_id=self.required_metadata_template_id, + is_published=True, + ).exists(): + raise ValidationError( + f'Submitted object must have a published CEDAR metadata record for template ' + f'"{self.required_metadata_template.schema_name}" to be submitted to this collection.' + ) + def __repr__(self): return ('(name={self.name!r}, default_license={self.default_license!r}, ' 'allow_submissions={self.allow_submissions!r}) with id {self.id!r}').format(self=self) From 0c1987a26e1f6a5066fc638d6f5b1f197073325e Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 29 May 2026 13:47:50 +0200 Subject: [PATCH 4/5] Fix comments --- api/collections/serializers.py | 22 ++++++++++----------- osf/models/cedar_metadata.py | 12 ++++++++++++ osf/models/provider.py | 36 +++++++++++++++++----------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/api/collections/serializers.py b/api/collections/serializers.py index 8d53d64d7e9..2f24a2eb56a 100644 --- a/api/collections/serializers.py +++ b/api/collections/serializers.py @@ -429,16 +429,15 @@ def create(self, validated_data): raise exceptions.ValidationError('"creator" must be specified.') if not (creator.has_perm('write_collection', collection) or (hasattr(guid.referent, 'has_permission') and guid.referent.has_permission(creator, WRITE))): raise exceptions.PermissionDenied('Must have write permission on either collection or collected object to collect.') + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR) and collection.provider_id: + try: + collection.provider.validate_required_metadata(guid.referent) + except ValidationError as e: + raise InvalidModelValueError(e.message) try: obj = collection.collect_object(guid.referent, creator, **validated_data) except ValidationError as e: raise InvalidModelValueError(e.message) - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - if collection.provider_id: - try: - collection.provider.validate_required_metadata(guid.referent) - except ValidationError as e: - raise InvalidModelValueError(e.message) if subjects: auth = get_user_auth(self.context['request']) try: @@ -471,16 +470,15 @@ def create(self, validated_data): raise exceptions.ValidationError('"creator" must be specified.') if not (creator.has_perm('write_collection', collection) or (hasattr(guid.referent, 'has_permission') and guid.referent.has_permission(creator, WRITE))): raise exceptions.PermissionDenied('Must have write permission on either collection or collected object to collect.') + if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR) and collection.provider_id: + try: + collection.provider.validate_required_metadata(guid.referent) + except ValidationError as e: + raise InvalidModelValueError(e.message) try: obj = collection.collect_object(guid.referent, creator, **validated_data) except ValidationError as e: raise InvalidModelValueError(e.message) - if waffle.switch_is_active(features.COLLECTION_SUBMISSION_WITH_CEDAR): - if collection.provider_id: - try: - collection.provider.validate_required_metadata(guid.referent) - except ValidationError as e: - raise InvalidModelValueError(e.message) if subjects: auth = get_user_auth(self.context['request']) try: diff --git a/osf/models/cedar_metadata.py b/osf/models/cedar_metadata.py index df8d4cda4d3..b14c27c4190 100644 --- a/osf/models/cedar_metadata.py +++ b/osf/models/cedar_metadata.py @@ -1,4 +1,6 @@ +from django.core.exceptions import ValidationError from django.db import models +from jsonschema import validate as jsonschema_validate, ValidationError as JsonSchemaValidationError from osf.models.base import BaseModel, ObjectIDMixin from osf.utils.datetime_aware_jsonfield import DateTimeAwareJSONField @@ -47,6 +49,16 @@ def get_template_name(self): def get_template_version(self): return self.template.template_version + def clean(self): + if self.is_published: + try: + jsonschema_validate(self.metadata, self.template.template) + except JsonSchemaValidationError as e: + raise ValidationError( + f'CEDAR metadata does not validate against template "{self.template.schema_name}": {e.message}' + ) + def save(self, *args, **kwargs): + self.clean() self.guid.referent.update_search() return super().save(*args, **kwargs) diff --git a/osf/models/provider.py b/osf/models/provider.py index ed093d819e3..1064f7e95c7 100644 --- a/osf/models/provider.py +++ b/osf/models/provider.py @@ -165,24 +165,6 @@ def update_or_create_from_json(cls, provider_data, user): related_name='required_by_providers', ) - def validate_required_metadata(self, obj): - """ - Raises ValidationError if obj does not have a published CedarMetadataRecord for - this provider's required_metadata_template. - Does nothing when required_metadata_template is not set. - """ - if not self.required_metadata_template_id: - return - guid = obj.guids.first() - if guid is None or not guid.cedar_metadata_records.filter( - template_id=self.required_metadata_template_id, - is_published=True, - ).exists(): - raise ValidationError( - f'Submitted object must have a published CEDAR metadata record for template ' - f'"{self.required_metadata_template.schema_name}" to be submitted to this collection.' - ) - def __repr__(self): return ('(name={self.name!r}, default_license={self.default_license!r}, ' 'allow_submissions={self.allow_submissions!r}) with id {self.id!r}').format(self=self) @@ -224,6 +206,24 @@ def top_level_subjects(self): def readable_type(self): raise NotImplementedError + def validate_required_metadata(self, obj): + """ + Raises ValidationError if obj does not have a published CedarMetadataRecord for + this provider's required_metadata_template. + Does nothing when required_metadata_template is not set. + """ + if not self.required_metadata_template_id: + return + guid = obj.guids.first() + if guid is None or not guid.cedar_metadata_records.filter( + template_id=self.required_metadata_template_id, + is_published=True, + ).exists(): + raise ValidationError( + f'Submitted object must have a published CEDAR metadata record for template ' + f'"{self.required_metadata_template.schema_name}" to be submitted to this collection.' + ) + def get_asset_url(self, name): """ Helper that returns an associated ProviderAssetFile's url, or None From 5fbd517c5fce13257923b6ef086984fe13136775 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 29 May 2026 16:49:12 +0200 Subject: [PATCH 5/5] Fix test --- api_tests/collections/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_tests/collections/test_views.py b/api_tests/collections/test_views.py index 67334721f41..aa2eeaea40c 100644 --- a/api_tests/collections/test_views.py +++ b/api_tests/collections/test_views.py @@ -4453,7 +4453,7 @@ def test_switch_active_no_provider_submission_succeeds(self, app, user_one, proj def test_switch_active_submission_without_cedar_record_fails( self, app, user_one, project, url, payload, cedar_template): - with capture_notifications(): + with capture_notifications(expect_none=True): with mock_update_share(): with override_switch(features.COLLECTION_SUBMISSION_WITH_CEDAR, active=True): res = app.post_json_api(