From 8d84cd27ef2494b53ea0820aff737d76cb694fbf Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 19 Jul 2023 18:29:32 -0500 Subject: [PATCH] feat: System defined taxonomies subclasses --- .../tagging/migrations/0003_system_defined.py | 57 ++++ openedx_tagging/core/tagging/models.py | 294 +++++++++++++++--- .../system_defined_taxonomies/object_tags.py | 210 ------------- .../core/fixtures/system_defined.yaml | 10 - .../core/fixtures/tagging.yaml | 22 +- .../openedx_tagging/core/tagging/test_api.py | 9 +- .../core/tagging/test_models.py | 263 +++++++++++++++- .../core/tagging/test_system_defined.py | 236 -------------- 8 files changed, 584 insertions(+), 517 deletions(-) create mode 100644 openedx_tagging/core/tagging/migrations/0003_system_defined.py delete mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py delete mode 100644 tests/openedx_tagging/core/fixtures/system_defined.yaml delete mode 100644 tests/openedx_tagging/core/tagging/test_system_defined.py diff --git a/openedx_tagging/core/tagging/migrations/0003_system_defined.py b/openedx_tagging/core/tagging/migrations/0003_system_defined.py new file mode 100644 index 00000000..d4c09425 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0003_system_defined.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.19 on 2023-07-19 20:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0002_auto_20230718_2026'), + ] + + operations = [ + migrations.CreateModel( + name='SystemDefinedTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.taxonomy',), + ), + migrations.CreateModel( + name='LanguageTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.systemdefinedtaxonomy',), + ), + migrations.CreateModel( + name='ModelSystemDefinedTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.systemdefinedtaxonomy',), + ), + migrations.CreateModel( + name='UserSystemDefinedTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.modelsystemdefinedtaxonomy',), + ), + ] diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index 3bd42bfa..eccf40f2 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -1,8 +1,9 @@ """ Tagging app data models """ import logging -from typing import List, Type, Union -from enum import Enum +from typing import Any, List, Type, Union +from django.conf import settings +from django.contrib.auth import get_user_model from django.db import models from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ @@ -244,6 +245,12 @@ def copy(self, taxonomy: "Taxonomy") -> "Taxonomy": self.visible_to_authors = taxonomy.visible_to_authors self._taxonomy_class = taxonomy._taxonomy_class return self + + def _get_tags_query_set(self) -> models.QuerySet: + """ + Base query set for used on `get_tags()` + """ + return self.tag_set def get_tags(self) -> List[Tag]: """ @@ -259,7 +266,7 @@ def get_tags(self) -> List[Tag]: parents = None for depth in range(TAXONOMY_MAX_DEPTH): - filtered_tags = self.tag_set.prefetch_related("parent") + filtered_tags = self._get_tags_query_set().prefetch_related("parent") if parents is None: filtered_tags = filtered_tags.filter(parent=None) else: @@ -346,6 +353,28 @@ def _check_object( """ return bool(object_tag.object_id) + def _set_tag( + self, + tag_ref, + object_tag: "ObjectTag", + ) -> "ObjectTag": + """ + Set a tag on `object_tag` and run a resync + + Subclasses can override this method to perform their own set tag process. + """ + try: + object_tag.tag = self.tag_set.get( + id=tag_ref, + ) + except (ValueError, Tag.DoesNotExist): + # This might be ok, e.g. if self.allow_free_text. + # We'll validate below before saving. + object_tag.value = tag_ref + + object_tag.resync() + return object_tag + def tag_object( self, tags: List, @@ -384,17 +413,9 @@ def tag_object( object_id=object_id, ) - try: - object_tag.tag = self.tag_set.get( - id=tag_ref, - ) - except (ValueError, Tag.DoesNotExist): - # This might be ok, e.g. if self.allow_free_text. - # We'll validate below before saving. - object_tag.value = tag_ref - - object_tag.resync() - if not self.validate_object_tag(object_tag): + object_tag = self._set_tag(tag_ref, object_tag) + + if not object_tag or not self.validate_object_tag(object_tag): raise ValueError( _(f"Invalid object tag for taxonomy ({self.id}): {tag_ref}") ) @@ -629,66 +650,239 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._name = object_tag._name return self - def _check_object(self): - """ - Returns True if this ObjectTag has a valid object. - Subclasses should override this method to perform any additional validation for the particular type of object tag. +class SystemDefinedTaxonomy(Taxonomy): + + class Meta: + proxy = True + + def _check_taxonomy( + self, + object_tag: ObjectTag + ) -> bool: + """ + Returns True if this is a system defined taxonomy """ - # Must have a valid object id/type: - return self.object_id and self.object_type + return ( + super()._check_taxonomy(object_tag) + and self.system_defined + ) -class ClosedObjectTag(OpenObjectTag): +class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): """ - Object tags linked to a closed taxonomy, where the available tag value options are known. + Model based system taxonomy abstract class. + + This type of taxonomies has an associated Django model in `tag_class_model()`. + Are designed to create a Tag when an ObjectTags with a new Tag. + The Tags are representations of the instances of the associated model. + + On Tag.external_id stores an identifier from the instance (`pk` as default) + and on Tag.value stores an human readable representation of the instance + (e.g. `username`). + The subclasses can override this behavior, to choose the right field. + + When an ObjectTag is created with an existing Tag, + the Tag is re-synchronized with its instance. """ class Meta: proxy = True - def _check_taxonomy(self): + def __init__( + self, + *args: Any, + **kwargs: Any + ) -> None: + """ + Checks if the `tag_class_model` is correct """ - Returns True if this ObjectTag is linked to a closed taxonomy. + assert issubclass(self.tag_class_model, models.Model) + super().__init__(*args, **kwargs) - Subclasses should override this method to perform any additional validation for the particular type of object tag. + @property + def tag_class_model(self) -> Type: """ - # Must be linked to a closed taxonomy - return self.taxonomy_id and not self.taxonomy.allow_free_text + Subclasses must implement this method to return the Django.model + class referenced by these object tags. + """ + raise NotImplementedError + + def _check_instance( + self, + object_tag: ObjectTag + ) -> bool: + """ + Returns True if the instance exists - def _check_tag(self): + Subclasses can override this method to perform their own instance validation checks. """ - Returns True if this ObjectTag has a valid tag. + intance_id = object_tag.tag.external_id + return self.tag_class_model.objects.filter(pk=intance_id).exists() - Subclasses should override this method to perform any additional validation for the particular type of object tag. + def _check_tag( + self, + object_tag: ObjectTag + ) -> bool: + """ + Returns True if pass the instance validation checks """ - # Closed taxonomies require a Tag - return bool(self.tag_id) + return ( + super()._check_tag(object_tag) + and self._check_instance(object_tag) + ) + + def _get_external_id_from_instance( + self, + instance + ) -> str: + """ + Returns the tag external id from the instance - def is_valid(self, check_taxonomy=True, check_tag=True, check_object=True) -> bool: + Subclasses can override this method to get the external id from their own models. """ - Returns True if this ObjectTag is valid for use with a closed taxonomy. + return str(instance.pk) - Subclasses should override this method to perform any additional validation for the particular type of object tag. + def _get_value_from_instance( + self, + instance + ) -> str: + """ + Returns the tag value from the instance - If `check_taxonomy` is False, then we skip validating the object tag's taxonomy reference. - If `check_tag` is False, then we skip validating the object tag's tag reference. - If `check_object` is False, then we skip validating the object ID/type. + Subclasses can override this method to get the value from their own models. """ - if not super().is_valid( - check_taxonomy=check_taxonomy, - check_tag=check_tag, - check_object=check_object, - ): - return False + return str(instance.pk) + + def _get_instance(self, pk): + """ + Gets the instance from `pk` + """ + try: + return self.tag_class_model.objects.get(pk=pk) + except self.tag_class_model.DoesNotExist: + return None + + def _resync_tag(self, tag: Tag) -> Tag: + """ + Resync the tag value with the value from the instance + """ + instance = self._get_instance(tag.external_id) + if instance: + value = self._get_value_from_instance(instance) + if tag.value != value: + tag.value = value + tag.save() + return tag + + def _set_tag( + self, + tag_ref, + object_tag: ObjectTag + ) -> ObjectTag: + """ + Set or create the respective Tag on `object_tag` - if check_tag and check_taxonomy and (self.tag.taxonomy_id != self.taxonomy_id): - return False + This function is used on `tag_object`. + For this to work it is necessary that `tag_ref` + is always the `pk` of the model to which you want + to associate it. + """ + try: + tag = self.tag_set.get( + external_id=tag_ref, + ) + # Run a resync of the tag + tag = self._resync_tag(tag) + object_tag.tag = tag + except (ValueError, Tag.DoesNotExist): + # Creates a new tag with the instance + instance = self._get_instance(tag_ref) + if not instance: + return None + new_tag = Tag( + taxonomy=self, + value=self._get_value_from_instance(instance), + external_id=self._get_external_id_from_instance(instance), + ) + new_tag.save() + object_tag.tag = new_tag - return True + return object_tag + +class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): + """ + User based system taxonomy class. + """ + + class Meta: + proxy = True + + @property + def tag_class_model(self) -> Type: + """ + Associate the user model + """ + return get_user_model() + + def _get_value_from_instance( + self, + instance + ) -> str: + """ + Returns the username as tag value + """ + return instance.get_username() + + +class LanguageTaxonomy(SystemDefinedTaxonomy): + """ + Language System-defined taxonomy + + The tags are filtered and validated taking into account the + languages available in Django LANGUAGES settings var + """ + class Meta: + proxy = True + + def _get_tags_query_set(self) -> models.QuerySet: + """ + Returns a query set of available languages tags. + """ + available_langs = self._get_available_languages() + return self.tag_set.filter(external_id__in=available_langs) -# Register the ObjectTag subclasses in reverse order of how we want them considered. -register_object_tag_class(OpenObjectTag) -register_object_tag_class(ClosedObjectTag) + def _get_available_languages(cls) -> List[str]: + """ + Get available languages from Django LANGUAGE. + """ + langs = set() + for django_lang in settings.LANGUAGES: + # Split to get the language part + langs.add(django_lang[0].split('-')[0]) + return langs + + def _check_valid_language( + self, + object_tag: ObjectTag + ) -> bool: + """ + Returns True if the tag is on the available languages + """ + available_langs = self._get_available_languages() + return ( + object_tag.tag.external_id in available_langs + ) + def _check_tag( + self, + object_tag: ObjectTag + ) -> bool: + """ + Returns True if the tag is on the available languages + """ + return ( + super()._check_tag(object_tag) + and self._check_valid_language(object_tag) + ) diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py deleted file mode 100644 index a54bf05c..00000000 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -ObjectTags for System-defined Taxonomies -""" -from enum import Enum -from typing import List - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.core.exceptions import FieldDoesNotExist -from django.db import models - - -from openedx_tagging.core.tagging.models import ( - Taxonomy, - OpenObjectTag, - ClosedObjectTag, -) -from openedx_tagging.core.tagging.registry import register_object_tag_class - - -class SystemDefinedIds(Enum): - """ - System-defined taxonomy IDs - """ - LanguageTaxonomy = 1 - - -class SystemDefinedObjectTagMixin: - """ - Mixing for ObjectTags used on all system defined taxonomies - - `system_defined_taxonomy_id``is used to connect the - ObjectTag with the system defined taxonomy. - This is because there can be several ObjectTags - for the same Taxonomy, ex: - - - LanguageCourseObjectTag - - LanguageBlockObjectTag - - On the example, there are ObjectTags for the same Language - taxonomy but with different objects. - - Using this approach makes the connection between the ObjectTag - and system defined taxonomy as hardcoded and can't be changed. - """ - - system_defined_taxonomy_id = None - - def _check_system_taxonomy(self, taxonomy: Taxonomy = None): - """ - Validates if the taxonomy is system-defined and match - with the name stored in the object tag - """ - return ( - bool(taxonomy) and - taxonomy.system_defined and - taxonomy.id == self.system_defined_taxonomy_id - ) - - -class OpenSystemObjectTag(OpenObjectTag, SystemDefinedObjectTagMixin): - """ - Free-text object tag used on system-defined taxonomies - """ - - class Meta: - proxy = True - - def _check_taxonomy(self): - return ( - super()._check_taxonomy() and - self._check_system_taxonomy(self.taxonomy) - ) - - -class ClosedSystemObjectTag(ClosedObjectTag, SystemDefinedObjectTagMixin): - """ - Object tags linked to a closed system-taxonomy - """ - - class Meta: - proxy = True - - def _check_taxonomy(self): - return ( - super()._check_taxonomy() and - self._check_system_taxonomy(self.taxonomy) - ) - - -class ModelObjectTag(OpenSystemObjectTag): - """ - Object tags used with tags that relate to the id of a model - - This object tag class is not registered as it needs to have an associated model - """ - - class Meta: - proxy = True - - tag_class_model = None - - - def _check_taxonomy(self): - """ - Validates if has an associated Django model that has an Id - """ - if not super()._check_taxonomy(): - return False - - if not self.tag_class_model: - return False - - # Verify that is a Django model - if not issubclass(self.tag_class_model, models.Model): - return False - - # Verify that the model has 'id' field - try: - self.tag_class_model._meta.get_field('id') - except FieldDoesNotExist: - return False - - return True - - def _check_instance(self): - """ - Validates if the instance exists - """ - try: - intance_id = int(self.value) - except ValueError: - return False - return self.tag_class_model.objects.filter(id=intance_id).exists() - - def _check_tag(self): - """ - Validates if the instance exists - """ - if not super()._check_tag(): - return False - - # Validates if the instance exists - if not self._check_instance(): - return False - - return True - - -class UserObjectTag(ModelObjectTag): - """ - Object tags used on taxonomies associated with user model - """ - - class Meta: - proxy = True - - tag_class_model = get_user_model() - - -class LanguageObjectTag(ClosedSystemObjectTag): - """ - Object tag for Languages - - The tags are filtered and validated taking into account the - languages available in Django LANGUAGES settings var - """ - - system_defined_taxonomy_id = SystemDefinedIds.LanguageTaxonomy.value - - class Meta: - proxy = True - - @classmethod - def get_tags_query_set(cls, taxonomy: Taxonomy) -> models.QuerySet: - """ - Returns a query set of available languages tags. - """ - available_langs = cls._get_available_languages() - return taxonomy.tag_set.filter(external_id__in=available_langs) - - @classmethod - def _get_available_languages(cls) -> List[str]: - """ - Get the available languages from Django LANGUAGE. - """ - langs = set() - for django_lang in settings.LANGUAGES: - # Split to get the language part - langs.add(django_lang[0].split('-')[0]) - return langs - - def _check_tag(self): - """ - Validates if the language tag is on the available languages - """ - if not super()._check_tag(): - return False - - available_langs = self._get_available_languages() - - # Must be linked to a tag and must be an available language - if not self.tag or not self.tag.external_id in available_langs: - return False - - return True - - -# Register the object tag classes in reverse order for how we want them considered -register_object_tag_class(LanguageObjectTag) diff --git a/tests/openedx_tagging/core/fixtures/system_defined.yaml b/tests/openedx_tagging/core/fixtures/system_defined.yaml deleted file mode 100644 index 9fade190..00000000 --- a/tests/openedx_tagging/core/fixtures/system_defined.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- model: oel_tagging.taxonomy - pk: 1 - fields: - name: System Languages - description: Allows tags for any language configured for use on the instance. - enabled: true - required: false - allow_multiple: false - allow_free_text: false - system_defined: true diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index 4174fb4f..90bca397 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -145,6 +145,13 @@ parent: 15 value: Mammalia external_id: null +- model: oel_tagging.tag + pk: 22 + fields: + taxonomy: 4 + parent: null + value: System Tag 1 + external_id: 'tag_1' - model: oel_tagging.taxonomy pk: 1 fields: @@ -165,6 +172,7 @@ allow_multiple: false allow_free_text: false system_defined: true + _taxonomy_class: openedx_tagging.core.tagging.models.LanguageTaxonomy - model: oel_tagging.taxonomy pk: 3 fields: @@ -173,5 +181,17 @@ enabled: true required: false allow_multiple: false - allow_free_text: true + allow_free_text: false + system_defined: true + _taxonomy_class: openedx_tagging.core.tagging.models.UserSystemDefinedTaxonomy +- model: oel_tagging.taxonomy + pk: 4 + fields: + name: System defined taxonomy + description: Generic System defined taxonomy + enabled: true + required: false + allow_multiple: false + allow_free_text: false system_defined: true + _taxonomy_class: openedx_tagging.core.tagging.models.SystemDefinedTaxonomy diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index b7d7ff7e..c6bb4805 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -47,15 +47,17 @@ def test_get_taxonomies(self): tax2 = tagging_api.create_taxonomy("Disabled", enabled=False) with self.assertNumQueries(1): enabled = list(tagging_api.get_taxonomies()) + assert enabled == [ tax1, self.taxonomy, self.system_taxonomy, - self.user_system_taxonomy + self.language_taxonomy, + self.user_taxonomy, ] assert str(enabled[0]) == f" ({tax1.id}) Enabled" assert str(enabled[1]) == " (1) Life on Earth" - assert str(enabled[2]) == " (2) System Languages" + assert str(enabled[2]) == " (4) System defined taxonomy" with self.assertNumQueries(1): disabled = list(tagging_api.get_taxonomies(enabled=False)) @@ -69,7 +71,8 @@ def test_get_taxonomies(self): tax1, self.taxonomy, self.system_taxonomy, - self.user_system_taxonomy + self.language_taxonomy, + self.user_taxonomy, ] def test_get_tags(self): diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 4e78a8f4..4197f9d1 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -1,9 +1,60 @@ """ Test the tagging models """ import ddt -from django.test.testcases import TestCase -from openedx_tagging.core.tagging.models import ObjectTag, Tag, Taxonomy +from django.contrib.auth import get_user_model +from django.test.testcases import TestCase, override_settings + +from openedx_tagging.core.tagging.models import ( + ObjectTag, + Tag, + Taxonomy, + SystemDefinedTaxonomy, + ModelSystemDefinedTaxonomy, + UserSystemDefinedTaxonomy, +) + +test_languages = [ + ('en', 'English'), + ('az', 'Azerbaijani'), + ('id', 'Indonesian'), + ('qu', 'Quechua'), + ('zu', 'Zulu'), +] + + +class EmptyTestClass: + """ + Empty class used for testing + """ + + +class InvalidModelTaxonomy(ModelSystemDefinedTaxonomy): + """ + Model used for testing + """ + @property + def tag_class_model(self): + return EmptyTestClass + + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' + + +class TestModelTaxonomy(ModelSystemDefinedTaxonomy): + """ + Model used for testing + """ + @property + def tag_class_model(self): + return get_user_model() + + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' def get_tag(value): @@ -23,14 +74,26 @@ class TestTagTaxonomyMixin: def setUp(self): super().setUp() self.taxonomy = Taxonomy.objects.get(name="Life on Earth") - self.system_taxonomy = Taxonomy.objects.get(name="System Languages") - self.user_system_taxonomy = Taxonomy.objects.get(name="User Authors") + self.system_taxonomy = Taxonomy.objects.get(name="System defined taxonomy").cast() + self.language_taxonomy = Taxonomy.objects.get(name="System Languages").cast() + self.user_taxonomy = Taxonomy.objects.get(name="User Authors").cast() self.archaea = get_tag("Archaea") self.archaebacteria = get_tag("Archaebacteria") self.bacteria = get_tag("Bacteria") self.eubacteria = get_tag("Eubacteria") self.chordata = get_tag("Chordata") self.mammalia = get_tag("Mammalia") + self.system_taxonomy_tag = get_tag("System Tag 1") + self.user_1 = get_user_model()( + id=1, + username='test_user_1', + ) + self.user_1.save() + self.user_2 = get_user_model()( + id=2, + username='test_user_2', + ) + self.user_2.save() # Domain tags (depth=0) # https://en.wikipedia.org/wiki/Domain_(biology) @@ -127,9 +190,9 @@ def test_representations(self): str(self.taxonomy) == repr(self.taxonomy) == " (1) Life on Earth" ) assert ( - str(self.system_taxonomy) - == repr(self.system_taxonomy) - == " (2) System Languages" + str(self.language_taxonomy) + == repr(self.language_taxonomy) + == " (2) System Languages" ) assert str(self.bacteria) == repr(self.bacteria) == " (1) Bacteria" @@ -403,3 +466,189 @@ def test_tag_object_invalid_tag(self): "biology101", ) assert "Invalid object tag for taxonomy" in str(exc.exception) + + +@ddt.ddt +class TestSystemDefinedTaxonomy(TestTagTaxonomyMixin, TestCase): + """ + Test for System defined Taxonomy + """ + + @ddt.data( + ("system_taxonomy", "system_taxonomy", True), # Valid + ("taxonomy", "taxonomy", False), # Not a system defined + ("system_taxonomy", "taxonomy", False), # Testing parent validations + ) + @ddt.unpack + def test_validations(self, taxonomy, second_taxonomy, expected): + taxonomy = getattr(self, taxonomy) + taxonomy.taxonomy_class = SystemDefinedTaxonomy + taxonomy = taxonomy.cast() + + second_taxonomy = getattr(self, second_taxonomy) + + assert taxonomy.validate_object_tag( + object_tag=ObjectTag( + object_id='id', + taxonomy=second_taxonomy + ), + check_taxonomy=True, + check_tag=False, + check_object=False, + ) == expected + + +@ddt.ddt +class TestModelSystemDefinedTaxonomy(TestTagTaxonomyMixin, TestCase): + """ + Test for Model Model System defined taxonomy + """ + + @ddt.data( + (ModelSystemDefinedTaxonomy, NotImplementedError), + (InvalidModelTaxonomy, AssertionError), + (UserSystemDefinedTaxonomy, None), + ) + @ddt.unpack + def test_implementation_error(self, taxonomy_cls, expected_exception): + if not expected_exception: + assert taxonomy_cls() + else: + with self.assertRaises(expected_exception): + taxonomy_cls() + + @ddt.data( + (1, "tag_id"), # Valid + (0, "tag_id"), # Invalid user + (1, None) # Testing parent validations + ) + @ddt.unpack + def test_validations(self, tag_external_id, tag_id): + tag = Tag( + id=tag_id, + taxonomy=self.user_taxonomy, + value='_val', + external_id=tag_external_id, + ) + object_tag = ObjectTag( + object_id='id', + tag=tag, + ) + + self.user_taxonomy.validate_object_tag( + object_tag=object_tag, + check_object=False, + check_taxonomy=False, + check_tag=True + ) + + def test_tag_object_invalid_user(self): + # Test user that doesn't exist + with self.assertRaises(ValueError): + self.user_taxonomy.tag_object( + tags=[4], + object_id='object_id' + ) + + def _tag_object(self): + return self.user_taxonomy.tag_object( + tags=[self.user_1.id], + object_id='object_id' + ) + + def test_tag_object_tag_creation(self): + # Test creation of a new Tag with user taxonomy + assert self.user_taxonomy.tag_set.count() == 0 + 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() + + # Test parent functions + taxonomy = TestModelTaxonomy( + name='Test', + description='Test', + system_defined=True, + ) + taxonomy.save() + assert taxonomy.tag_set.count() == 0 + updated_tags = taxonomy.tag_object( + tags=[self.user_1.id], + object_id='object_id' + ) + assert taxonomy.tag_set.count() == 1 + assert 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 == str(self.user_1.id) + + + 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() + + def test_tag_object_resync(self): + self._tag_object() + + self.user_1.username = "new_username" + self.user_1.save() + 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() + + def test_tag_object_delete_user(self): + # Test after delete user + self._tag_object() + user_1_id = self.user_1.id + self.user_1.delete() + with self.assertRaises(ValueError): + self.user_taxonomy.tag_object( + tags=[user_1_id], + object_id='object_id', + ) + +@ddt.ddt +@override_settings(LANGUAGES=test_languages) +class TestLanguageTaxonomy(TestTagTaxonomyMixin, TestCase): + """ + Test for Language taxonomy + """ + + @ddt.data( + ('en', 'tag_id'), # Valid + ('es', 'tag_id'), # Not available lang + ('en', None), # Test parent validations + ) + @ddt.unpack + def test_validations(self, lang, tag_id): + tag = Tag( + id=tag_id, + taxonomy=self.language_taxonomy, + value='_val', + external_id=lang, + ) + object_tag = ObjectTag( + object_id='id', + tag=tag, + ) + self.language_taxonomy.validate_object_tag( + object_tag=object_tag, + check_object=False, + check_taxonomy=False, + check_tag=True, + ) + + def test_get_tags(self): + tags = self.language_taxonomy.get_tags() + for tag in tags: + assert tag.external_id in test_languages + assert tag.annotated_field == 0 diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py deleted file mode 100644 index 9de7fb3c..00000000 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ /dev/null @@ -1,236 +0,0 @@ -""" Test the System-defined taxonomies and object tags """ - -import ddt - -from django.contrib.auth import get_user_model -from django.db import models -from django.test.testcases import TestCase, override_settings - -from openedx_tagging.core.tagging.system_defined_taxonomies.object_tags import ( - ModelObjectTag, - UserObjectTag, - LanguageObjectTag, -) -from openedx_tagging.core.tagging.models import Tag, Taxonomy - -from .test_models import TestTagTaxonomyMixin - -test_languages = [ - ('en', 'English'), - ('az', 'Azerbaijani'), - ('id', 'Indonesian'), - ('qu', 'Quechua'), - ('zu', 'Zulu'), -] - - -class EmptyTestClass: - """ - Empty class used for testing - """ - - -class EmptyTestModel(models.Model): - """ - Model used for testing - """ - mi_id = models.AutoField(primary_key=True) - - class Meta: - managed = False - app_label = 'oel_tagging' - - -class EmptyModelObjectTag(ModelObjectTag): - """ - Model ObjectTag used for testing - """ - - system_defined_taxonomy_id = 3 - - class Meta: - proxy = True - managed = False - app_label = 'oel_tagging' - - -class NotDjangoModelObjectTag(ModelObjectTag): - """ - Model ObjectTag used for testing - """ - - system_defined_taxonomy_id = 3 - - class Meta: - proxy = True - managed = False - app_label = 'oel_tagging' - - tag_class_model = EmptyTestClass - - -class NotIdModelObjectTag(ModelObjectTag): - """ - Model ObjectTag used for testing - """ - - system_defined_taxonomy_id = 3 - - class Meta: - proxy = True - managed = False - app_label = 'oel_tagging' - - tag_class_model = EmptyTestModel - - -class TestUserObjectTag(UserObjectTag): - """ - User ObjectTag used for testing - """ - - system_defined_taxonomy_id = 3 - - class Meta: - proxy = True - managed = False - app_label = 'oel_tagging' - - -@ddt.ddt -class TestSystemDefinedObjectTags(TestTagTaxonomyMixin, TestCase): - """ - Test for generic system defined object tags - """ - def test_system_defined_is_valid(self): - # Valid - assert TestUserObjectTag()._check_system_taxonomy(taxonomy=self.user_system_taxonomy) - - # Null taxonomy - assert not UserObjectTag()._check_system_taxonomy() - - # Not system defined taxonomy - assert not UserObjectTag()._check_system_taxonomy(taxonomy=self.taxonomy) - - # Not connected with the taxonomy - assert not UserObjectTag()._check_system_taxonomy(taxonomy=self.user_system_taxonomy) - - @ddt.data( - (EmptyModelObjectTag, False), # Without associated model - (NotDjangoModelObjectTag, False), # Associated class is not a Django model - (NotIdModelObjectTag, False), # Associated model has not 'id' field - (ModelObjectTag, False), # Testing parent class validations - (TestUserObjectTag, True), #Valid - ) - @ddt.unpack - def test_model_object_is_valid(self, object_tag, assert_value): - args = { - 'taxonomy': self.user_system_taxonomy, - 'object_id': 'id', - 'object_type': 'object', - 'value': 'value', - } - result = object_tag(**args).is_valid(check_object=False, check_tag=False, check_taxonomy=True) - self.assertEqual(result, assert_value) - - @ddt.data( - (None, True), # Valid - ('user_id', False), # Invalid user id - ('10000', False), # User don't exits - (None, False), # Testing parent class validations - ) - @ddt.unpack - def test_user_object_is_valid(self, value, assert_value): - if assert_value: - user = get_user_model()( - username='username', - email='email' - ) - user.save() - value = user.id - - object_tag = TestUserObjectTag( - taxonomy=self.user_system_taxonomy, - object_id='id', - object_type='object', - value=value, - ) - - result = object_tag.is_valid( - check_taxonomy=True, - check_object=True, - check_tag=True, - ) - - self.assertEqual(result, assert_value) - - -@override_settings(LANGUAGES=test_languages) -class TestLanguageObjectClass(TestCase): - """ - Test for Language object class - """ - - fixtures = [ - "tests/openedx_tagging/core/fixtures/system_defined.yaml", - "openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml" - ] - - def setUp(self): - super().setUp() - self.taxonomy = Taxonomy.objects.get(name="System Languages") - self.expected_langs_ids = sorted([item[0] for item in test_languages]) - self.expected_langs_values = sorted([item[1] for item in test_languages]) - self.english_tag = Tag.objects.get(value="English") - self.spanish_tag = Tag.objects.get(value="Spanish") - - def test_get_available_languages(self): - langs = LanguageObjectTag._get_available_languages() - self.assertEqual(sorted(langs), self.expected_langs_ids) - - def test_is_valid(self): - valid_object_tag = LanguageObjectTag( - taxonomy=self.taxonomy, - object_id='id 1', - object_type='object', - tag=self.english_tag, - ) - - invalid_onbject_tag_1 = LanguageObjectTag( - taxonomy=self.taxonomy, - object_id='id 2', - object_type='object', - tag=self.spanish_tag, - ) - - invalid_onbject_tag_2 = LanguageObjectTag( - taxonomy=self.taxonomy, - object_id='id 2', - object_type='object', - ) - - # Tag is not in available languages - assert not invalid_onbject_tag_1.is_valid( - check_taxonomy=True, - check_object=True, - check_tag=True, - ) - - # Testing parent class validations - assert not invalid_onbject_tag_2.is_valid( - check_taxonomy=True, - check_object=True, - check_tag=True, - ) - - # Valid - assert valid_object_tag.is_valid( - check_taxonomy=True, - check_object=True, - check_tag=True, - ) - - def test_get_tags_query_set(self): - tags = LanguageObjectTag.get_tags_query_set(self.taxonomy) - for tag in tags: - self.assertIn(tag.value, self.expected_langs_values)