Skip to content

Commit 82f5286

Browse files
akocharij-awada
authored andcommitted
SS-1953 New field for keywords from controlled vocabularies (#545)
1 parent 2db29c1 commit 82f5286

21 files changed

Lines changed: 297 additions & 166 deletions

apps/background_tasks/tasks/doi_provisioning.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,20 @@ def _build_additional_metadata(
6161
additional_metadata["creators"] = creators
6262
logger.debug(f"DOI provisioning: Added {len(creators)} creators from form data")
6363

64-
# Subjects/tags from form data (prefer form data over model instance tags)
64+
# Subjects/keywords from form data (prefer form data over model instance subjects_keywords)
6565
if subjects and isinstance(subjects, list):
66-
# Form-processed subjects/tags take priority
66+
# Form-processed subjects/keywords take priority
6767
additional_metadata["subjects"] = subjects
6868
logger.debug(f"DOI provisioning: Added {len(subjects)} subjects from form data")
69-
elif hasattr(app_instance, "tags") and app_instance.tags:
70-
# Fallback to model instance tags if no form data
69+
elif hasattr(app_instance, "subjects_keywords"):
70+
# Fallback to model instance subjects_keywords if no form data
7171
try:
72-
tag_names = [t.name for t in app_instance.tags.all()]
73-
if tag_names:
74-
additional_metadata["subjects"] = tag_names
75-
logger.debug(f"DOI provisioning: Added {len(tag_names)} subjects from model instance")
72+
subjects_keywords = app_instance.subjects_keywords or []
73+
if subjects_keywords:
74+
additional_metadata["subjects"] = subjects_keywords
75+
logger.debug(f"DOI provisioning: Added {len(subjects_keywords)} subjects from model instance")
7676
except Exception:
77-
tag_names = None
77+
subjects_keywords = None
7878

7979
return additional_metadata if additional_metadata else None
8080

@@ -135,7 +135,7 @@ def execute(self, app_instance, **kwargs) -> dict[str, Any]:
135135
language=kwargs.get("language"),
136136
funding=kwargs.get("funding"),
137137
creators=kwargs.get("creators"),
138-
subjects=kwargs.get("tags"), # Note: task receives 'tags' but function expects 'subjects'
138+
subjects=kwargs.get("subjects_keywords"),
139139
)
140140

141141
try:

apps/forms/base.py

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ def _setup_form_fields(self):
8383

8484
# Handle name
8585
self.fields["name"].initial = ""
86-
# Initialize the tags field to existing tags or empty list
87-
if self.instance and self.instance.pk and hasattr(self.instance, "tags"):
86+
# Initialize subjects_keywords field to existing JSON data or empty list
87+
if self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
8888
self.instance.refresh_from_db()
89-
self._original_tags = list(self.instance.tags.all())
89+
self._original_subjects_keywords = self.instance.subjects_keywords or []
9090
else:
91-
self._original_tags = []
91+
self._original_subjects_keywords = []
9292

9393
self._restore_model_help_text()
9494

@@ -243,9 +243,44 @@ def clean_note_on_linkonly_privacy(self):
243243

244244
return note_on_linkonly_privacy
245245

246-
def clean_tags(self):
247-
cleaned_data = super().clean()
248-
return cleaned_data.get("tags", [])
246+
def clean_subjects_keywords(self):
247+
raw = self.cleaned_data.get("subjects_keywords") or []
248+
249+
if isinstance(raw, str):
250+
try:
251+
raw = json.loads(raw)
252+
except json.JSONDecodeError as e:
253+
raise forms.ValidationError("Invalid subjects/keywords data.") from e
254+
255+
if not isinstance(raw, list):
256+
raise forms.ValidationError("Subjects/keywords must be a list.")
257+
258+
cleaned_subjects_keywords = []
259+
260+
for item in raw:
261+
if not isinstance(item, dict):
262+
raise forms.ValidationError("Each subject/keyword entry must be an object.")
263+
264+
subject = (item.get("subject") or "").strip()
265+
subject_scheme = (item.get("subject_scheme") or "").strip()
266+
classification_code = (item.get("classification_code") or "").strip()
267+
268+
if not subject:
269+
raise forms.ValidationError("Each subject/keyword entry must have a subject.")
270+
if not subject_scheme:
271+
raise forms.ValidationError("Each subject/keyword entry must have a scheme.")
272+
if not classification_code:
273+
raise forms.ValidationError("Each subject/keyword entry must have an identifier.")
274+
275+
cleaned_subjects_keywords.append(
276+
{
277+
"subject": subject,
278+
"subject_scheme": subject_scheme,
279+
"classification_code": classification_code,
280+
}
281+
)
282+
283+
return cleaned_subjects_keywords
249284

250285
def clean_funding_sources_json(self):
251286
raw = self.cleaned_data.get("funding_sources_json") or "[]"
@@ -372,26 +407,28 @@ def normalize(data):
372407
# If there's an error, keep the field in changed_data to be safe
373408
pass
374409

375-
# Handle tags field - compare current input with initial tags
376-
if "tags" in changed_data and self.instance and self.instance.pk:
410+
# Handle subjects_keywords field - compare JSON data
411+
if "subjects_keywords" in changed_data and self.instance and self.instance.pk:
377412
try:
378-
# Try invenio_tags field first, then tags field
379-
current_tags_input = self.data.get("invenio_tags", "") or self.data.get("tags", "") or ""
380-
initial_tags_input = None
381-
382-
if "invenio_tags" in self.fields:
383-
initial_tags_input = self.fields["invenio_tags"].initial or ""
384-
elif hasattr(self, "_original_tags") and self._original_tags:
385-
# Fallback to database tags formatted as pipe-separated
386-
initial_tags_input = " | ".join(str(tag) for tag in self._original_tags)
387-
else:
388-
initial_tags_input = ""
413+
current_subjects_keywords = self.data.get("subjects_keywords", "") or "[]"
414+
initial_subjects_keywords = self.fields["subjects_keywords"].initial or "[]"
389415

390-
# If the tag input is the same, remove from changed_data
391-
if current_tags_input == initial_tags_input:
392-
changed_data.remove("tags")
393-
except (AttributeError, KeyError):
394-
# If there's an error, keep the field in changed_data to be safe
416+
current_data = (
417+
json.loads(current_subjects_keywords)
418+
if isinstance(current_subjects_keywords, str)
419+
else current_subjects_keywords
420+
)
421+
initial_data = (
422+
json.loads(initial_subjects_keywords)
423+
if isinstance(initial_subjects_keywords, str)
424+
else initial_subjects_keywords
425+
)
426+
427+
# If the data is the same, remove from changed_data
428+
if current_data == initial_data:
429+
changed_data.remove("subjects_keywords")
430+
except (json.JSONDecodeError, KeyError, ValueError, TypeError):
431+
# If there's an error parsing, keep the field in changed_data to be safe
395432
pass
396433

397434
# Handle language field - compare current value with initial form value

apps/forms/custom.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ def __init__(self, *args, **kwargs):
4949

5050
def _load_existing_tags_to_invenio_field(self):
5151
"""Load existing database tags into the invenio_tags field"""
52-
if self.instance and self.instance.pk and hasattr(self.instance, "tags"):
53-
existing_tags = list(self.instance.tags.all())
52+
if self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
53+
existing_tags = self.instance.subjects_keywords or []
5454
if existing_tags:
5555
# Convert tag objects to pipe-separated format for template
56-
tag_names = [str(tag) for tag in existing_tags]
57-
tag_string = " | ".join(tag_names)
56+
tag_names = [item.get("subject", "") for item in existing_tags if isinstance(item, dict)]
57+
tag_string = " | ".join(filter(None, tag_names))
5858
self.fields["invenio_tags"].initial = tag_string
5959
else:
6060
self.fields["invenio_tags"].initial = ""
@@ -161,13 +161,13 @@ def clean(self):
161161

162162
if invenio_tags_value:
163163
# Process the tags using the existing cleaning logic
164-
cleaned_data["tags"] = self.clean_keyword_tags()
165-
elif self.instance and self.instance.pk and hasattr(self.instance, "tags"):
164+
cleaned_data["subjects_keywords"] = self.clean_keyword_tags()
165+
elif self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
166166
# If no tags in form but instance has existing tags, preserve them
167-
existing_tags = list(self.instance.tags.all())
167+
existing_tags = self.instance.subjects_keywords or []
168168
if existing_tags:
169169
# Keep existing tags as they are
170-
cleaned_data["tags"] = existing_tags
170+
cleaned_data["subjects_keywords"] = existing_tags
171171
return cleaned_data
172172

173173
class Meta:
@@ -183,11 +183,11 @@ class Meta:
183183
"source_code_url",
184184
"port",
185185
"image",
186-
"tags",
186+
"subjects_keywords",
187187
"default_url_subpath",
188188
"mount_path",
189189
]
190190
labels = {
191191
"note_on_linkonly_privacy": "Reason for choosing the link only option",
192-
"tags": "Subjects and keywords",
192+
"subjects_keywords": "Subjects and keywords",
193193
}

apps/forms/dash.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ def __init__(self, *args, **kwargs):
4848

4949
def _load_existing_tags_to_invenio_field(self):
5050
"""Load existing database tags into the invenio_tags field"""
51-
if self.instance and self.instance.pk and hasattr(self.instance, "tags"):
52-
existing_tags = list(self.instance.tags.all())
51+
if self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
52+
existing_tags = self.instance.subjects_keywords or []
5353
if existing_tags:
5454
# Convert tag objects to pipe-separated format for template
55-
tag_names = [str(tag) for tag in existing_tags]
56-
tag_string = " | ".join(tag_names)
55+
tag_names = [item.get("subject", "") for item in existing_tags if isinstance(item, dict)]
56+
tag_string = " | ".join(filter(None, tag_names))
5757
self.fields["invenio_tags"].initial = tag_string
5858
else:
5959
self.fields["invenio_tags"].initial = ""
@@ -168,13 +168,13 @@ def clean(self):
168168

169169
if invenio_tags_value:
170170
# Process the tags using the existing cleaning logic
171-
cleaned_data["tags"] = self.clean_keyword_tags()
172-
elif self.instance and self.instance.pk and hasattr(self.instance, "tags"):
171+
cleaned_data["subjects_keywords"] = self.clean_keyword_tags()
172+
elif self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
173173
# If no tags in form but instance has existing tags, preserve them
174-
existing_tags = list(self.instance.tags.all())
174+
existing_tags = self.instance.subjects_keywords or []
175175
if existing_tags:
176176
# Keep existing tags as they are
177-
cleaned_data["tags"] = existing_tags
177+
cleaned_data["subjects_keywords"] = existing_tags
178178
return cleaned_data
179179

180180
class Meta:
@@ -188,10 +188,10 @@ class Meta:
188188
"source_code_url",
189189
"port",
190190
"image",
191-
"tags",
191+
"subjects_keywords",
192192
"default_url_subpath",
193193
]
194194
labels = {
195-
"tags": "Subjects and keywords",
195+
"subjects_keywords": "Subjects and keywords",
196196
"note_on_linkonly_privacy": "Reason for choosing the link only option",
197197
}

apps/forms/depictio.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ def _setup_form_fields(self):
2626
)
2727

2828
# Load existing tags into the invenio_tags field
29-
if self.instance and self.instance.pk and hasattr(self.instance, "tags"):
30-
existing_tags = list(self.instance.tags.all())
29+
if self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
30+
existing_tags = self.instance.subjects_keywords or []
3131
if existing_tags:
3232
# Convert tag objects to pipe-separated format for template
33-
tag_names = [str(tag) for tag in existing_tags]
34-
tag_string = " | ".join(tag_names)
33+
tag_names = [item.get("subject", "") for item in existing_tags if isinstance(item, dict)]
34+
tag_string = " | ".join(filter(None, tag_names))
3535
self.fields["invenio_tags"].initial = tag_string
3636
else:
3737
self.fields["invenio_tags"].initial = ""
@@ -78,13 +78,13 @@ def clean(self):
7878

7979
if invenio_tags_value:
8080
# Process the tags using the existing cleaning logic
81-
cleaned_data["tags"] = self.clean_keyword_tags()
82-
elif self.instance and self.instance.pk and hasattr(self.instance, "tags"):
81+
cleaned_data["subjects_keywords"] = self.clean_keyword_tags()
82+
elif self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
8383
# If no tags in form but instance has existing tags, preserve them
84-
existing_tags = list(self.instance.tags.all())
84+
existing_tags = self.instance.subjects_keywords or []
8585
if existing_tags:
8686
# Keep existing tags as they are
87-
cleaned_data["tags"] = existing_tags
87+
cleaned_data["subjects_keywords"] = existing_tags
8888

8989
return cleaned_data
9090

@@ -101,7 +101,7 @@ def changed_data(self):
101101

102102
class Meta:
103103
model = DepictioInstance
104-
fields = ["name", "description", "access", "tags"]
104+
fields = ["name", "description", "access", "subjects_keywords"]
105105
labels = {
106-
"tags": "Subjects and keywords",
106+
"subjects_keywords": "Subjects and keywords",
107107
}

apps/forms/gradio.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ def __init__(self, *args, **kwargs):
4747

4848
def _load_existing_tags_to_invenio_field(self):
4949
"""Load existing database tags into the invenio_tags field"""
50-
if self.instance and self.instance.pk and hasattr(self.instance, "tags"):
51-
existing_tags = list(self.instance.tags.all())
50+
if self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
51+
existing_tags = self.instance.subjects_keywords or []
5252
if existing_tags:
5353
# Convert tag objects to pipe-separated format for template
54-
tag_names = [str(tag) for tag in existing_tags]
55-
tag_string = " | ".join(tag_names)
54+
tag_names = [item.get("subject", "") for item in existing_tags if isinstance(item, dict)]
55+
tag_string = " | ".join(filter(None, tag_names))
5656
self.fields["invenio_tags"].initial = tag_string
5757
else:
5858
self.fields["invenio_tags"].initial = ""
@@ -134,13 +134,13 @@ def clean(self):
134134

135135
if invenio_tags_value:
136136
# Process the tags using the existing cleaning logic
137-
cleaned_data["tags"] = self.clean_keyword_tags()
138-
elif self.instance and self.instance.pk and hasattr(self.instance, "tags"):
137+
cleaned_data["subjects_keywords"] = self.clean_keyword_tags()
138+
elif self.instance and self.instance.pk and hasattr(self.instance, "subjects_keywords"):
139139
# If no tags in form but instance has existing tags, preserve them
140-
existing_tags = list(self.instance.tags.all())
140+
existing_tags = self.instance.subjects_keywords or []
141141
if existing_tags:
142142
# Keep existing tags as they are
143-
cleaned_data["tags"] = existing_tags
143+
cleaned_data["subjects_keywords"] = existing_tags
144144
return cleaned_data
145145

146146
class Meta:
@@ -156,10 +156,10 @@ class Meta:
156156
"source_code_url",
157157
"port",
158158
"image",
159-
"tags",
159+
"subjects_keywords",
160160
"mount_path",
161161
]
162162
labels = {
163163
"note_on_linkonly_privacy": "Reason for choosing the link only option",
164-
"tags": "Subjects and keywords",
164+
"subjects_keywords": "Subjects and keywords",
165165
}

apps/forms/mixins.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,14 +425,24 @@ def clean_keyword_tags(self):
425425

426426
service = VocabularyMemoryService()
427427
valid_tags = []
428+
428429
for tag in tags_list:
429430
found = False
431+
430432
for term in service.term_metadata.values():
431433
if (term.subject or "").lower() == tag.lower():
432-
valid_tags.append(tag)
434+
valid_tags.append(
435+
{
436+
"subject": term.subject,
437+
"subject_scheme": term.subject_scheme,
438+
"classification_code": term.classification_code,
439+
}
440+
)
433441
found = True
434442
break
443+
435444
if not found:
436445
self.add_error("invenio_tags", f"Tag '{tag}' is not valid.")
437446
return []
447+
438448
return valid_tags

0 commit comments

Comments
 (0)