Skip to content

Commit f78ab9a

Browse files
authored
Merge pull request #3354 from rtibbles/dotpath_deletion
Tests and updates for addition/deletion of metadata labels
2 parents ecb82b9 + 428b3a8 commit f78ab9a

3 files changed

Lines changed: 102 additions & 6 deletions

File tree

contentcuration/contentcuration/tests/viewsets/test_contentnode.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django_concurrent_tests.helpers import make_concurrent_calls
1616
from le_utils.constants import content_kinds
1717
from le_utils.constants import roles
18+
from le_utils.constants.labels.accessibility_categories import ACCESSIBILITYCATEGORIESLIST
1819

1920
from contentcuration import models
2021
from contentcuration.tests import testdata
@@ -615,6 +616,64 @@ def test_update_contentnode_extra_fields(self):
615616
models.ContentNode.objects.get(id=contentnode.id).extra_fields["randomize"], randomize
616617
)
617618

619+
def test_update_contentnode_remove_from_extra_fields(self):
620+
user = testdata.user()
621+
metadata = self.contentnode_db_metadata
622+
metadata["extra_fields"] = {
623+
"m": 5,
624+
}
625+
contentnode = models.ContentNode.objects.create(**metadata)
626+
self.client.force_authenticate(user=user)
627+
# Remove extra_fields.m
628+
response = self.client.post(
629+
self.sync_url,
630+
[generate_update_event(contentnode.id, CONTENTNODE, {"extra_fields.m": None})],
631+
format="json",
632+
)
633+
self.assertEqual(response.status_code, 200, response.content)
634+
with self.assertRaises(KeyError):
635+
models.ContentNode.objects.get(id=contentnode.id).extra_fields["m"]
636+
637+
def test_update_contentnode_add_multiple_metadata_labels(self):
638+
user = testdata.user()
639+
640+
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)
641+
self.client.force_authenticate(user=user)
642+
# Add metadata label to accessibility_labels
643+
response = self.client.post(
644+
self.sync_url,
645+
[generate_update_event(contentnode.id, CONTENTNODE, {"accessibility_labels.{}".format(ACCESSIBILITYCATEGORIESLIST[0]): True})],
646+
format="json",
647+
)
648+
self.assertEqual(response.status_code, 200, response.content)
649+
self.assertTrue(models.ContentNode.objects.get(id=contentnode.id).accessibility_labels[ACCESSIBILITYCATEGORIESLIST[0]])
650+
651+
response = self.client.post(
652+
self.sync_url,
653+
[generate_update_event(contentnode.id, CONTENTNODE, {"accessibility_labels.{}".format(ACCESSIBILITYCATEGORIESLIST[1]): True})],
654+
format="json",
655+
)
656+
self.assertEqual(response.status_code, 200, response.content)
657+
self.assertTrue(models.ContentNode.objects.get(id=contentnode.id).accessibility_labels[ACCESSIBILITYCATEGORIESLIST[0]])
658+
self.assertTrue(models.ContentNode.objects.get(id=contentnode.id).accessibility_labels[ACCESSIBILITYCATEGORIESLIST[1]])
659+
660+
def test_update_contentnode_remove_metadata_label(self):
661+
user = testdata.user()
662+
metadata = self.contentnode_db_metadata
663+
metadata["accessibility_labels"] = {ACCESSIBILITYCATEGORIESLIST[0]: True}
664+
665+
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)
666+
self.client.force_authenticate(user=user)
667+
# Add metadata label to accessibility_labels
668+
response = self.client.post(
669+
self.sync_url,
670+
[generate_update_event(contentnode.id, CONTENTNODE, {"accessibility_labels.{}".format(ACCESSIBILITYCATEGORIESLIST[0]): None})],
671+
format="json",
672+
)
673+
self.assertEqual(response.status_code, 200, response.content)
674+
with self.assertRaises(KeyError):
675+
models.ContentNode.objects.get(id=contentnode.id).accessibility_labels[ACCESSIBILITYCATEGORIESLIST[0]]
676+
618677
def test_update_contentnode_tags(self):
619678
user = testdata.user()
620679
contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata)

contentcuration/contentcuration/viewsets/common.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def get_value(self, dictionary):
125125
# get just field name
126126
value = dictionary.get(self.field_name, {})
127127

128+
self.initial_value = value
129+
128130
if value is None:
129131
return empty
130132

@@ -151,7 +153,31 @@ def create(self, validated_data):
151153
return instance
152154

153155
def update(self, instance, validated_data):
156+
instance = instance or self.default_value()
154157
instance.update(validated_data)
158+
# This should have been set when get_value was invoked
159+
# But could be `None`, so we check if it is truthy here.
160+
if getattr(self, "initial_value", None):
161+
# Iterate through each field
162+
for key in self.initial_value:
163+
# If the field is explicitly being set as None, then
164+
# we need to delete it from the instance.
165+
if self.initial_value[key] is None:
166+
# Follow the dot path to find the nested object
167+
obj = instance
168+
# Iterate through each part of the dot path
169+
# up until, but not including the final key
170+
for part in key.split(".")[:-1]:
171+
if isinstance(obj, dict):
172+
# If it's a dict use get to get the next level object
173+
obj = obj.get(part)
174+
elif isinstance(obj, list):
175+
# If it's a list, use the index to get the next level object
176+
obj = obj[int(part)]
177+
else:
178+
raise ValidationError("Tried to access a dot path in an invalid type")
179+
# Finally, delete the final key
180+
obj.pop(key.split(".")[-1])
155181
return instance
156182

157183

contentcuration/contentcuration/viewsets/contentnode.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def __init__(self, choices, *args, **kwargs):
280280
def get_fields(self):
281281
fields = {}
282282
for label_id, label_name in self.choices:
283-
field = BooleanField(required=False, label=label_name)
283+
field = BooleanField(required=False, label=label_name, allow_null=True)
284284
fields[label_id] = field
285285
return fields
286286

@@ -306,6 +306,16 @@ class ContentNodeSerializer(BulkModelSerializer):
306306
categories = MetadataLabelsField(subjects.choices, required=False)
307307
learner_needs = MetadataLabelsField(needs.choices, required=False)
308308

309+
dict_fields = [
310+
"extra_fields",
311+
"grade_levels",
312+
"resource_types",
313+
"learning_activities",
314+
"accessibility_labels",
315+
"categories",
316+
"learner_needs",
317+
]
318+
309319
class Meta:
310320
model = ContentNode
311321
fields = (
@@ -368,11 +378,12 @@ def update(self, instance, validated_data):
368378
{"parent": "This field should only be changed by a move operation"}
369379
)
370380

371-
extra_fields = validated_data.pop("extra_fields", None)
372-
if extra_fields is not None:
373-
validated_data["extra_fields"] = self.fields["extra_fields"].update(
374-
instance.extra_fields, extra_fields
375-
)
381+
for field in self.dict_fields:
382+
field_data = validated_data.pop(field, None)
383+
if field_data is not None:
384+
validated_data[field] = self.fields[field].update(
385+
getattr(instance, field), field_data
386+
)
376387
if "tags" in validated_data:
377388
tags = validated_data.pop("tags")
378389
set_tags({instance.id: tags})

0 commit comments

Comments
 (0)