From bbb24e6404cc967b5bae576df1075053ec57d488 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 20 Jun 2023 15:07:46 -0500 Subject: [PATCH 01/14] feat: Autocomplete tags --- openedx_tagging/core/tagging/api.py | 34 +++++++++ .../0002_alter_objecttag_index_together.py | 17 +++++ .../openedx_tagging/core/tagging/test_api.py | 71 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 5d8ca1158..b52c6fca9 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -137,3 +137,37 @@ def tag_object( Preserves existing (valid) tags, adds new (valid) tags, and removes omitted (or invalid) tags. """ return taxonomy.cast().tag_object(tags, object_id) + + +def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[str]: + """ + Returns the first `count` tag values in the given Taxonomy with names + that begin with the given prefix string. The result is returned in alphabetical + + Closed taxonomies return tag values that exist in the taxonomy. Also excludes all Tags that the + `object_id` already has. + + Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes + the tags used by `object_id`. + """ + result = [] + excluded_tags = ObjectTag.objects.filter(object_id=object_id, taxonomy=taxonomy) + if taxonomy.allow_free_text: + # Free-text taxonomy + excluded_tags = excluded_tags.values_list('_value', flat=True) + result = ObjectTag.objects.filter(taxonomy=taxonomy, _value__startswith=prefix) \ + .exclude(object_id=object_id) \ + .exclude(_value__in=excluded_tags) \ + .order_by('_value') \ + .values_list('_value', flat=True) + # Remove repeat tags + result = list(set(result))[:count] + else: + # Closed taxonomy + excluded_tags = excluded_tags.filter(tag__isnull=False).values_list('tag__id', flat=True) + result = Tag.objects.filter(taxonomy=taxonomy, value__startswith=prefix) \ + .exclude(id__in=excluded_tags) \ + .order_by('value') \ + .values_list('value', flat=True) \ + [:count] + return result diff --git a/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py b/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py new file mode 100644 index 000000000..e35285e55 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.19 on 2023-06-20 20:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0001_squashed'), + ] + + operations = [ + migrations.AlterIndexTogether( + name='objecttag', + index_together={('taxonomy', '_value')}, + ), + ] diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index a75aebb61..4cfb95961 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -460,3 +460,74 @@ def test_get_object_tags(self): ) == [ beta, ] + + def test_autocomplete_tags_closed_taxonomy(self): + object_id = 'course_id_1' + prefix = 'a' + self._validate_autocomplete_tags(self.taxonomy, prefix, object_id) + + def test_autocomplete_tags_free_text_taxonomy(self): + object_id_1 = 'course_id_1' + object_id_2 = 'course_id_2' + prefix = 'a' + + taxonomy = tagging_api.create_taxonomy("Free_Text_Taxonomy", allow_free_text=True) + tags = [ + 'Archaea', + 'Archaebacteria', + 'Animalia', + 'Arthropoda', + 'Plantae', + 'Monera', + 'Gastrotrich', + 'Placozoa', + # For testing repeats + 'Animalia', + 'Arthropoda', + ] + for tag in tags: + ObjectTag( + object_id=object_id_1, + object_type='course', + taxonomy=taxonomy, + _value=tag, + ).save() + + self._validate_autocomplete_tags(taxonomy, prefix, object_id_2, free_text=True) + + def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=False): + """ + Validate autocomplete tags + """ + + # Normal search + result = tagging_api.autocomplete_tags(taxonomy, prefix) + self.assertEqual(len(result), 4) + for tag in result: + self.assertEqual(tag[0].lower(), prefix) + + # Search with count + result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) + self.assertEqual(len(result), 2) + + # Create ObjectTag to simulate the content tagging + for tag in result: + tag_model = None + if not free_text: + tag_model = get_tag(tag) + + ObjectTag( + object_id=object_id, + object_type='course', + taxonomy=taxonomy, + tag=tag_model, + _value=tag, + ).save() + + # Search with object + result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) + self.assertEqual(len(result), 2) + + # Search with object and count + result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) + self.assertEqual(len(result), 1) From 8ce5f8e993e69e287735f3d7d40006eb5457b96a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 21 Jun 2023 14:25:02 -0500 Subject: [PATCH 02/14] chore: Adding comments, fixing some cases --- openedx_tagging/core/tagging/api.py | 50 ++++++++++++++----- .../openedx_tagging/core/tagging/test_api.py | 16 ++++-- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index b52c6fca9..30544ef6a 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -151,23 +151,47 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou the tags used by `object_id`. """ result = [] + + # Fetch tags that the object already has to exclude them from the result excluded_tags = ObjectTag.objects.filter(object_id=object_id, taxonomy=taxonomy) if taxonomy.allow_free_text: # Free-text taxonomy + + # Obtain the value of the excluded tags excluded_tags = excluded_tags.values_list('_value', flat=True) - result = ObjectTag.objects.filter(taxonomy=taxonomy, _value__startswith=prefix) \ - .exclude(object_id=object_id) \ - .exclude(_value__in=excluded_tags) \ - .order_by('_value') \ - .values_list('_value', flat=True) - # Remove repeat tags - result = list(set(result))[:count] + result = ( + # Fetch object tags from this taxonomy whose value starts with the given prefix + ObjectTag.objects.filter(taxonomy=taxonomy, _value__startswith=prefix) + # omit any tags applied to the given object + .exclude(object_id=object_id) + # omit any free-text tags from other objects whose values match the tags on the given object + .exclude(_value__in=excluded_tags) + # alphabetical ordering + .order_by('_value') + # obtain the values of the tags + .values_list('_value', flat=True) + # remove repeats + .distinct() + # get only first `count` values + [:count] + ) else: # Closed taxonomy + + # Obtain the id of the excluded tags excluded_tags = excluded_tags.filter(tag__isnull=False).values_list('tag__id', flat=True) - result = Tag.objects.filter(taxonomy=taxonomy, value__startswith=prefix) \ - .exclude(id__in=excluded_tags) \ - .order_by('value') \ - .values_list('value', flat=True) \ - [:count] - return result + result = ( + # Fetch tags from this taxonomy whose value starts with the given prefix + Tag.objects.filter(taxonomy=taxonomy, value__startswith=prefix) + # omit any tags applied to the given object + .exclude(id__in=excluded_tags) + # alphabetical ordering + .order_by('value') + # obtain the values of the tags + .values_list('value', flat=True) + # remove repeats + .distinct() + # get only first `count` values + [:count] + ) + return list(result) diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 4cfb95961..991683cf9 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -464,6 +464,14 @@ def test_get_object_tags(self): def test_autocomplete_tags_closed_taxonomy(self): object_id = 'course_id_1' prefix = 'a' + + # Creating a repeated tag to test that case + Tag( + taxonomy=self.taxonomy, + value="Archaebacteria", + external_id="tag_30", + ).save() + self._validate_autocomplete_tags(self.taxonomy, prefix, object_id) def test_autocomplete_tags_free_text_taxonomy(self): @@ -502,13 +510,13 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal # Normal search result = tagging_api.autocomplete_tags(taxonomy, prefix) - self.assertEqual(len(result), 4) + self.assertEqual(result, ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"]) for tag in result: self.assertEqual(tag[0].lower(), prefix) # Search with count result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) - self.assertEqual(len(result), 2) + self.assertEqual(result, ["Animalia", "Archaea"]) # Create ObjectTag to simulate the content tagging for tag in result: @@ -526,8 +534,8 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal # Search with object result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) - self.assertEqual(len(result), 2) + self.assertEqual(result, ["Archaebacteria", "Arthropoda"]) # Search with object and count result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) - self.assertEqual(len(result), 1) + self.assertEqual(result, ["Archaebacteria"]) From f67bacb67e98b49f9575cf3ee772f088d79e5b51 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 21 Jun 2023 16:48:01 -0500 Subject: [PATCH 03/14] chore: autocomplete_tags function updated To return a list of TagResult containing the id and value of the tags. --- openedx_tagging/core/tagging/api.py | 27 +++++++---- .../0002_alter_objecttag_index_together.py | 17 ------- openedx_tagging/core/tagging/models/base.py | 9 ++++ .../openedx_tagging/core/tagging/test_api.py | 47 ++++++++++++++++--- 4 files changed, 68 insertions(+), 32 deletions(-) delete mode 100644 openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 30544ef6a..cbacbf3ca 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -15,8 +15,7 @@ from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ -from .models import ObjectTag, Tag, Taxonomy - +from .models import ObjectTag, Tag, Taxonomy, TagResult def create_taxonomy( name: str, @@ -139,7 +138,7 @@ def tag_object( return taxonomy.cast().tag_object(tags, object_id) -def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[str]: +def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[TagResult]: """ Returns the first `count` tag values in the given Taxonomy with names that begin with the given prefix string. The result is returned in alphabetical @@ -175,6 +174,8 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou # get only first `count` values [:count] ) + # Build tag results + result = [TagResult(id=None, value=value) for value in result] else: # Closed taxonomy @@ -187,11 +188,21 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou .exclude(id__in=excluded_tags) # alphabetical ordering .order_by('value') - # obtain the values of the tags - .values_list('value', flat=True) - # remove repeats - .distinct() + # obtain only id and values of the tags + .values('id', 'value') # get only first `count` values [:count] ) - return list(result) + + # We need to delete the repeats here because the current database backend + # don't support `distinct()` on fields + unique_results = {} + for item in result: + value = item['value'] + if value not in unique_results: + unique_results[value] = item + result = list(unique_results.values()) + + # Build tag results + result = [TagResult(id=item['id'], value=item['value']) for item in result] + return result diff --git a/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py b/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py deleted file mode 100644 index e35285e55..000000000 --- a/openedx_tagging/core/tagging/migrations/0002_alter_objecttag_index_together.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.19 on 2023-06-20 20:04 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('oel_tagging', '0001_squashed'), - ] - - operations = [ - migrations.AlterIndexTogether( - name='objecttag', - index_together={('taxonomy', '_value')}, - ), - ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 499149845..b099a4649 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -668,3 +668,12 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._value = object_tag._value self._name = object_tag._name return self + +class TagResult: + """ + Lightweight class used to return tags on some API methods + """ + + def __init__(self, id: Union[str, None], value: str): + self.id = id + self.value = value diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 991683cf9..9bfe20d65 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -503,6 +503,12 @@ def test_autocomplete_tags_free_text_taxonomy(self): self._validate_autocomplete_tags(taxonomy, prefix, object_id_2, free_text=True) + def _get_tag_result_values(self, tag_results): + return [tag_result.value for tag_result in tag_results] + + def _get_tag_result_ids(self, tag_results): + return [tag_result.id for tag_result in tag_results] + def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=False): """ Validate autocomplete tags @@ -510,16 +516,31 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal # Normal search result = tagging_api.autocomplete_tags(taxonomy, prefix) - self.assertEqual(result, ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"]) - for tag in result: + tag_ids = self._get_tag_result_ids(result) + tag_values = self._get_tag_result_values(result) + self.assertEqual(tag_values, ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"]) + + for tag in tag_values: self.assertEqual(tag[0].lower(), prefix) + if free_text: + self.assertEqual(tag_ids, [None, None, None, None]) + else: + self.assertEqual(tag_ids, [9,2,5,14]) + # Search with count result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) - self.assertEqual(result, ["Animalia", "Archaea"]) + tag_ids = self._get_tag_result_ids(result) + tag_values = self._get_tag_result_values(result) + self.assertEqual(tag_values, ["Animalia", "Archaea"]) + + if free_text: + self.assertEqual(tag_ids, [None, None]) + else: + self.assertEqual(tag_ids, [9,2]) # Create ObjectTag to simulate the content tagging - for tag in result: + for tag in tag_values: tag_model = None if not free_text: tag_model = get_tag(tag) @@ -534,8 +555,20 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal # Search with object result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) - self.assertEqual(result, ["Archaebacteria", "Arthropoda"]) + tag_ids = self._get_tag_result_ids(result) + self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria", "Arthropoda"]) + + if free_text: + self.assertEqual(tag_ids, [None, None]) + else: + self.assertEqual(tag_ids, [5,14]) - # Search with object and count + # Search with object and coun result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) - self.assertEqual(result, ["Archaebacteria"]) + tag_ids = self._get_tag_result_ids(result) + self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria"]) + + if free_text: + self.assertEqual(tag_ids, [None]) + else: + self.assertEqual(tag_ids, [5]) From 2b7948bde75a58e42c740075803aa3ecf179e839 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 22 Jun 2023 14:44:17 -0500 Subject: [PATCH 04/14] chore: Fix MySQL case-sensitive error We need to use __istartswith to make a case-insensitive query on MySQL --- openedx_tagging/core/tagging/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index cbacbf3ca..4b0906d7e 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -160,7 +160,7 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou excluded_tags = excluded_tags.values_list('_value', flat=True) result = ( # Fetch object tags from this taxonomy whose value starts with the given prefix - ObjectTag.objects.filter(taxonomy=taxonomy, _value__startswith=prefix) + ObjectTag.objects.filter(taxonomy=taxonomy, _value__istartswith=prefix) # omit any tags applied to the given object .exclude(object_id=object_id) # omit any free-text tags from other objects whose values match the tags on the given object @@ -183,7 +183,7 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou excluded_tags = excluded_tags.filter(tag__isnull=False).values_list('tag__id', flat=True) result = ( # Fetch tags from this taxonomy whose value starts with the given prefix - Tag.objects.filter(taxonomy=taxonomy, value__startswith=prefix) + Tag.objects.filter(taxonomy=taxonomy, value__istartswith=prefix) # omit any tags applied to the given object .exclude(id__in=excluded_tags) # alphabetical ordering From 73167865a9a4ff8dcf2db933372831b11e012773 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 29 Jun 2023 12:37:15 -0500 Subject: [PATCH 05/14] chore: Duplicate tags behavior updated --- openedx_tagging/core/tagging/api.py | 9 --------- openedx_tagging/core/tagging/models/base.py | 1 + tests/openedx_tagging/core/tagging/test_api.py | 7 ------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 4b0906d7e..25af40bc8 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -194,15 +194,6 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou [:count] ) - # We need to delete the repeats here because the current database backend - # don't support `distinct()` on fields - unique_results = {} - for item in result: - value = item['value'] - if value not in unique_results: - unique_results[value] = item - result = list(unique_results.values()) - # Build tag results result = [TagResult(id=item['id'], value=item['value']) for item in result] return result diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index b099a4649..c1217d6ba 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -669,6 +669,7 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._name = object_tag._name return self + class TagResult: """ Lightweight class used to return tags on some API methods diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 9bfe20d65..2953baa58 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -465,13 +465,6 @@ def test_autocomplete_tags_closed_taxonomy(self): object_id = 'course_id_1' prefix = 'a' - # Creating a repeated tag to test that case - Tag( - taxonomy=self.taxonomy, - value="Archaebacteria", - external_id="tag_30", - ).save() - self._validate_autocomplete_tags(self.taxonomy, prefix, object_id) def test_autocomplete_tags_free_text_taxonomy(self): From ccd82de20a5105f37e139418da651ed24f5d1a8c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 29 Jun 2023 13:57:26 -0500 Subject: [PATCH 06/14] chore: Index and query updates --- openedx_tagging/core/tagging/api.py | 4 +--- ..._objecttag_oel_tagging_taxonom_aa24e6_idx.py | 17 +++++++++++++++++ openedx_tagging/core/tagging/models/base.py | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 25af40bc8..a83998b36 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -161,9 +161,7 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou result = ( # Fetch object tags from this taxonomy whose value starts with the given prefix ObjectTag.objects.filter(taxonomy=taxonomy, _value__istartswith=prefix) - # omit any tags applied to the given object - .exclude(object_id=object_id) - # omit any free-text tags from other objects whose values match the tags on the given object + # omit any free-text tags whose values match the tags on the given object .exclude(_value__in=excluded_tags) # alphabetical ordering .order_by('_value') diff --git a/openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py b/openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py new file mode 100644 index 000000000..922b093d1 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.19 on 2023-06-29 17:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0001_initial'), + ] + + operations = [ + migrations.AddIndex( + model_name='objecttag', + index=models.Index(fields=['taxonomy', 'object_id'], name='oel_tagging_taxonom_aa24e6_idx'), + ), + ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index c1217d6ba..c94a8036b 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -496,6 +496,7 @@ class Meta: indexes = [ models.Index(fields=["taxonomy", "object_id"]), models.Index(fields=["taxonomy", "_value"]), + models.Index(fields=["taxonomy", "object_id"]), ] def __repr__(self): From 72005a78960ce74c948c1b0c11c262494c12c79b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 29 Jun 2023 14:19:05 -0500 Subject: [PATCH 07/14] chore: autocomplete_tags_result added --- openedx_tagging/core/tagging/api.py | 18 ++++++++++++------ tests/openedx_tagging/core/tagging/test_api.py | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index a83998b36..fc77252d8 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -138,7 +138,7 @@ def tag_object( return taxonomy.cast().tag_object(tags, object_id) -def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[TagResult]: +def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> QuerySet: """ Returns the first `count` tag values in the given Taxonomy with names that begin with the given prefix string. The result is returned in alphabetical @@ -158,7 +158,7 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou # Obtain the value of the excluded tags excluded_tags = excluded_tags.values_list('_value', flat=True) - result = ( + return ( # Fetch object tags from this taxonomy whose value starts with the given prefix ObjectTag.objects.filter(taxonomy=taxonomy, _value__istartswith=prefix) # omit any free-text tags whose values match the tags on the given object @@ -172,14 +172,12 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou # get only first `count` values [:count] ) - # Build tag results - result = [TagResult(id=None, value=value) for value in result] else: # Closed taxonomy # Obtain the id of the excluded tags excluded_tags = excluded_tags.filter(tag__isnull=False).values_list('tag__id', flat=True) - result = ( + return ( # Fetch tags from this taxonomy whose value starts with the given prefix Tag.objects.filter(taxonomy=taxonomy, value__istartswith=prefix) # omit any tags applied to the given object @@ -192,6 +190,14 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou [:count] ) - # Build tag results + +def autocomplete_tags_result(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[TagResult]: + """ + Calls `autocomplete_tags` and serialize the results into `TagResult` + """ + result = autocomplete_tags(taxonomy, prefix, object_id=object_id, count=count) + if taxonomy.allow_free_text: + result = [TagResult(id=None, value=value) for value in result] + else: result = [TagResult(id=item['id'], value=item['value']) for item in result] return result diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 2953baa58..c45fcaeaa 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -508,7 +508,7 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal """ # Normal search - result = tagging_api.autocomplete_tags(taxonomy, prefix) + result = tagging_api.autocomplete_tags_result(taxonomy, prefix) tag_ids = self._get_tag_result_ids(result) tag_values = self._get_tag_result_values(result) self.assertEqual(tag_values, ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"]) @@ -522,7 +522,7 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal self.assertEqual(tag_ids, [9,2,5,14]) # Search with count - result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) + result = tagging_api.autocomplete_tags_result(taxonomy, prefix, count=2) tag_ids = self._get_tag_result_ids(result) tag_values = self._get_tag_result_values(result) self.assertEqual(tag_values, ["Animalia", "Archaea"]) @@ -547,7 +547,7 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal ).save() # Search with object - result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) + result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id) tag_ids = self._get_tag_result_ids(result) self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria", "Arthropoda"]) @@ -557,7 +557,7 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal self.assertEqual(tag_ids, [5,14]) # Search with object and coun - result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) + result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id, count=1) tag_ids = self._get_tag_result_ids(result) self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria"]) From 56a2e3e028228c07ddb7182527d017e1b054a60c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 3 Jul 2023 15:53:21 -0500 Subject: [PATCH 08/14] chore: Unique constraint on Tag model added --- ...m_aa24e6_idx.py => 0002_auto_20230703_1548.py} | 6 +++++- tests/openedx_tagging/core/tagging/test_models.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) rename openedx_tagging/core/tagging/migrations/{0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py => 0002_auto_20230703_1548.py} (66%) diff --git a/openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py b/openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py similarity index 66% rename from openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py rename to openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py index 922b093d1..1f35efb2b 100644 --- a/openedx_tagging/core/tagging/migrations/0002_objecttag_oel_tagging_taxonom_aa24e6_idx.py +++ b/openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.19 on 2023-06-29 17:39 +# Generated by Django 3.2.19 on 2023-07-03 20:48 from django.db import migrations, models @@ -10,6 +10,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AlterUniqueTogether( + name='tag', + unique_together={('taxonomy', 'value')}, + ), migrations.AddIndex( model_name='objecttag', index=models.Index(fields=['taxonomy', 'object_id'], name='oel_tagging_taxonom_aa24e6_idx'), diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 2db7911af..a30093bd4 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -2,6 +2,7 @@ import ddt from django.contrib.auth import get_user_model from django.test.testcases import TestCase +from django.db.utils import IntegrityError from openedx_tagging.core.tagging.models import ( ObjectTag, @@ -237,6 +238,20 @@ def test_get_tags_shallow_taxonomy(self): with self.assertNumQueries(2): assert taxonomy.get_tags() == tags + def test_unique_tags(self): + # Creating new tag + Tag( + taxonomy=self.taxonomy, + value='New value' + ).save() + + # Creating repeated tag + with self.assertRaises(IntegrityError): + Tag( + taxonomy=self.taxonomy, + value=self.archaea.value, + ).save() + class TestModelObjectTag(TestTagTaxonomyMixin, TestCase): """ From c5e4bb55e541c9c4bad9c7299d0f6bb6f9ea6115 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 25 Jul 2023 13:06:19 -0500 Subject: [PATCH 09/14] feat: unique constraint to ObjectTag --- openedx_tagging/core/tagging/api.py | 31 ++++++++++------- .../migrations/0002_auto_20230703_1548.py | 21 ------------ .../migrations/0003_auto_20230725_1257.py | 26 ++++++++++++++ openedx_tagging/core/tagging/models/base.py | 3 +- .../openedx_tagging/core/tagging/test_api.py | 34 +++++++++---------- 5 files changed, 63 insertions(+), 52 deletions(-) delete mode 100644 openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py create mode 100644 openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index fc77252d8..686317d7d 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -17,6 +17,7 @@ from .models import ObjectTag, Tag, Taxonomy, TagResult + def create_taxonomy( name: str, description: str = None, @@ -138,13 +139,15 @@ def tag_object( return taxonomy.cast().tag_object(tags, object_id) -def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> QuerySet: +def autocomplete_tags( + taxonomy: Taxonomy, prefix: str, object_id: str = None, count=10 +) -> QuerySet: """ - Returns the first `count` tag values in the given Taxonomy with names + Returns the first `count` tag values in the given Taxonomy with names that begin with the given prefix string. The result is returned in alphabetical Closed taxonomies return tag values that exist in the taxonomy. Also excludes all Tags that the - `object_id` already has. + `object_id` already has. Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes the tags used by `object_id`. @@ -157,16 +160,16 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou # Free-text taxonomy # Obtain the value of the excluded tags - excluded_tags = excluded_tags.values_list('_value', flat=True) + excluded_tags = excluded_tags.values_list("_value", flat=True) return ( # Fetch object tags from this taxonomy whose value starts with the given prefix ObjectTag.objects.filter(taxonomy=taxonomy, _value__istartswith=prefix) # omit any free-text tags whose values match the tags on the given object .exclude(_value__in=excluded_tags) # alphabetical ordering - .order_by('_value') + .order_by("_value") # obtain the values of the tags - .values_list('_value', flat=True) + .values_list("_value", flat=True) # remove repeats .distinct() # get only first `count` values @@ -174,24 +177,28 @@ def autocomplete_tags(taxonomy: Taxonomy, prefix: str, object_id: str= None, cou ) else: # Closed taxonomy - + # Obtain the id of the excluded tags - excluded_tags = excluded_tags.filter(tag__isnull=False).values_list('tag__id', flat=True) + excluded_tags = excluded_tags.filter(tag__isnull=False).values_list( + "tag__id", flat=True + ) return ( # Fetch tags from this taxonomy whose value starts with the given prefix Tag.objects.filter(taxonomy=taxonomy, value__istartswith=prefix) # omit any tags applied to the given object .exclude(id__in=excluded_tags) # alphabetical ordering - .order_by('value') + .order_by("value") # obtain only id and values of the tags - .values('id', 'value') + .values("id", "value") # get only first `count` values [:count] ) -def autocomplete_tags_result(taxonomy: Taxonomy, prefix: str, object_id: str= None, count=10) -> List[TagResult]: +def autocomplete_tags_result( + taxonomy: Taxonomy, prefix: str, object_id: str = None, count=10 +) -> List[TagResult]: """ Calls `autocomplete_tags` and serialize the results into `TagResult` """ @@ -199,5 +206,5 @@ def autocomplete_tags_result(taxonomy: Taxonomy, prefix: str, object_id: str= No if taxonomy.allow_free_text: result = [TagResult(id=None, value=value) for value in result] else: - result = [TagResult(id=item['id'], value=item['value']) for item in result] + result = [TagResult(id=item["id"], value=item["value"]) for item in result] return result diff --git a/openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py b/openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py deleted file mode 100644 index 1f35efb2b..000000000 --- a/openedx_tagging/core/tagging/migrations/0002_auto_20230703_1548.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.19 on 2023-07-03 20:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('oel_tagging', '0001_initial'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='tag', - unique_together={('taxonomy', 'value')}, - ), - migrations.AddIndex( - model_name='objecttag', - index=models.Index(fields=['taxonomy', 'object_id'], name='oel_tagging_taxonom_aa24e6_idx'), - ), - ] diff --git a/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py b/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py new file mode 100644 index 000000000..e090adc25 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.19 on 2023-07-25 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("oel_tagging", "0002_auto_20230718_2026"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="objecttag", + unique_together={("taxonomy", "_value", "object_id")}, + ), + migrations.AlterUniqueTogether( + name="tag", + unique_together={("taxonomy", "value")}, + ), + migrations.AddIndex( + model_name="objecttag", + index=models.Index( + fields=["taxonomy", "object_id"], name="oel_tagging_taxonom_aa24e6_idx" + ), + ), + ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index c94a8036b..d0e9dfdec 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -498,6 +498,7 @@ class Meta: models.Index(fields=["taxonomy", "_value"]), models.Index(fields=["taxonomy", "object_id"]), ] + unique_together = ("taxonomy", "_value", "object_id") def __repr__(self): """ @@ -669,7 +670,7 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._value = object_tag._value self._name = object_tag._name return self - + class TagResult: """ diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index c45fcaeaa..ee27d41e4 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -486,10 +486,9 @@ def test_autocomplete_tags_free_text_taxonomy(self): 'Animalia', 'Arthropoda', ] - for tag in tags: + for index, tag in enumerate(tags): ObjectTag( - object_id=object_id_1, - object_type='course', + object_id=f"{object_id_1}_{index}", taxonomy=taxonomy, _value=tag, ).save() @@ -511,26 +510,26 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal result = tagging_api.autocomplete_tags_result(taxonomy, prefix) tag_ids = self._get_tag_result_ids(result) tag_values = self._get_tag_result_values(result) - self.assertEqual(tag_values, ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"]) + assert tag_values == ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"] for tag in tag_values: - self.assertEqual(tag[0].lower(), prefix) + assert tag[0].lower() == prefix if free_text: - self.assertEqual(tag_ids, [None, None, None, None]) + assert tag_ids == [None, None, None, None] else: - self.assertEqual(tag_ids, [9,2,5,14]) + assert tag_ids == [9,2,5,14] # Search with count result = tagging_api.autocomplete_tags_result(taxonomy, prefix, count=2) tag_ids = self._get_tag_result_ids(result) tag_values = self._get_tag_result_values(result) - self.assertEqual(tag_values, ["Animalia", "Archaea"]) + assert tag_values == ["Animalia", "Archaea"] if free_text: - self.assertEqual(tag_ids, [None, None]) + assert tag_ids == [None, None] else: - self.assertEqual(tag_ids, [9,2]) + assert tag_ids == [9,2] # Create ObjectTag to simulate the content tagging for tag in tag_values: @@ -540,7 +539,6 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal ObjectTag( object_id=object_id, - object_type='course', taxonomy=taxonomy, tag=tag_model, _value=tag, @@ -549,19 +547,19 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal # Search with object result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id) tag_ids = self._get_tag_result_ids(result) - self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria", "Arthropoda"]) + assert self._get_tag_result_values(result) == ["Archaebacteria", "Arthropoda"] if free_text: - self.assertEqual(tag_ids, [None, None]) + assert tag_ids == [None, None] else: - self.assertEqual(tag_ids, [5,14]) + assert tag_ids == [5,14] - # Search with object and coun + # Search with object and count result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id, count=1) tag_ids = self._get_tag_result_ids(result) - self.assertEqual(self._get_tag_result_values(result), ["Archaebacteria"]) + assert self._get_tag_result_values(result) == ["Archaebacteria"] if free_text: - self.assertEqual(tag_ids, [None]) + assert tag_ids == [None] else: - self.assertEqual(tag_ids, [5]) + assert tag_ids == [5] From 3ac4cbc15fe20ff70860d1499f5d1bef0a477d19 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 25 Jul 2023 13:41:48 -0500 Subject: [PATCH 10/14] feat: function moved to Taxonomy model --- openedx_tagging/core/tagging/api.py | 64 ++---------------- openedx_tagging/core/tagging/models/base.py | 66 ++++++++++++++++--- .../openedx_tagging/core/tagging/test_api.py | 57 ++++++++++------ 3 files changed, 98 insertions(+), 89 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 686317d7d..0e70ef0ed 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -15,7 +15,7 @@ from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ -from .models import ObjectTag, Tag, Taxonomy, TagResult +from .models import ObjectTag, Tag, Taxonomy def create_taxonomy( @@ -140,7 +140,10 @@ def tag_object( def autocomplete_tags( - taxonomy: Taxonomy, prefix: str, object_id: str = None, count=10 + taxonomy: Taxonomy, + prefix: str, + object_id: str = None, + count=10, ) -> QuerySet: """ Returns the first `count` tag values in the given Taxonomy with names @@ -152,59 +155,4 @@ def autocomplete_tags( Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes the tags used by `object_id`. """ - result = [] - - # Fetch tags that the object already has to exclude them from the result - excluded_tags = ObjectTag.objects.filter(object_id=object_id, taxonomy=taxonomy) - if taxonomy.allow_free_text: - # Free-text taxonomy - - # Obtain the value of the excluded tags - excluded_tags = excluded_tags.values_list("_value", flat=True) - return ( - # Fetch object tags from this taxonomy whose value starts with the given prefix - ObjectTag.objects.filter(taxonomy=taxonomy, _value__istartswith=prefix) - # omit any free-text tags whose values match the tags on the given object - .exclude(_value__in=excluded_tags) - # alphabetical ordering - .order_by("_value") - # obtain the values of the tags - .values_list("_value", flat=True) - # remove repeats - .distinct() - # get only first `count` values - [:count] - ) - else: - # Closed taxonomy - - # Obtain the id of the excluded tags - excluded_tags = excluded_tags.filter(tag__isnull=False).values_list( - "tag__id", flat=True - ) - return ( - # Fetch tags from this taxonomy whose value starts with the given prefix - Tag.objects.filter(taxonomy=taxonomy, value__istartswith=prefix) - # omit any tags applied to the given object - .exclude(id__in=excluded_tags) - # alphabetical ordering - .order_by("value") - # obtain only id and values of the tags - .values("id", "value") - # get only first `count` values - [:count] - ) - - -def autocomplete_tags_result( - taxonomy: Taxonomy, prefix: str, object_id: str = None, count=10 -) -> List[TagResult]: - """ - Calls `autocomplete_tags` and serialize the results into `TagResult` - """ - result = autocomplete_tags(taxonomy, prefix, object_id=object_id, count=count) - if taxonomy.allow_free_text: - result = [TagResult(id=None, value=value) for value in result] - else: - result = [TagResult(id=item["id"], value=item["value"]) for item in result] - return result + return taxonomy.cast().autocomplete_tags(prefix, object_id, count) diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index d0e9dfdec..38a007683 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -430,6 +430,62 @@ def tag_object( return updated_tags + def autocomplete_tags( + self, prefix: str, object_id: str = None, count=10 + ) -> models.QuerySet: + """ + Returns the first `count` tag values in the given Taxonomy with names + that begin with the given prefix string. The result is returned in alphabetical + + Closed taxonomies return tag values that exist in the taxonomy. Also excludes all Tags that the + `object_id` already has. + + Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes + the tags used by `object_id`. + + Subclasses can override this method to perform their own autocomplete process. + """ + # Fetch tags that the object already has to exclude them from the result + excluded_tags = self.objecttag_set.filter(object_id=object_id) + if self.allow_free_text: + # Free-text taxonomy + + # Obtain the value of the excluded tags + excluded_tags = excluded_tags.values_list("_value", flat=True) + return ( + # Fetch object tags from this taxonomy whose value starts with the given prefix + self.objecttag_set.filter(_value__istartswith=prefix) + # omit any free-text tags whose values match the tags on the given object + .exclude(_value__in=excluded_tags) + # alphabetical ordering + .order_by("_value") + # obtain the values of the tags + .values_list("_value", flat=True) + # remove repeats + .distinct() + # get only first `count` values + [:count] + ) + else: + # Closed taxonomy + + # Obtain the id of the excluded tags + excluded_tags = excluded_tags.filter(tag__isnull=False).values_list( + "tag__id", flat=True + ) + return ( + # Fetch tags from this taxonomy whose value starts with the given prefix + self.tag_set.filter(value__istartswith=prefix) + # omit any tags applied to the given object + .exclude(id__in=excluded_tags) + # alphabetical ordering + .order_by("value") + # obtain only id and values of the tags + .values("id", "value") + # get only first `count` values + [:count] + ) + class ObjectTag(models.Model): """ @@ -670,13 +726,3 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._value = object_tag._value self._name = object_tag._name return self - - -class TagResult: - """ - Lightweight class used to return tags on some API methods - """ - - def __init__(self, id: Union[str, None], value: str): - self.id = id - self.value = value diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index ee27d41e4..f376bcc21 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -495,11 +495,26 @@ def test_autocomplete_tags_free_text_taxonomy(self): self._validate_autocomplete_tags(taxonomy, prefix, object_id_2, free_text=True) - def _get_tag_result_values(self, tag_results): - return [tag_result.value for tag_result in tag_results] - - def _get_tag_result_ids(self, tag_results): - return [tag_result.id for tag_result in tag_results] + def _get_tag_values(self, tags): + """ + Get tag values from tagging_api.autocomplete_tags() result + """ + result = [] + for tag in tags: + if isinstance(tag, str) : + result.append(tag) + elif tag.get("value"): + result.append(tag.get("value")) + return result + + def _get_tag_ids(self, tags): + """ + Get tag ids from tagging_api.autocomplete_tags() result + """ + return [ + tag.get('id') for tag in tags + if not isinstance(tag, str) and tag.get('id') + ] def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=False): """ @@ -507,27 +522,27 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal """ # Normal search - result = tagging_api.autocomplete_tags_result(taxonomy, prefix) - tag_ids = self._get_tag_result_ids(result) - tag_values = self._get_tag_result_values(result) + result = tagging_api.autocomplete_tags(taxonomy, prefix) + tag_ids = self._get_tag_ids(result) + tag_values = self._get_tag_values(result) assert tag_values == ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"] for tag in tag_values: assert tag[0].lower() == prefix if free_text: - assert tag_ids == [None, None, None, None] + assert tag_ids == [] else: assert tag_ids == [9,2,5,14] # Search with count - result = tagging_api.autocomplete_tags_result(taxonomy, prefix, count=2) - tag_ids = self._get_tag_result_ids(result) - tag_values = self._get_tag_result_values(result) + result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) + tag_ids = self._get_tag_ids(result) + tag_values = self._get_tag_values(result) assert tag_values == ["Animalia", "Archaea"] if free_text: - assert tag_ids == [None, None] + assert tag_ids == [] else: assert tag_ids == [9,2] @@ -545,21 +560,21 @@ def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=Fal ).save() # Search with object - result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id) - tag_ids = self._get_tag_result_ids(result) - assert self._get_tag_result_values(result) == ["Archaebacteria", "Arthropoda"] + result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) + tag_ids = self._get_tag_ids(result) + assert self._get_tag_values(result) == ["Archaebacteria", "Arthropoda"] if free_text: - assert tag_ids == [None, None] + assert tag_ids == [] else: assert tag_ids == [5,14] # Search with object and count - result = tagging_api.autocomplete_tags_result(taxonomy, prefix, object_id, count=1) - tag_ids = self._get_tag_result_ids(result) - assert self._get_tag_result_values(result) == ["Archaebacteria"] + result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) + tag_ids = self._get_tag_ids(result) + assert self._get_tag_values(result) == ["Archaebacteria"] if free_text: - assert tag_ids == [None] + assert tag_ids == [] else: assert tag_ids == [5] From 61d9a7d90ee9bc54ec2bff72c201aa2509c18c26 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 26 Jul 2023 13:43:49 -0500 Subject: [PATCH 11/14] refactor: autocomplete function - Match text anywhere in the value - Only autocomplete from existing ObjectTags, ignoring the free-text vs closed taxonomy distinction. - Remove the count parameter - Refactoring tests to use ddt --- openedx_tagging/core/tagging/api.py | 15 +- openedx_tagging/core/tagging/models/base.py | 68 ++----- .../openedx_tagging/core/tagging/test_api.py | 179 +++++++++--------- 3 files changed, 118 insertions(+), 144 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 0e70ef0ed..70c69579a 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -141,18 +141,15 @@ def tag_object( def autocomplete_tags( taxonomy: Taxonomy, - prefix: str, + search: str, object_id: str = None, - count=10, ) -> QuerySet: """ - Returns the first `count` tag values in the given Taxonomy with names - that begin with the given prefix string. The result is returned in alphabetical + Returns tag values that contains the given search string. + The result is returned in alphabetical order. - Closed taxonomies return tag values that exist in the taxonomy. Also excludes all Tags that the - `object_id` already has. + The output is a QuerySet of dictionaries in with `_value` and `tag`. - Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes - the tags used by `object_id`. + Subclasses can override this method to perform their own autocomplete process. """ - return taxonomy.cast().autocomplete_tags(prefix, object_id, count) + return taxonomy.cast().autocomplete_tags(search, object_id) diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 38a007683..c0f225134 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -430,61 +430,31 @@ def tag_object( return updated_tags - def autocomplete_tags( - self, prefix: str, object_id: str = None, count=10 - ) -> models.QuerySet: + def autocomplete_tags(self, search: str, object_id: str = None) -> models.QuerySet: """ - Returns the first `count` tag values in the given Taxonomy with names - that begin with the given prefix string. The result is returned in alphabetical + Returns tag values that contains the given search string. + The result is returned in alphabetical order. - Closed taxonomies return tag values that exist in the taxonomy. Also excludes all Tags that the - `object_id` already has. - - Free-text taxonomies return only tag values that are currently in use on (another) object. Also excludes - the tags used by `object_id`. + The output is a QuerySet of dictionaries in with `_value` and `tag`. Subclasses can override this method to perform their own autocomplete process. """ # Fetch tags that the object already has to exclude them from the result - excluded_tags = self.objecttag_set.filter(object_id=object_id) - if self.allow_free_text: - # Free-text taxonomy - - # Obtain the value of the excluded tags - excluded_tags = excluded_tags.values_list("_value", flat=True) - return ( - # Fetch object tags from this taxonomy whose value starts with the given prefix - self.objecttag_set.filter(_value__istartswith=prefix) - # omit any free-text tags whose values match the tags on the given object - .exclude(_value__in=excluded_tags) - # alphabetical ordering - .order_by("_value") - # obtain the values of the tags - .values_list("_value", flat=True) - # remove repeats - .distinct() - # get only first `count` values - [:count] - ) - else: - # Closed taxonomy - - # Obtain the id of the excluded tags - excluded_tags = excluded_tags.filter(tag__isnull=False).values_list( - "tag__id", flat=True - ) - return ( - # Fetch tags from this taxonomy whose value starts with the given prefix - self.tag_set.filter(value__istartswith=prefix) - # omit any tags applied to the given object - .exclude(id__in=excluded_tags) - # alphabetical ordering - .order_by("value") - # obtain only id and values of the tags - .values("id", "value") - # get only first `count` values - [:count] - ) + excluded_tags = self.objecttag_set.filter(object_id=object_id).values_list( + "_value", flat=True + ) + return ( + # Fetch object tags from this taxonomy whose value contains the search + self.objecttag_set.filter(_value__icontains=search) + # omit any tags whose values match the tags on the given object + .exclude(_value__in=excluded_tags) + # alphabetical ordering + .order_by("_value") + # obtain tag values + .values("_value", "tag") + # remove repeats + .distinct() + ) class ObjectTag(models.Model): diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index f376bcc21..abd3061a6 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -1,4 +1,5 @@ """ Test the tagging APIs """ +import ddt from django.test.testcases import TestCase, override_settings @@ -16,6 +17,7 @@ ] +@ddt.ddt class TestApiTagging(TestTagTaxonomyMixin, TestCase): """ Test the Tagging API methods. @@ -461,18 +463,27 @@ def test_get_object_tags(self): beta, ] - def test_autocomplete_tags_closed_taxonomy(self): - object_id = 'course_id_1' - prefix = 'a' - - self._validate_autocomplete_tags(self.taxonomy, prefix, object_id) - - def test_autocomplete_tags_free_text_taxonomy(self): - object_id_1 = 'course_id_1' - object_id_2 = 'course_id_2' - prefix = 'a' - - taxonomy = tagging_api.create_taxonomy("Free_Text_Taxonomy", allow_free_text=True) + @ddt.data( + ("ChA", ["Archaea", "Archaebacteria"], [2,5]), + ("ar", ['Archaea', 'Archaebacteria', 'Arthropoda'], [2,5,14]), + ("aE", ['Archaea', 'Archaebacteria', 'Plantae'], [2,5,10]), + ( + "a", + [ + 'Animalia', + 'Archaea', + 'Archaebacteria', + 'Arthropoda', + 'Gastrotrich', + 'Monera', + 'Placozoa', + 'Plantae', + ], + [9,2,5,14,16,13,19,10], + ), + ) + @ddt.unpack + def test_autocomplete_tags(self, search, expected_values, expected_ids): tags = [ 'Archaea', 'Archaebacteria', @@ -482,99 +493,95 @@ def test_autocomplete_tags_free_text_taxonomy(self): 'Monera', 'Gastrotrich', 'Placozoa', - # For testing repeats - 'Animalia', - 'Arthropoda', - ] - for index, tag in enumerate(tags): + ] + expected_values # To create repeats + open_taxonomy = self.taxonomy + closed_taxonomy = tagging_api.create_taxonomy( + "Free_Text_Taxonomy", + allow_free_text=True, + ) + + for index, value in enumerate(tags): + # Creating ObjectTags for open taxonomy ObjectTag( - object_id=f"{object_id_1}_{index}", - taxonomy=taxonomy, - _value=tag, + object_id=f"object_id_{index}", + taxonomy=open_taxonomy, + _value=value, + ).save() + + # Creating ObjectTags for closed taxonomy + tag = get_tag(value) + ObjectTag( + object_id=f"object_id_{index}", + taxonomy=closed_taxonomy, + tag=tag, + _value=value, ).save() - self._validate_autocomplete_tags(taxonomy, prefix, object_id_2, free_text=True) + # Test for open taxonomy + self._validate_autocomplete_tags( + open_taxonomy, + search, + expected_values, + [None] * len(expected_ids), + ) + + # Test for closed taxonomy + self._validate_autocomplete_tags( + closed_taxonomy, + search, + expected_values, + expected_ids, + ) + def _get_tag_values(self, tags): """ Get tag values from tagging_api.autocomplete_tags() result - """ - result = [] - for tag in tags: - if isinstance(tag, str) : - result.append(tag) - elif tag.get("value"): - result.append(tag.get("value")) - return result + """ + return [tag.get("_value") for tag in tags] def _get_tag_ids(self, tags): """ Get tag ids from tagging_api.autocomplete_tags() result """ - return [ - tag.get('id') for tag in tags - if not isinstance(tag, str) and tag.get('id') - ] - - def _validate_autocomplete_tags(self, taxonomy, prefix, object_id, free_text=False): + return [tag.get("tag") for tag in tags] + + + def _validate_autocomplete_tags( + self, + taxonomy, + search, + expected_values, + expected_ids, + ): """ Validate autocomplete tags """ # Normal search - result = tagging_api.autocomplete_tags(taxonomy, prefix) - tag_ids = self._get_tag_ids(result) + result = tagging_api.autocomplete_tags(taxonomy, search) tag_values = self._get_tag_values(result) - assert tag_values == ["Animalia", "Archaea", "Archaebacteria", "Arthropoda"] - - for tag in tag_values: - assert tag[0].lower() == prefix - - if free_text: - assert tag_ids == [] - else: - assert tag_ids == [9,2,5,14] - - # Search with count - result = tagging_api.autocomplete_tags(taxonomy, prefix, count=2) - tag_ids = self._get_tag_ids(result) - tag_values = self._get_tag_values(result) - assert tag_values == ["Animalia", "Archaea"] - - if free_text: - assert tag_ids == [] - else: - assert tag_ids == [9,2] + for value in tag_values: + assert search.lower() in value.lower() + print(tag_values) + assert tag_values == expected_values + assert self._get_tag_ids(result) == expected_ids + # Create ObjectTag to simulate the content tagging - for tag in tag_values: - tag_model = None - if not free_text: - tag_model = get_tag(tag) - - ObjectTag( - object_id=object_id, - taxonomy=taxonomy, - tag=tag_model, - _value=tag, - ).save() + tag_model = None + if not taxonomy.allow_free_text: + tag_model = get_tag(tag_values[0]) + + object_id = 'new_object_id' + ObjectTag( + object_id=object_id, + taxonomy=taxonomy, + tag=tag_model, + _value=tag_values[0], + ).save() # Search with object - result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id) - tag_ids = self._get_tag_ids(result) - assert self._get_tag_values(result) == ["Archaebacteria", "Arthropoda"] - - if free_text: - assert tag_ids == [] - else: - assert tag_ids == [5,14] - - # Search with object and count - result = tagging_api.autocomplete_tags(taxonomy, prefix, object_id, count=1) - tag_ids = self._get_tag_ids(result) - assert self._get_tag_values(result) == ["Archaebacteria"] - - if free_text: - assert tag_ids == [] - else: - assert tag_ids == [5] + result = tagging_api.autocomplete_tags(taxonomy, search, object_id) + assert self._get_tag_values(result) == expected_values[1:] + assert self._get_tag_ids(result) == expected_ids[1:] From 71a14280d359f036979176f8b247c50dec6baa24 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 27 Jul 2023 14:59:36 -0500 Subject: [PATCH 12/14] style: nits, lint and docs --- openedx_tagging/core/tagging/api.py | 30 +++++++++++++++---- openedx_tagging/core/tagging/models/base.py | 30 ++++++++++++++----- .../openedx_tagging/core/tagging/test_api.py | 15 ++++------ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 70c69579a..5c66dd531 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -145,11 +145,29 @@ def autocomplete_tags( object_id: str = None, ) -> QuerySet: """ - Returns tag values that contains the given search string. - The result is returned in alphabetical order. - - The output is a QuerySet of dictionaries in with `_value` and `tag`. - - Subclasses can override this method to perform their own autocomplete process. + Provides auto-complete suggestions by matching the `search` string against existing + ObjectTags linked to the given taxonomy. A case-insensitive search is used in order + to return the highest number of relevant tags. + + If `object_id` is provided, then object tag values already linked to this object + are omitted from the returned suggestions. (ObjectTag values must be unique for a + given object + taxonomy, and so omitting these suggestions helps users avoid + duplication errors.). + + Returns a QuerySet of dictionaries containing distinct `value` (string) and + `tag` (numeric ID) values, sorted alphabetically by `value`. + The `value` is what should be shown as a suggestion to users, + and if it's a free-text taxonomy, `tag` will be `None`: we include the `tag` ID + in anticipation of the second use case listed below. + + Use cases: + * This method is useful for reducing tag variation in free-text taxonomies by showing + users tags that are similar to what they're typing. E.g., if the `search` string "dn" + shows that other objects have been tagged with "DNA", "DNA electrophoresis", and "DNA fingerprinting", + this encourages users to use those existing tags if relevant, instead of creating new ones that + look similar (e.g. "dna finger-printing"). + * It could also be used to assist tagging for closed taxonomies with a list of possible tags which is too + large to return all at once, e.g. a user model taxonomy that dynamically creates tags on request for any + registered user in the database. (Note that this is not implemented yet, but may be as part of a future change.) """ return taxonomy.cast().autocomplete_tags(search, object_id) diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index c0f225134..da1aeff32 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -432,17 +432,31 @@ def tag_object( def autocomplete_tags(self, search: str, object_id: str = None) -> models.QuerySet: """ - Returns tag values that contains the given search string. - The result is returned in alphabetical order. + Provides auto-complete suggestions by matching the `search` string against existing + ObjectTags linked to the given taxonomy. A case-insensitive search is used in order + to return the highest number of relevant tags. - The output is a QuerySet of dictionaries in with `_value` and `tag`. + If `object_id` is provided, then object tag values already linked to this object + are omitted from the returned suggestions. (ObjectTag values must be unique for a + given object + taxonomy, and so omitting these suggestions helps users avoid + duplication errors.). + + Returns a QuerySet of dictionaries containing distinct `value` (string) and `tag` + (numeric ID) values, sorted alphabetically by `value`. Subclasses can override this method to perform their own autocomplete process. + Subclass use cases: + * Large taxonomy associated with a model. It can be overridden to get + the suggestions directly from the model by doing own filtering. + * Taxonomy with a list of available tags: It can be overridden to only + search the suggestions on a list of available tags. """ # Fetch tags that the object already has to exclude them from the result - excluded_tags = self.objecttag_set.filter(object_id=object_id).values_list( - "_value", flat=True - ) + excluded_tags = [] + if object_id: + excluded_tags = self.objecttag_set.filter(object_id=object_id).values_list( + "_value", flat=True + ) return ( # Fetch object tags from this taxonomy whose value contains the search self.objecttag_set.filter(_value__icontains=search) @@ -450,8 +464,10 @@ def autocomplete_tags(self, search: str, object_id: str = None) -> models.QueryS .exclude(_value__in=excluded_tags) # alphabetical ordering .order_by("_value") + # Alias the `_value` field to `value` to make it nicer for users + .annotate(value=models.F("_value")) # obtain tag values - .values("_value", "tag") + .values("value", "tag") # remove repeats .distinct() ) diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index abd3061a6..d4ab8d386 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -494,8 +494,8 @@ def test_autocomplete_tags(self, search, expected_values, expected_ids): 'Gastrotrich', 'Placozoa', ] + expected_values # To create repeats - open_taxonomy = self.taxonomy - closed_taxonomy = tagging_api.create_taxonomy( + closed_taxonomy = self.taxonomy + open_taxonomy = tagging_api.create_taxonomy( "Free_Text_Taxonomy", allow_free_text=True, ) @@ -507,7 +507,7 @@ def test_autocomplete_tags(self, search, expected_values, expected_ids): taxonomy=open_taxonomy, _value=value, ).save() - + # Creating ObjectTags for closed taxonomy tag = get_tag(value) ObjectTag( @@ -533,19 +533,17 @@ def test_autocomplete_tags(self, search, expected_values, expected_ids): expected_ids, ) - def _get_tag_values(self, tags): """ Get tag values from tagging_api.autocomplete_tags() result - """ - return [tag.get("_value") for tag in tags] + """ + return [tag.get("value") for tag in tags] def _get_tag_ids(self, tags): """ Get tag ids from tagging_api.autocomplete_tags() result """ return [tag.get("tag") for tag in tags] - def _validate_autocomplete_tags( self, @@ -564,10 +562,9 @@ def _validate_autocomplete_tags( for value in tag_values: assert search.lower() in value.lower() - print(tag_values) assert tag_values == expected_values assert self._get_tag_ids(result) == expected_ids - + # Create ObjectTag to simulate the content tagging tag_model = None if not taxonomy.allow_free_text: From f0fef57e9a3f9acf0821879aa0b3c1e4faa3e41a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 1 Aug 2023 12:14:05 -0500 Subject: [PATCH 13/14] style: Nits --- openedx_tagging/core/tagging/api.py | 9 +++++++++ openedx_tagging/core/tagging/models/base.py | 8 ++++++-- tests/openedx_tagging/core/tagging/test_api.py | 6 +++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 5c66dd531..55aa0d376 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -143,6 +143,7 @@ def autocomplete_tags( taxonomy: Taxonomy, search: str, object_id: str = None, + object_tags_only=True, ) -> QuerySet: """ Provides auto-complete suggestions by matching the `search` string against existing @@ -170,4 +171,12 @@ def autocomplete_tags( large to return all at once, e.g. a user model taxonomy that dynamically creates tags on request for any registered user in the database. (Note that this is not implemented yet, but may be as part of a future change.) """ + if not object_tags_only: + raise NotImplementedError( + _( + "Using this would return a query set of tags instead of object tags." + "For now we recommend fetching all of the taxonomy's tags " + "using get_tags() and filtering them on the frontend." + ) + ) return taxonomy.cast().autocomplete_tags(search, object_id) diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index da1aeff32..9c36454b7 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -430,7 +430,11 @@ def tag_object( return updated_tags - def autocomplete_tags(self, search: str, object_id: str = None) -> models.QuerySet: + def autocomplete_tags( + self, + search: str, + object_id: str = None, + ) -> models.QuerySet: """ Provides auto-complete suggestions by matching the `search` string against existing ObjectTags linked to the given taxonomy. A case-insensitive search is used in order @@ -467,7 +471,7 @@ def autocomplete_tags(self, search: str, object_id: str = None) -> models.QueryS # Alias the `_value` field to `value` to make it nicer for users .annotate(value=models.F("_value")) # obtain tag values - .values("value", "tag") + .values("value", "tag_id") # remove repeats .distinct() ) diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index d4ab8d386..b6affec4a 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -533,6 +533,10 @@ def test_autocomplete_tags(self, search, expected_values, expected_ids): expected_ids, ) + def test_autocompleate_not_implemented(self): + with self.assertRaises(NotImplementedError): + tagging_api.autocomplete_tags(self.taxonomy, 'test', None, object_tags_only=False) + def _get_tag_values(self, tags): """ Get tag values from tagging_api.autocomplete_tags() result @@ -543,7 +547,7 @@ def _get_tag_ids(self, tags): """ Get tag ids from tagging_api.autocomplete_tags() result """ - return [tag.get("tag") for tag in tags] + return [tag.get("tag_id") for tag in tags] def _validate_autocomplete_tags( self, From ea01201ce7483392faa85427e71c323a9ad27624 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 2 Aug 2023 11:35:25 -0500 Subject: [PATCH 14/14] chore: Rebase with main and bump version --- openedx_learning/__init__.py | 2 +- .../migrations/0003_auto_20230725_1257.py | 26 ------------------- .../0006_alter_objecttag_unique_together.py | 16 ++++++++++++ openedx_tagging/core/tagging/models/base.py | 1 - .../tagging/test_system_defined_models.py | 8 +++--- 5 files changed, 20 insertions(+), 33 deletions(-) delete mode 100644 openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py create mode 100644 openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 485f44ac2..b3f475621 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py b/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py deleted file mode 100644 index e090adc25..000000000 --- a/openedx_tagging/core/tagging/migrations/0003_auto_20230725_1257.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 3.2.19 on 2023-07-25 17:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("oel_tagging", "0002_auto_20230718_2026"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="objecttag", - unique_together={("taxonomy", "_value", "object_id")}, - ), - migrations.AlterUniqueTogether( - name="tag", - unique_together={("taxonomy", "value")}, - ), - migrations.AddIndex( - model_name="objecttag", - index=models.Index( - fields=["taxonomy", "object_id"], name="oel_tagging_taxonom_aa24e6_idx" - ), - ), - ] diff --git a/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py b/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py new file mode 100644 index 000000000..0ffe5f7c6 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.19 on 2023-08-02 16:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("oel_tagging", "0005_language_taxonomy"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="objecttag", + unique_together={("taxonomy", "_value", "object_id")}, + ), + ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 9c36454b7..efdbda55a 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -542,7 +542,6 @@ class Meta: indexes = [ models.Index(fields=["taxonomy", "object_id"]), models.Index(fields=["taxonomy", "_value"]), - models.Index(fields=["taxonomy", "object_id"]), ] unique_together = ("taxonomy", "_value", "object_id") diff --git a/tests/openedx_tagging/core/tagging/test_system_defined_models.py b/tests/openedx_tagging/core/tagging/test_system_defined_models.py index 432d07110..783a2604e 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -1,6 +1,7 @@ """ Test the tagging system-defined taxonomy models """ import ddt +from django.db.utils import IntegrityError from django.test.testcases import TestCase, override_settings from django.contrib.auth import get_user_model @@ -160,11 +161,8 @@ def test_tag_object_existing_tag(self): # Test add an existing Tag self._tag_object() assert self.user_taxonomy.tag_set.count() == 1 - updated_tags = self._tag_object() - assert self.user_taxonomy.tag_set.count() == 1 - assert len(updated_tags) == 1 - assert updated_tags[0].tag.external_id == str(self.user_1.id) - assert updated_tags[0].tag.value == self.user_1.get_username() + with self.assertRaises(IntegrityError): + self._tag_object() def test_tag_object_resync(self): self._tag_object()