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: