-
Notifications
You must be signed in to change notification settings - Fork 11
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
Enforce limit on number of tags per object #81
Changes from 7 commits
bad29ee
def631f
73aad4e
876dfe5
40602f7
f014c9b
c303cbb
3ddfcda
432e4c2
54d3fbd
c75751e
005379d
862ee91
8e875b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -404,6 +404,20 @@ def _find_object_tag_index(tag_ref, object_tags) -> int: | |
-1, | ||
) | ||
|
||
def _check_current_tag_count() -> None: | ||
""" | ||
Checks if the current count of tags for the object is less than 100 | ||
""" | ||
# Exclude self.id to avoid counting the tags that are going to be updated | ||
current_count = ObjectTag.objects.filter(object_id=object_id).exclude(taxonomy_id=self.id).count() | ||
|
||
if current_count >= 100: | ||
raise ValueError( | ||
_(f"Object ({object_id}) already have 100 or more tags.") | ||
) | ||
|
||
_check_current_tag_count() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this check also include the count of the new tags that will will be potentially be added, since this check is happening at the beginning of the function call? Eg: What happens when the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right @yusuf-musleh! I totally missed the allow_multiple cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 432e4c2 |
||
|
||
if not isinstance(tags, list): | ||
raise ValueError(_(f"Tags must be a list, not {type(tags).__name__}.")) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -406,7 +406,7 @@ def _object_permission(_user, object_id: str) -> bool: | |
""" | ||
Everyone have object permission on object_id "abc" | ||
""" | ||
return object_id == "abc" | ||
return object_id in ("abc", "limit_tag_count") | ||
|
||
super().setUp() | ||
|
||
|
@@ -449,28 +449,39 @@ def _object_permission(_user, object_id: str) -> bool: | |
) | ||
|
||
# Free-Text Taxonomies created by taxonomy admins, each linked | ||
# to 200 ObjectTags | ||
# to 10 ObjectTags | ||
self.open_taxonomy_enabled = Taxonomy.objects.create(name="Enabled Free-Text Taxonomy", allow_free_text=True) | ||
self.open_taxonomy_disabled = Taxonomy.objects.create( | ||
name="Disabled Free-Text Taxonomy", allow_free_text=True, enabled=False | ||
) | ||
for i in range(200): | ||
for i in range(10): | ||
ObjectTag.objects.create(object_id="abc", taxonomy=self.open_taxonomy_enabled, _value=f"Free Text {i}") | ||
ObjectTag.objects.create(object_id="abc", taxonomy=self.open_taxonomy_disabled, _value=f"Free Text {i}") | ||
|
||
self.dummy_taxonomies = [] | ||
for i in range(100): | ||
taxonomy = Taxonomy.objects.create(name=f"Dummy Taxonomy {i}", allow_free_text=True) | ||
ObjectTag.objects.create( | ||
object_id="limit_tag_count", | ||
taxonomy=taxonomy, | ||
_name=taxonomy.name, | ||
_value="Dummy Tag" | ||
) | ||
self.dummy_taxonomies.append(taxonomy) | ||
|
||
# Override the object permission for the test | ||
rules.set_perm("oel_tagging.change_objecttag_objectid", _object_permission) | ||
|
||
@ddt.data( | ||
(None, "abc", status.HTTP_403_FORBIDDEN, None, None), | ||
("user", "abc", status.HTTP_200_OK, 461, 10), | ||
("staff", "abc", status.HTTP_200_OK, 461, 10), | ||
(None, "non-existing-id", status.HTTP_403_FORBIDDEN, None, None), | ||
("user", "non-existing-id", status.HTTP_200_OK, 0, 0), | ||
("staff", "non-existing-id", status.HTTP_200_OK, 0, 0), | ||
(None, "abc", status.HTTP_403_FORBIDDEN, None), | ||
("user", "abc", status.HTTP_200_OK, 81), | ||
("staff", "abc", status.HTTP_200_OK, 81), | ||
(None, "non-existing-id", status.HTTP_403_FORBIDDEN, None), | ||
("user", "non-existing-id", status.HTTP_200_OK, 0), | ||
("staff", "non-existing-id", status.HTTP_200_OK, 0), | ||
) | ||
@ddt.unpack | ||
def test_retrieve_object_tags(self, user_attr, object_id, expected_status, expected_count, expected_results): | ||
def test_retrieve_object_tags(self, user_attr, object_id, expected_status, expected_count): | ||
""" | ||
Test retrieving object tags | ||
""" | ||
|
@@ -484,18 +495,16 @@ def test_retrieve_object_tags(self, user_attr, object_id, expected_status, expec | |
assert response.status_code == expected_status | ||
|
||
if status.is_success(expected_status): | ||
assert response.data.get("count") == expected_count | ||
assert response.data.get("results") is not None | ||
assert len(response.data.get("results")) == expected_results | ||
assert len(response.data) == expected_count | ||
|
||
@ddt.data( | ||
(None, "abc", status.HTTP_403_FORBIDDEN, None, None), | ||
("user", "abc", status.HTTP_200_OK, 20, 10), | ||
("staff", "abc", status.HTTP_200_OK, 20, 10), | ||
(None, "abc", status.HTTP_403_FORBIDDEN, None), | ||
("user", "abc", status.HTTP_200_OK, 20), | ||
("staff", "abc", status.HTTP_200_OK, 20), | ||
) | ||
@ddt.unpack | ||
def test_retrieve_object_tags_taxonomy_queryparam( | ||
self, user_attr, object_id, expected_status, expected_count, expected_results | ||
self, user_attr, object_id, expected_status, expected_count | ||
): | ||
""" | ||
Test retrieving object tags for specific taxonomies provided | ||
|
@@ -509,11 +518,8 @@ def test_retrieve_object_tags_taxonomy_queryparam( | |
response = self.client.get(url, {"taxonomy": self.enabled_taxonomy.pk}) | ||
assert response.status_code == expected_status | ||
if status.is_success(expected_status): | ||
assert response.data.get("count") == expected_count | ||
assert response.data.get("results") is not None | ||
assert len(response.data.get("results")) == expected_results | ||
object_tags = response.data.get("results") | ||
for object_tag in object_tags: | ||
assert len(response.data) == expected_count | ||
for object_tag in response.data: | ||
assert object_tag.get("is_valid") is True | ||
assert object_tag.get("taxonomy_id") == self.enabled_taxonomy.pk | ||
|
||
|
@@ -537,51 +543,6 @@ def test_retrieve_object_tags_invalid_taxonomy_queryparam(self, user_attr, objec | |
response = self.client.get(url, {"taxonomy": 123123}) | ||
assert response.status_code == expected_status | ||
|
||
@ddt.data( | ||
# Page 1, default page size 10, total count 200, returns 10 results | ||
(None, 1, None, status.HTTP_403_FORBIDDEN, None, None), | ||
("user", 1, None, status.HTTP_200_OK, 200, 10), | ||
("staff", 1, None, status.HTTP_200_OK, 200, 10), | ||
# Page 2, default page size 10, total count 200, returns 10 results | ||
(None, 2, None, status.HTTP_403_FORBIDDEN, None, None), | ||
("user", 2, None, status.HTTP_200_OK, 200, 10), | ||
("staff", 2, None, status.HTTP_200_OK, 200, 10), | ||
# Page 21, default page size 10, total count 200, no more results | ||
(None, 21, None, status.HTTP_403_FORBIDDEN, None, None), | ||
("user", 21, None, status.HTTP_404_NOT_FOUND, None, None), | ||
("staff", 21, None, status.HTTP_404_NOT_FOUND, None, None), | ||
# Page 3, page size 2, total count 200, returns 2 results | ||
(None, 3, 2, status.HTTP_403_FORBIDDEN, 200, 2), | ||
("user", 3, 2, status.HTTP_200_OK, 200, 2), | ||
("staff", 3, 2, status.HTTP_200_OK, 200, 2), | ||
) | ||
@ddt.unpack | ||
def test_retrieve_object_tags_pagination( | ||
self, user_attr, page, page_size, expected_status, expected_count, expected_results | ||
): | ||
""" | ||
Test pagination for retrieve object tags | ||
""" | ||
url = OBJECT_TAGS_RETRIEVE_URL.format(object_id="abc") | ||
|
||
if user_attr: | ||
user = getattr(self, user_attr) | ||
self.client.force_authenticate(user=user) | ||
|
||
query_params = {"taxonomy": self.open_taxonomy_enabled.pk, "page": page} | ||
if page_size: | ||
query_params["page_size"] = page_size | ||
|
||
response = self.client.get(url, query_params) | ||
assert response.status_code == expected_status | ||
if status.is_success(expected_status): | ||
assert response.data.get("count") == expected_count | ||
assert response.data.get("results") is not None | ||
assert len(response.data.get("results")) == expected_results | ||
object_tags = response.data.get("results") | ||
for object_tag in object_tags: | ||
assert object_tag.get("taxonomy_id") == self.open_taxonomy_enabled.pk | ||
|
||
@ddt.data( | ||
(None, "POST", status.HTTP_403_FORBIDDEN), | ||
(None, "PATCH", status.HTTP_403_FORBIDDEN), | ||
|
@@ -660,8 +621,8 @@ def test_tag_object(self, user_attr, taxonomy_attr, tag_values, expected_status) | |
response = self.client.put(url, {"tags": tag_values}, format="json") | ||
assert response.status_code == expected_status | ||
if status.is_success(expected_status): | ||
assert len(response.data.get("results")) == len(tag_values) | ||
assert set(t["value"] for t in response.data["results"]) == set(tag_values) | ||
assert len(response.data) == len(tag_values) | ||
assert set(t["value"] for t in response.data) == set(tag_values) | ||
|
||
@ddt.data( | ||
# Can't add invalid tags to a closed taxonomy | ||
|
@@ -727,8 +688,8 @@ def test_tag_object_clear(self, user_attr, taxonomy_attr, tag_values, expected_s | |
response = self.client.put(url, {"tags": tag_values}, format="json") | ||
assert response.status_code == expected_status | ||
if status.is_success(expected_status): | ||
assert len(response.data.get("results")) == len(tag_values) | ||
assert set(t["value"] for t in response.data["results"]) == set(tag_values) | ||
assert len(response.data) == len(tag_values) | ||
assert set(t["value"] for t in response.data) == set(tag_values) | ||
|
||
@ddt.data( | ||
# Users and staff can add multiple tags to a allow_multiple=True taxonomy | ||
|
@@ -764,8 +725,8 @@ def test_tag_object_multiple(self, user_attr, taxonomy_attr, tag_values, expecte | |
response = self.client.put(url, {"tags": tag_values}, format="json") | ||
assert response.status_code == expected_status | ||
if status.is_success(expected_status): | ||
assert len(response.data.get("results")) == len(tag_values) | ||
assert set(t["value"] for t in response.data["results"]) == set(tag_values) | ||
assert len(response.data) == len(tag_values) | ||
assert set(t["value"] for t in response.data) == set(tag_values) | ||
|
||
@ddt.data( | ||
(None, status.HTTP_403_FORBIDDEN), | ||
|
@@ -782,3 +743,21 @@ def test_tag_object_without_permission(self, user_attr, expected_status): | |
|
||
response = self.client.put(url, {"tags": ["Tag 1"]}, format="json") | ||
assert response.status_code == expected_status | ||
assert not status.is_success(expected_status) # No success cases here | ||
|
||
def test_tag_object_count_limit(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would super helpful if we could also add a test case that covers the scenario described above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed! Thank you! Done here: 432e4c2 |
||
""" | ||
Checks if the limit of 100 tags per object is enforced | ||
""" | ||
object_id = "limit_tag_count" | ||
url = OBJECT_TAGS_UPDATE_URL.format(object_id=object_id, taxonomy_id=self.enabled_taxonomy.pk) | ||
self.client.force_authenticate(user=self.staff) | ||
response = self.client.put(url, {"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 self.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") | ||
assert response.status_code == status.HTTP_200_OK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: just a small spelling fix, "already have" -> "already has"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed here:
openedx-learning/openedx_tagging/core/tagging/models/base.py
Line 458 in 432e4c2
We are talking about the future state, not the current.