Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FC-0036] feat: Update ObjectTagView #180

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
Open edX Learning ("Learning Core").
"""
__version__ = "0.9.0"
__version__ = "0.9.1"
17 changes: 8 additions & 9 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,22 +191,21 @@ def to_representation(self, instance: list[ObjectTag]) -> dict:
return by_object


class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable=abstract-method
class ObjectTagUpdateByTaxonomySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer of the body for the ObjectTag UPDATE view
Serializer of a taxonomy item of ObjectTag UPDATE view.
"""

taxonomy = serializers.PrimaryKeyRelatedField(
queryset=Taxonomy.objects.all(), required=True
)
tags = serializers.ListField(child=serializers.CharField(), required=True)


class ObjectTagUpdateQueryParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method
class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer of the query params for the ObjectTag UPDATE view
Serializer of the body for the ObjectTag UPDATE view
"""

taxonomy = serializers.PrimaryKeyRelatedField(
queryset=Taxonomy.objects.all(), required=True
)
tagsData = serializers.ListField(child=ObjectTagUpdateByTaxonomySerializer(), required=True)


class TagDataSerializer(UserPermissionsSerializerMixin, serializers.Serializer): # pylint: disable=abstract-method
Expand Down
92 changes: 56 additions & 36 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
ObjectTagsByTaxonomySerializer,
ObjectTagSerializer,
ObjectTagUpdateBodySerializer,
ObjectTagUpdateQueryParamsSerializer,
TagDataSerializer,
TaxonomyExportQueryParamsSerializer,
TaxonomyImportBodySerializer,
Expand Down Expand Up @@ -501,8 +500,8 @@ def update(self, request, *args, **kwargs) -> Response:
Update ObjectTags that belong to a given object_id

Pass a list of Tag ids or Tag values to be applied to an object id in the
body `tag` parameter. Passing an empty list will remove all tags from
the object id.
body `tag` parameter, by each taxonomy. Passing an empty list will remove all tags from
the object id on an specific taxonomy.

**Example Body Requests**

Expand All @@ -511,54 +510,75 @@ def update(self, request, *args, **kwargs) -> Response:
**Example Body Requests**
```json
{
"tags": [1, 2, 3]
"tagsData": [
{
"taxonomy": 1,
"tags": [1, 2, 3]
},
{
"taxonomy": 1,
"tags": [1, 2, 3]
}
],
},
{
"tags": ["Tag 1", "Tag 2"]
"tagsData": [
{
"taxonomy": 1,
"tags": ["Tag 1", "Tag 2"]
},
]
},
{
"tags": []
"tagsData": [
{
"taxonomy": 1,
"tags": []
},
]
}
"""

partial = kwargs.pop('partial', False)
if partial:
raise MethodNotAllowed("PATCH", detail="PATCH not allowed")

query_params = ObjectTagUpdateQueryParamsSerializer(
data=request.query_params.dict()
)
query_params.is_valid(raise_exception=True)
taxonomy = query_params.validated_data.get("taxonomy", None)
taxonomy = taxonomy.cast()

object_id = kwargs.pop('object_id')
perm = "oel_tagging.can_tag_object"
body = ObjectTagUpdateBodySerializer(data=request.data)
body.is_valid(raise_exception=True)

object_id = kwargs.pop('object_id')
perm_obj = ObjectTagPermissionItem(
taxonomy=taxonomy,
object_id=object_id,
)
data = body.validated_data.get("tagsData", [])

if not request.user.has_perm(
perm,
# The obj arg expects a model, but we are passing an object
perm_obj, # type: ignore[arg-type]
):
raise PermissionDenied(
"You do not have permission to change object tags for this taxonomy or object_id."
)
if not data:
return self.retrieve(request, object_id)

body = ObjectTagUpdateBodySerializer(data=request.data)
body.is_valid(raise_exception=True)
# Check permissions
for tagsData in data:
taxonomy = tagsData.get("taxonomy")

tags = body.data.get("tags", [])
try:
tag_object(object_id, taxonomy, tags)
except TagDoesNotExist as e:
raise ValidationError from e
except ValueError as e:
raise ValidationError from e
perm_obj = ObjectTagPermissionItem(
taxonomy=taxonomy,
object_id=object_id,
)
if not request.user.has_perm(
perm,
# The obj arg expects a model, but we are passing an object
perm_obj, # type: ignore[arg-type]
):
raise PermissionDenied(
f"You do not have permission to change object tags for {str(taxonomy)} or object_id."
bradenmacdonald marked this conversation as resolved.
Show resolved Hide resolved
)

# Tag object_id per taxonomy
for tagsData in data:
taxonomy = tagsData.get("taxonomy")
tags = tagsData.get("tags", [])
try:
tag_object(object_id, taxonomy, tags)
except TagDoesNotExist as e:
raise ValidationError from e
except ValueError as e:
raise ValidationError from e

return self.retrieve(request, object_id)

Expand Down
1 change: 1 addition & 0 deletions tests/openedx_tagging/core/tagging/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def setUp(self):
self.language_taxonomy = LanguageTaxonomy.objects.get(name="Languages")
self.user_taxonomy = Taxonomy.objects.get(name="User Authors").cast()
self.free_text_taxonomy = api.create_taxonomy(name="Free Text", allow_free_text=True)
self.import_taxonomy = Taxonomy.objects.get(name="Import Taxonomy Test")

# References to some tags:
self.archaea = get_tag("Archaea")
Expand Down
116 changes: 103 additions & 13 deletions tests/openedx_tagging/core/tagging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

OBJECT_TAGS_RETRIEVE_URL = "/tagging/rest_api/v1/object_tags/{object_id}/"
OBJECT_TAG_COUNTS_URL = "/tagging/rest_api/v1/object_tag_counts/{object_id_pattern}/"
OBJECT_TAGS_UPDATE_URL = "/tagging/rest_api/v1/object_tags/{object_id}/?taxonomy={taxonomy_id}"
OBJECT_TAGS_UPDATE_URL = "/tagging/rest_api/v1/object_tags/{object_id}/"

LANGUAGE_TAXONOMY_ID = -1

Expand Down Expand Up @@ -1049,15 +1049,45 @@ def test_tag_object(self, user_attr, taxonomy_attr, taxonomy_flags, tag_values,

object_id = "abc"

url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=taxonomy.pk)
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)

response = self.client.put(url, {"tags": tag_values}, format="json")
data = [{
"taxonomy": taxonomy.pk,
"tags": tag_values,
}]

response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == expected_status
if status.is_success(expected_status):
assert [t["value"] for t in response.data[object_id]["taxonomies"][0]["tags"]] == tag_values
# And retrieving the object tags again should return an identical response:
assert response.data == self.client.get(OBJECT_TAGS_RETRIEVE_URL.format(object_id=object_id)).data

def test_tag_object_multiple_taxonomy(self):
self.client.force_authenticate(user=self.staff)

object_id = "abc"
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
tag_value_1 = ["Tag 4"]
tag_value_2 = ["Mammalia", "Fungi"]
data = [
{
"taxonomy": self.import_taxonomy.pk,
"tags": tag_value_1,
},
{
"taxonomy": self.taxonomy.pk,
"tags": tag_value_2,
},
]

response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == status.HTTP_200_OK
assert [t["value"] for t in response.data[object_id]["taxonomies"][0]["tags"]] == tag_value_1
assert [t["value"] for t in response.data[object_id]["taxonomies"][1]["tags"]] == tag_value_2
# And retrieving the object tags again should return an identical response:
assert response.data == self.client.get(OBJECT_TAGS_RETRIEVE_URL.format(object_id=object_id)).data

@ddt.data(
# Users and staff can clear tags
(None, {}, status.HTTP_401_UNAUTHORIZED),
Expand Down Expand Up @@ -1089,9 +1119,14 @@ def test_tag_object_clear(self, user_attr, taxonomy_flags, expected_status):
setattr(self.taxonomy, k, v)
self.taxonomy.save()

url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=self.taxonomy.pk)
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)

response = self.client.put(url, {"tags": []}, format="json")
data = [{
"taxonomy": self.taxonomy.pk,
"tags": [],
}]

response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == expected_status
if status.is_success(expected_status):
# Now there are no tags applied:
Expand All @@ -1103,6 +1138,47 @@ def test_tag_object_clear(self, user_attr, taxonomy_flags, expected_status):
self.taxonomy.save()
assert [ot.value for ot in api.get_object_tags(object_id=object_id)] == ["Fungi"]

def test_tag_object_clear_multiple_taxonomy(self):
object_id = "abc"
self.client.force_authenticate(user=self.staff)
api.tag_object(object_id=object_id, taxonomy=self.taxonomy, tags=["Mammalia", "Fungi"])
api.tag_object(object_id=object_id, taxonomy=self.import_taxonomy, tags=["Tag 4"])

url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
data = [
{
"taxonomy": self.import_taxonomy.pk,
"tags": [],
},
{
"taxonomy": self.taxonomy.pk,
"tags": [],
},
]

response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == status.HTTP_200_OK
assert response.data[object_id]["taxonomies"] == []

def test_tag_object_clear_simple_taxonomy(self):
object_id = "abc"
self.client.force_authenticate(user=self.staff)
tag_values = ["Mammalia", "Fungi"]
api.tag_object(object_id=object_id, taxonomy=self.taxonomy, tags=tag_values)
api.tag_object(object_id=object_id, taxonomy=self.import_taxonomy, tags=["Tag 4"])

url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
data = [
{
"taxonomy": self.import_taxonomy.pk,
"tags": [],
},
]

response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == status.HTTP_200_OK
assert [t["value"] for t in response.data[object_id]["taxonomies"][0]["tags"]] == tag_values

@ddt.data(
(None, status.HTTP_401_UNAUTHORIZED),
("user_1", status.HTTP_403_FORBIDDEN),
Expand All @@ -1114,9 +1190,14 @@ def test_tag_object_without_permission(self, user_attr, expected_status):
user = getattr(self, user_attr)
self.client.force_authenticate(user=user)

url = OBJECT_TAGS_UPDATE_URL.format(object_id="view_only", taxonomy_id=self.taxonomy.pk)
url = OBJECT_TAGS_UPDATE_URL.format(object_id="view_only")

data = [{
"taxonomy": self.taxonomy.pk,
"tags": ["Tag 1"],
}]

response = self.client.put(url, {"tags": ["Tag 1"]}, format="json")
response = self.client.put(url, {"tagsData": data}, format="json")
assert response.status_code == expected_status
assert not status.is_success(expected_status) # No success cases here

Expand All @@ -1127,22 +1208,31 @@ def test_tag_object_count_limit(self):
object_id = "limit_tag_count"
dummy_taxonomies = self.create_100_taxonomies()

url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=self.taxonomy.pk)
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
self.client.force_authenticate(user=self.staff)
response = self.client.put(url, {"tags": ["Tag 1"]}, format="json")
response = self.client.put(url, {"tagsData": [{
"taxonomy": self.taxonomy.pk,
"tags": ["Tag 1"],
}]}, format="json")
# Can't add another tag because the object already has 100 tags
assert response.status_code == status.HTTP_400_BAD_REQUEST

# The user can edit the tags that are already on the object
for taxonomy in dummy_taxonomies:
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=taxonomy.pk)
response = self.client.put(url, {"tags": ["New Tag"]}, format="json")
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
response = self.client.put(url, {"tagsData": [{
"taxonomy": taxonomy.pk,
"tags": ["New Tag"],
}]}, format="json")
assert response.status_code == status.HTTP_200_OK

# Editing tags adding another one will fail
for taxonomy in dummy_taxonomies:
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=taxonomy.pk)
response = self.client.put(url, {"tags": ["New Tag 1", "New Tag 2"]}, format="json")
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id)
response = self.client.put(url, {"tagsData": [{
"taxonomy": taxonomy.pk,
"tags": ["New Tag 1", "New Tag 2"],
}]}, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST


Expand Down
Loading