Skip to content

Commit 93fcd18

Browse files
committed
Merge branch 'release/26.12.0'
2 parents fbdbf5c + ed8af91 commit 93fcd18

37 files changed

Lines changed: 1820 additions & 220 deletions

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

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

5+
26.12.0 (2026-06-23)
6+
====================
7+
8+
- ES2 Consolidation
9+
510
26.11.0 (2026-06-10)
611
====================
712

admin/base/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Utility functions and classes
33
"""
4-
from osf.models import Subject, NodeLicense, Brand
4+
from osf.models import Subject, NodeLicense, Brand, CedarMetadataTemplate
55

66
from django.core.exceptions import ValidationError, PermissionDenied
77
from django.urls import reverse
@@ -116,6 +116,11 @@ def get_brand_choices():
116116
brands = Brand.objects.all()
117117
return [no_default] + [(brand.id, brand.name) for brand in brands]
118118

119+
def get_cedar_template_choices():
120+
no_default = ('', '---------')
121+
templates = CedarMetadataTemplate.objects.filter(active=True)
122+
return [no_default] + [(t.id, t.schema_name) for t in templates]
123+
119124
def get_toplevel_subjects():
120125
return Subject.objects.filter(parent__isnull=True, provider___id='osf').values_list('id', 'text')
121126

admin/cedar/forms.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from django.forms import ModelForm, CharField, JSONField
1+
from django.forms import ModelForm, CharField, JSONField, BooleanField
22

33
from osf.models import CedarMetadataTemplate
44

5+
56
class CedarMetadataTemplateForm(ModelForm):
67
schema_name = CharField(
78
disabled=True,
@@ -18,7 +19,8 @@ class CedarMetadataTemplateForm(ModelForm):
1819
template = JSONField(
1920
disabled=True
2021
)
22+
is_for_collections = BooleanField(label='For collections only:', required=False)
2123

2224
class Meta:
2325
model = CedarMetadataTemplate
24-
fields = ['schema_name', 'cedar_id', 'template_version', 'template', 'active']
26+
fields = ['schema_name', 'cedar_id', 'template_version', 'template', 'is_for_collections', 'active', 'should_index_for_search']

admin/collection_providers/forms.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from framework.utils import sanitize_html
66
from osf.models import CollectionProvider
7-
from admin.base.utils import get_nodelicense_choices, get_defaultlicense_choices, validate_slug
7+
from admin.base.utils import get_nodelicense_choices, get_defaultlicense_choices, validate_slug, get_cedar_template_choices
88

99

1010
class CollectionProviderForm(forms.ModelForm):
@@ -37,9 +37,11 @@ class Meta:
3737
def __init__(self, *args, **kwargs):
3838
nodelicense_choices = get_nodelicense_choices()
3939
defaultlicense_choices = get_defaultlicense_choices()
40+
cedar_template_choices = get_cedar_template_choices()
4041
super().__init__(*args, **kwargs)
4142
self.fields['licenses_acceptable'].choices = nodelicense_choices
4243
self.fields['default_license'].choices = defaultlicense_choices
44+
self.fields['required_metadata_template'].choices = cedar_template_choices
4345

4446
def clean_description(self, *args, **kwargs):
4547
if not self.data.get('description'):

admin/collection_providers/views.py

Lines changed: 41 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -132,127 +132,78 @@ def get_context_data(self, *args, **kwargs):
132132
licenses_html += '</ul>'
133133
collection_provider_attributes['licenses_acceptable'] = licenses_html
134134

135-
primary_collection = collection_provider.primary_collection
136-
# compile html list of collected_type_choices
137-
collected_type_choices_html = '<ul>'
138-
for choice in primary_collection.collected_type_choices:
139-
collected_type_choices_html += f'<li>{choice}</li>'
140-
collected_type_choices_html += '</ul>'
141-
kwargs['collected_type_choices'] = collected_type_choices_html
142-
143-
# compile html list of status_choices
144-
status_choices_html = '<ul>'
145-
for choice in primary_collection.status_choices:
146-
status_choices_html += f'<li>{choice}</li>'
147-
status_choices_html += '</ul>'
148-
kwargs['status_choices'] = status_choices_html
149-
150-
# compile html list of volume_choices
151-
volume_choices_html = '<ul>'
152-
for choice in primary_collection.volume_choices:
153-
volume_choices_html += f'<li>{choice}</li>'
154-
volume_choices_html += '</ul>'
155-
kwargs['volume_choices'] = volume_choices_html
156-
157-
# compile html list of issue_choices
158-
issue_choices_html = '<ul>'
159-
for choice in primary_collection.issue_choices:
160-
issue_choices_html += f'<li>{choice}</li>'
161-
issue_choices_html += '</ul>'
162-
kwargs['issue_choices'] = issue_choices_html
163-
164-
# compile html list of program_area_choices
165-
program_area_choices_html = '<ul>'
166-
for choice in primary_collection.program_area_choices:
167-
program_area_choices_html += f'<li>{choice}</li>'
168-
program_area_choices_html += '</ul>'
169-
kwargs['program_area_choices'] = program_area_choices_html
170-
171-
# compile html list of school_type_choices
172-
school_type_choices_html = '<ul>{choices}</ul>'.format(choices=''.join(
173-
f'<li>{choice}</li>' for choice in primary_collection.school_type_choices
174-
))
175-
kwargs['school_type_choices'] = school_type_choices_html
176-
177-
# compile html list of study_design_choices
178-
study_design_choices_html = '<ul>{choices}</ul>'.format(choices=''.join(
179-
f'<li>{choice}</li>' for choice in primary_collection.study_design_choices
180-
))
181-
kwargs['study_design_choices'] = study_design_choices_html
182-
183-
disease_choices_html = '<ul>{choices}</ul>'.format(choices=''.join(
184-
f'<li>{choice}</li>' for choice in primary_collection.disease_choices
185-
))
186-
kwargs['disease_choices'] = disease_choices_html
187-
188-
data_type_choices_html = '<ul>{choices}</ul>'.format(choices=''.join(
189-
f'<li>{choice}</li>' for choice in primary_collection.data_type_choices
190-
))
191-
kwargs['data_type_choices'] = data_type_choices_html
192-
193-
grade_levels_choices_html = '<ul>{choices}</ul>'.format(choices=''.join(
194-
f'<li>{choice}</li>' for choice in primary_collection.grade_levels_choices
195-
))
196-
kwargs['grade_levels_choices'] = grade_levels_choices_html
197-
198-
# get a dict of model fields so that we can set the initial value for the update form
199135
fields = model_to_dict(collection_provider)
200-
fields['collected_type_choices'] = json.dumps(primary_collection.collected_type_choices)
201-
fields['status_choices'] = json.dumps(primary_collection.status_choices)
202-
fields['volume_choices'] = json.dumps(primary_collection.volume_choices)
203-
fields['issue_choices'] = json.dumps(primary_collection.issue_choices)
204-
fields['program_area_choices'] = json.dumps(primary_collection.program_area_choices)
205136

206-
fields = model_to_dict(collection_provider)
207-
208-
fields['school_type_choices'] = json.dumps(primary_collection.school_type_choices)
209-
fields['study_design_choices'] = json.dumps(primary_collection.study_design_choices)
210-
fields['data_type_choices'] = json.dumps(primary_collection.data_type_choices)
211-
fields['disease_choices'] = json.dumps(primary_collection.disease_choices)
212-
fields['grade_levels_choices'] = json.dumps(primary_collection.grade_levels_choices)
213-
214-
# compile html list of collected_type_choices
215137
if collection_provider.primary_collection:
138+
primary_collection = collection_provider.primary_collection
139+
140+
# compile html list of collected_type_choices
216141
collected_type_choices_html = '<ul>'
217-
for choice in collection_provider.primary_collection.collected_type_choices:
142+
for choice in primary_collection.collected_type_choices:
218143
collected_type_choices_html += f'<li>{choice}</li>'
219144
collected_type_choices_html += '</ul>'
220145
kwargs['collected_type_choices'] = collected_type_choices_html
221146

222147
# compile html list of status_choices
223148
status_choices_html = '<ul>'
224-
for choice in collection_provider.primary_collection.status_choices:
149+
for choice in primary_collection.status_choices:
225150
status_choices_html += f'<li>{choice}</li>'
226151
status_choices_html += '</ul>'
227152
kwargs['status_choices'] = status_choices_html
228153

229154
# compile html list of volume_choices
230155
volume_choices_html = '<ul>'
231-
for choice in collection_provider.primary_collection.volume_choices:
156+
for choice in primary_collection.volume_choices:
232157
volume_choices_html += f'<li>{choice}</li>'
233158
volume_choices_html += '</ul>'
234159
kwargs['volume_choices'] = volume_choices_html
235160

236161
# compile html list of issue_choices
237162
issue_choices_html = '<ul>'
238-
for choice in collection_provider.primary_collection.issue_choices:
163+
for choice in primary_collection.issue_choices:
239164
issue_choices_html += f'<li>{choice}</li>'
240165
issue_choices_html += '</ul>'
241166
kwargs['issue_choices'] = issue_choices_html
242167

243168
# compile html list of program_area_choices
244169
program_area_choices_html = '<ul>'
245-
for choice in collection_provider.primary_collection.program_area_choices:
170+
for choice in primary_collection.program_area_choices:
246171
program_area_choices_html += f'<li>{choice}</li>'
247172
program_area_choices_html += '</ul>'
248173
kwargs['program_area_choices'] = program_area_choices_html
249174

250-
# get a dict of model fields so that we can set the initial value for the update form
251-
fields['collected_type_choices'] = json.dumps(collection_provider.primary_collection.collected_type_choices)
252-
fields['status_choices'] = json.dumps(collection_provider.primary_collection.status_choices)
253-
fields['volume_choices'] = json.dumps(collection_provider.primary_collection.volume_choices)
254-
fields['issue_choices'] = json.dumps(collection_provider.primary_collection.issue_choices)
255-
fields['program_area_choices'] = json.dumps(collection_provider.primary_collection.program_area_choices)
175+
# compile html list of school_type_choices
176+
kwargs['school_type_choices'] = '<ul>{choices}</ul>'.format(choices=''.join(
177+
f'<li>{choice}</li>' for choice in primary_collection.school_type_choices
178+
))
179+
180+
# compile html list of study_design_choices
181+
kwargs['study_design_choices'] = '<ul>{choices}</ul>'.format(choices=''.join(
182+
f'<li>{choice}</li>' for choice in primary_collection.study_design_choices
183+
))
184+
185+
kwargs['disease_choices'] = '<ul>{choices}</ul>'.format(choices=''.join(
186+
f'<li>{choice}</li>' for choice in primary_collection.disease_choices
187+
))
188+
189+
kwargs['data_type_choices'] = '<ul>{choices}</ul>'.format(choices=''.join(
190+
f'<li>{choice}</li>' for choice in primary_collection.data_type_choices
191+
))
192+
193+
kwargs['grade_levels_choices'] = '<ul>{choices}</ul>'.format(choices=''.join(
194+
f'<li>{choice}</li>' for choice in primary_collection.grade_levels_choices
195+
))
196+
197+
fields['collected_type_choices'] = json.dumps(primary_collection.collected_type_choices)
198+
fields['status_choices'] = json.dumps(primary_collection.status_choices)
199+
fields['volume_choices'] = json.dumps(primary_collection.volume_choices)
200+
fields['issue_choices'] = json.dumps(primary_collection.issue_choices)
201+
fields['program_area_choices'] = json.dumps(primary_collection.program_area_choices)
202+
fields['school_type_choices'] = json.dumps(primary_collection.school_type_choices)
203+
fields['study_design_choices'] = json.dumps(primary_collection.study_design_choices)
204+
fields['data_type_choices'] = json.dumps(primary_collection.data_type_choices)
205+
fields['disease_choices'] = json.dumps(primary_collection.disease_choices)
206+
fields['grade_levels_choices'] = json.dumps(primary_collection.grade_levels_choices)
256207

257208
kwargs['form'] = CollectionProviderForm(initial=fields)
258209

@@ -272,7 +223,7 @@ def form_valid(self, form):
272223
if self.object.primary_collection:
273224
for choices_name in ['collected_type', 'status', 'issue', 'volume', 'program_area', 'school_type', 'study_design', 'data_type', 'disease', 'grade_levels']:
274225
_process_collection_choices(self.object, choices_name, form)
275-
self.object.primary_collection.save()
226+
self.object.primary_collection.save()
276227
return super().form_valid(form)
277228

278229
def form_invalid(self, form):

admin/preprint_providers/forms.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
Subject
99
)
1010
from admin.base.utils import (get_subject_rules, get_toplevel_subjects,
11-
get_nodelicense_choices, get_defaultlicense_choices, validate_slug)
11+
get_nodelicense_choices, get_defaultlicense_choices, validate_slug,
12+
get_cedar_template_choices)
1213

1314

1415
class PreprintProviderForm(forms.ModelForm):
@@ -42,10 +43,12 @@ def __init__(self, *args, **kwargs):
4243
toplevel_choices = get_toplevel_subjects()
4344
nodelicense_choices = get_nodelicense_choices()
4445
defaultlicense_choices = get_defaultlicense_choices()
46+
cedar_template_choices = get_cedar_template_choices()
4547
super().__init__(*args, **kwargs)
4648
self.fields['toplevel_subjects'].choices = toplevel_choices
4749
self.fields['licenses_acceptable'].choices = nodelicense_choices
4850
self.fields['default_license'].choices = defaultlicense_choices
51+
self.fields['required_metadata_template'].choices = cedar_template_choices
4952

5053
def clean_subjects_acceptable(self, *args, **kwargs):
5154
subject_ids = [_f for _f in self.data['subjects_chosen'].split(', ') if _f]

admin/registration_providers/forms.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
get_defaultlicense_choices,
88
validate_slug,
99
get_brand_choices,
10+
get_cedar_template_choices,
1011
)
1112

1213

@@ -41,10 +42,12 @@ def __init__(self, *args, **kwargs):
4142
nodelicense_choices = get_nodelicense_choices()
4243
defaultlicense_choices = get_defaultlicense_choices()
4344
brand_choices = get_brand_choices()
45+
cedar_template_choices = get_cedar_template_choices()
4446
super().__init__(*args, **kwargs)
4547
self.fields['licenses_acceptable'].choices = nodelicense_choices
4648
self.fields['default_license'].choices = defaultlicense_choices
4749
self.fields['brand'].choices = brand_choices
50+
self.fields['required_metadata_template'].choices = cedar_template_choices
4851
if kwargs.get('initial', None) and kwargs.get('initial').get('_id', None):
4952
provider = RegistrationProvider.load(kwargs.get('initial').get('_id'))
5053
self.fields['default_schema'].choices = provider.schemas.filter(visible=True, active=True).values_list('id', 'name')

admin/templates/cedar/list.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ <h2>List of Cedar Metadata Templates</h2>
1414
<th>Name</th>
1515
<th>Version</th>
1616
<th>Active</th>
17+
<th>For Collections only</th>
1718
</tr>
1819
</thead>
1920
<tbody>
@@ -33,6 +34,9 @@ <h2>List of Cedar Metadata Templates</h2>
3334
<td>
3435
{{ template.active }}
3536
</td>
37+
<td>
38+
{{ template.is_for_collections }}
39+
</td>
3640
</tr>
3741
{% endfor %}
3842
</tbody>

api/cedar_metadata_records/serializers.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from api.cedar_metadata_records.utils import can_create_record
1212

1313
from osf.exceptions import ValidationError
14-
from osf.models import CedarMetadataRecord, CedarMetadataTemplate, Guid
14+
from osf.models import CedarMetadataRecord, CedarMetadataTemplate, CollectionSubmission, Guid
15+
from osf.utils.workflows import CollectionSubmissionStates
1516

1617
logger = logging.getLogger(__name__)
1718

@@ -115,13 +116,32 @@ def create(self, validated_data):
115116
raise PermissionDenied
116117
if not template.is_active():
117118
raise NotFound
118-
record = CedarMetadataRecord(guid=guid, template=template, metadata=metadata, is_published=is_published)
119-
try:
120-
record.save()
121-
except ValidationError as e:
122-
raise InvalidModelValueError(detail=e.messages[0])
123-
except IntegrityError:
124-
raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]')
119+
has_active_collection_submission = CollectionSubmission.objects.filter(
120+
guid=guid,
121+
collection__provider__required_metadata_template=template,
122+
machine_state__in=[CollectionSubmissionStates.PENDING.value, CollectionSubmissionStates.ACCEPTED.value],
123+
).exists()
124+
if template.is_for_collections and not has_active_collection_submission:
125+
record, _ = CedarMetadataRecord.objects.get_or_create(
126+
guid=guid,
127+
template=template,
128+
defaults={'metadata': metadata, 'is_published': is_published},
129+
)
130+
if record.metadata != metadata or record.is_published != is_published:
131+
record.metadata = metadata
132+
record.is_published = is_published
133+
try:
134+
record.save()
135+
except ValidationError as e:
136+
raise InvalidModelValueError(detail=e.messages[0])
137+
else:
138+
record = CedarMetadataRecord(guid=guid, template=template, metadata=metadata, is_published=is_published)
139+
try:
140+
record.save()
141+
except ValidationError as e:
142+
raise InvalidModelValueError(detail=e.messages[0])
143+
except IntegrityError:
144+
raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]')
125145
return record
126146

127147
def update(self, instance, validated_data):

api/cedar_metadata_templates/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ class CedarMetadataTemplateSerializer(JSONAPISerializer):
99
class Meta:
1010
type_ = 'cedar-metadata-templates'
1111

12-
filterable_fields = frozenset(['schema_name', 'cedar_id', 'active'])
12+
filterable_fields = frozenset(['schema_name', 'cedar_id', 'active', 'is_for_collections'])
1313

1414
id = ser.CharField(source='_id', read_only=True)
1515
schema_name = ser.CharField(read_only=True)
1616
cedar_id = ser.CharField(read_only=True)
1717
template = ser.DictField(read_only=True)
1818
active = ser.BooleanField(read_only=True)
1919
template_version = ser.IntegerField(read_only=True)
20+
is_for_collections = ser.BooleanField(read_only=True)
2021

2122
links = LinksField({'self': 'get_absolute_url'})
2223

0 commit comments

Comments
 (0)