From ac21e1c66dc49b7d31fa6a9baad7020021346f07 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 3 Jul 2023 15:39:15 -0500 Subject: [PATCH 01/27] feat: System-defined taxonomies --- .../tagging/management/commands/__init__.py | 0 .../commands/build_language_fixture.py | 60 + .../tagging/migrations/0002_systemtaxonomy.py | 24 + openedx_tagging/core/tagging/models.py | 102 ++ .../fixtures/language_taxonomy.yaml | 1297 +++++++++++++++++ .../system_defined.py | 29 + .../core/tagging/test_system_defined.py | 39 + 7 files changed, 1551 insertions(+) create mode 100644 openedx_tagging/core/tagging/management/commands/__init__.py create mode 100644 openedx_tagging/core/tagging/management/commands/build_language_fixture.py create mode 100644 openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py create mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml create mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py create mode 100644 tests/openedx_tagging/core/tagging/test_system_defined.py diff --git a/openedx_tagging/core/tagging/management/commands/__init__.py b/openedx_tagging/core/tagging/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py new file mode 100644 index 000000000..1b5f26977 --- /dev/null +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -0,0 +1,60 @@ +""" +Script that downloads all the ISO 639-1 languages and processes them +to write the fixture for the Language system-defined taxonomy. + +This function is intended to be used only once, +but can be edited in the future if more data needs to be added to the fixture. +""" +import urllib.request +import json + +from django.core.management.base import BaseCommand + +endpoint = "https://pkgstore.datahub.io/core/language-codes/language-codes_json/data/97607046542b532c395cf83df5185246/language-codes_json.json" +output = "./openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml" +taxonomy = """\ +- model: oel_tagging.taxonomy + pk: 1 + fields: + name: Language + description: ISO 639-1 Languages + enabled: true + required: true + allow_multiple: false + allow_free_text: false +""" + +class Command(BaseCommand): + + def handle(self, **options): + json_data = self.download_json() + self.build_fixture(json_data) + + def download_json(self): + with urllib.request.urlopen(endpoint) as response: + json_data = response.read() + return json.loads(json_data) + + def build_fixture(self, json_data): + tag_pk = 1 + with open(output, 'w') as output_file: + output_file.writelines(taxonomy) + for lang_data in json_data: + lang_value = self.get_lang_value(lang_data) + lang_code = lang_data["alpha2"] + output_file.write("- model: oel_tagging.tag\n") + output_file.write(f" pk: {tag_pk}\n") + output_file.write(" fields:\n") + output_file.write(" taxonomy: 1\n") + output_file.write(" parent: null\n") + output_file.write(f" value: {lang_value}\n") + output_file.write(f" external_id: {lang_code}\n") + tag_pk += 1 + + def get_lang_value(self, lang_data): + """ + Gets the lang value. Some languages has many values. + """ + lang_list = lang_data["English"].split(';') + return lang_list[0] + \ No newline at end of file diff --git a/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py b/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py new file mode 100644 index 000000000..4c4ff74b6 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.19 on 2023-07-04 20:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SystemTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.taxonomy',), + ), + ] diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index 3c8222bcd..f16338ee6 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -1,6 +1,7 @@ """ Tagging app data models """ import logging from typing import List, Type, Union +from enum import Enum from django.db import models from django.utils.module_loading import import_string @@ -18,6 +19,13 @@ # Will contain 0...TAXONOMY_MAX_DEPTH elements. Lineage = List[str] +class SystemDefinedTaxonomyTagsType(Enum): + """ + Tag types of system-defined taxonomies + """ + closed = 'closed' # Tags are created by fixtures/migrations + free_form = 'free_form' # Tags are free form + class Tag(models.Model): """ @@ -627,3 +635,97 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._value = object_tag._value 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. + """ + # Must have a valid object id/type: + return self.object_id and self.object_type + + +class ClosedObjectTag(OpenObjectTag): + """ + Object tags linked to a closed taxonomy, where the available tag value options are known. + """ + + class Meta: + proxy = True + + def _check_taxonomy(self): + """ + Returns True if this ObjectTag is linked to a closed taxonomy. + + Subclasses should override this method to perform any additional validation for the particular type of object tag. + """ + # Must be linked to a closed taxonomy + return self.taxonomy_id and not self.taxonomy.allow_free_text + + def _check_tag(self): + """ + Returns True if this ObjectTag has a valid tag. + + Subclasses should override this method to perform any additional validation for the particular type of object tag. + """ + # Closed taxonomies require a Tag + return bool(self.tag_id) + + def is_valid(self, check_taxonomy=True, check_tag=True, check_object=True) -> bool: + """ + Returns True if this ObjectTag is valid for use with a closed taxonomy. + + Subclasses should override this method to perform any additional validation for the particular type of object tag. + + 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. + """ + if not super().is_valid( + check_taxonomy=check_taxonomy, + check_tag=check_tag, + check_object=check_object, + ): + return False + + if check_tag and check_taxonomy and (self.tag.taxonomy_id != self.taxonomy_id): + return False + + return True + + +# Register the ObjectTag subclasses in reverse order of how we want them considered. +register_object_tag_class(OpenObjectTag) +register_object_tag_class(ClosedObjectTag) + +class SystemTaxonomy(Taxonomy): + """ + System-defined taxonomies are not editable by normal users; they're defined by fixtures/migrations, and may have + dynamically-determined Tags and ObjectTags. + """ + + @property + def system_defined(self) -> bool: + """ + This is a system-defined taxonomy. + """ + return True + + @property + def is_visible(self) -> bool: + """ + Controls the visibility of this taxonomy to content authors. + + This value is static and must be implemented per each system-defined taxonomy. + """ + raise NotImplementedError + + @property + def creation_type (self) -> SystemDefinedTaxonomyTagsType: + """ + Controls the behaviour of the tags on this taxonomy + + This value is static and must be implemented per each system-defined taxonomy. + """ + raise NotImplementedError diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml new file mode 100644 index 000000000..bb10d6c96 --- /dev/null +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml @@ -0,0 +1,1297 @@ +- model: oel_tagging.taxonomy + pk: 1 + fields: + name: Language + description: ISO 639-1 Languages + enabled: true + required: true + allow_multiple: false + allow_free_text: false +- model: oel_tagging.tag + pk: 1 + fields: + taxonomy: 1 + parent: null + value: Afar + external_id: aa +- model: oel_tagging.tag + pk: 2 + fields: + taxonomy: 1 + parent: null + value: Abkhazian + external_id: ab +- model: oel_tagging.tag + pk: 3 + fields: + taxonomy: 1 + parent: null + value: Avestan + external_id: ae +- model: oel_tagging.tag + pk: 4 + fields: + taxonomy: 1 + parent: null + value: Afrikaans + external_id: af +- model: oel_tagging.tag + pk: 5 + fields: + taxonomy: 1 + parent: null + value: Akan + external_id: ak +- model: oel_tagging.tag + pk: 6 + fields: + taxonomy: 1 + parent: null + value: Amharic + external_id: am +- model: oel_tagging.tag + pk: 7 + fields: + taxonomy: 1 + parent: null + value: Aragonese + external_id: an +- model: oel_tagging.tag + pk: 8 + fields: + taxonomy: 1 + parent: null + value: Arabic + external_id: ar +- model: oel_tagging.tag + pk: 9 + fields: + taxonomy: 1 + parent: null + value: Assamese + external_id: as +- model: oel_tagging.tag + pk: 10 + fields: + taxonomy: 1 + parent: null + value: Avaric + external_id: av +- model: oel_tagging.tag + pk: 11 + fields: + taxonomy: 1 + parent: null + value: Aymara + external_id: ay +- model: oel_tagging.tag + pk: 12 + fields: + taxonomy: 1 + parent: null + value: Azerbaijani + external_id: az +- model: oel_tagging.tag + pk: 13 + fields: + taxonomy: 1 + parent: null + value: Bashkir + external_id: ba +- model: oel_tagging.tag + pk: 14 + fields: + taxonomy: 1 + parent: null + value: Belarusian + external_id: be +- model: oel_tagging.tag + pk: 15 + fields: + taxonomy: 1 + parent: null + value: Bulgarian + external_id: bg +- model: oel_tagging.tag + pk: 16 + fields: + taxonomy: 1 + parent: null + value: Bihari languages + external_id: bh +- model: oel_tagging.tag + pk: 17 + fields: + taxonomy: 1 + parent: null + value: Bislama + external_id: bi +- model: oel_tagging.tag + pk: 18 + fields: + taxonomy: 1 + parent: null + value: Bambara + external_id: bm +- model: oel_tagging.tag + pk: 19 + fields: + taxonomy: 1 + parent: null + value: Bengali + external_id: bn +- model: oel_tagging.tag + pk: 20 + fields: + taxonomy: 1 + parent: null + value: Tibetan + external_id: bo +- model: oel_tagging.tag + pk: 21 + fields: + taxonomy: 1 + parent: null + value: Breton + external_id: br +- model: oel_tagging.tag + pk: 22 + fields: + taxonomy: 1 + parent: null + value: Bosnian + external_id: bs +- model: oel_tagging.tag + pk: 23 + fields: + taxonomy: 1 + parent: null + value: Catalan + external_id: ca +- model: oel_tagging.tag + pk: 24 + fields: + taxonomy: 1 + parent: null + value: Chechen + external_id: ce +- model: oel_tagging.tag + pk: 25 + fields: + taxonomy: 1 + parent: null + value: Chamorro + external_id: ch +- model: oel_tagging.tag + pk: 26 + fields: + taxonomy: 1 + parent: null + value: Corsican + external_id: co +- model: oel_tagging.tag + pk: 27 + fields: + taxonomy: 1 + parent: null + value: Cree + external_id: cr +- model: oel_tagging.tag + pk: 28 + fields: + taxonomy: 1 + parent: null + value: Czech + external_id: cs +- model: oel_tagging.tag + pk: 29 + fields: + taxonomy: 1 + parent: null + value: Church Slavic + external_id: cu +- model: oel_tagging.tag + pk: 30 + fields: + taxonomy: 1 + parent: null + value: Chuvash + external_id: cv +- model: oel_tagging.tag + pk: 31 + fields: + taxonomy: 1 + parent: null + value: Welsh + external_id: cy +- model: oel_tagging.tag + pk: 32 + fields: + taxonomy: 1 + parent: null + value: Danish + external_id: da +- model: oel_tagging.tag + pk: 33 + fields: + taxonomy: 1 + parent: null + value: German + external_id: de +- model: oel_tagging.tag + pk: 34 + fields: + taxonomy: 1 + parent: null + value: Divehi + external_id: dv +- model: oel_tagging.tag + pk: 35 + fields: + taxonomy: 1 + parent: null + value: Dzongkha + external_id: dz +- model: oel_tagging.tag + pk: 36 + fields: + taxonomy: 1 + parent: null + value: Ewe + external_id: ee +- model: oel_tagging.tag + pk: 37 + fields: + taxonomy: 1 + parent: null + value: Greek, Modern (1453-) + external_id: el +- model: oel_tagging.tag + pk: 38 + fields: + taxonomy: 1 + parent: null + value: English + external_id: en +- model: oel_tagging.tag + pk: 39 + fields: + taxonomy: 1 + parent: null + value: Esperanto + external_id: eo +- model: oel_tagging.tag + pk: 40 + fields: + taxonomy: 1 + parent: null + value: Spanish + external_id: es +- model: oel_tagging.tag + pk: 41 + fields: + taxonomy: 1 + parent: null + value: Estonian + external_id: et +- model: oel_tagging.tag + pk: 42 + fields: + taxonomy: 1 + parent: null + value: Basque + external_id: eu +- model: oel_tagging.tag + pk: 43 + fields: + taxonomy: 1 + parent: null + value: Persian + external_id: fa +- model: oel_tagging.tag + pk: 44 + fields: + taxonomy: 1 + parent: null + value: Fulah + external_id: ff +- model: oel_tagging.tag + pk: 45 + fields: + taxonomy: 1 + parent: null + value: Finnish + external_id: fi +- model: oel_tagging.tag + pk: 46 + fields: + taxonomy: 1 + parent: null + value: Fijian + external_id: fj +- model: oel_tagging.tag + pk: 47 + fields: + taxonomy: 1 + parent: null + value: Faroese + external_id: fo +- model: oel_tagging.tag + pk: 48 + fields: + taxonomy: 1 + parent: null + value: French + external_id: fr +- model: oel_tagging.tag + pk: 49 + fields: + taxonomy: 1 + parent: null + value: Western Frisian + external_id: fy +- model: oel_tagging.tag + pk: 50 + fields: + taxonomy: 1 + parent: null + value: Irish + external_id: ga +- model: oel_tagging.tag + pk: 51 + fields: + taxonomy: 1 + parent: null + value: Gaelic + external_id: gd +- model: oel_tagging.tag + pk: 52 + fields: + taxonomy: 1 + parent: null + value: Galician + external_id: gl +- model: oel_tagging.tag + pk: 53 + fields: + taxonomy: 1 + parent: null + value: Guarani + external_id: gn +- model: oel_tagging.tag + pk: 54 + fields: + taxonomy: 1 + parent: null + value: Gujarati + external_id: gu +- model: oel_tagging.tag + pk: 55 + fields: + taxonomy: 1 + parent: null + value: Manx + external_id: gv +- model: oel_tagging.tag + pk: 56 + fields: + taxonomy: 1 + parent: null + value: Hausa + external_id: ha +- model: oel_tagging.tag + pk: 57 + fields: + taxonomy: 1 + parent: null + value: Hebrew + external_id: he +- model: oel_tagging.tag + pk: 58 + fields: + taxonomy: 1 + parent: null + value: Hindi + external_id: hi +- model: oel_tagging.tag + pk: 59 + fields: + taxonomy: 1 + parent: null + value: Hiri Motu + external_id: ho +- model: oel_tagging.tag + pk: 60 + fields: + taxonomy: 1 + parent: null + value: Croatian + external_id: hr +- model: oel_tagging.tag + pk: 61 + fields: + taxonomy: 1 + parent: null + value: Haitian + external_id: ht +- model: oel_tagging.tag + pk: 62 + fields: + taxonomy: 1 + parent: null + value: Hungarian + external_id: hu +- model: oel_tagging.tag + pk: 63 + fields: + taxonomy: 1 + parent: null + value: Armenian + external_id: hy +- model: oel_tagging.tag + pk: 64 + fields: + taxonomy: 1 + parent: null + value: Herero + external_id: hz +- model: oel_tagging.tag + pk: 65 + fields: + taxonomy: 1 + parent: null + value: Interlingua (International Auxiliary Language Association) + external_id: ia +- model: oel_tagging.tag + pk: 66 + fields: + taxonomy: 1 + parent: null + value: Indonesian + external_id: id +- model: oel_tagging.tag + pk: 67 + fields: + taxonomy: 1 + parent: null + value: Interlingue + external_id: ie +- model: oel_tagging.tag + pk: 68 + fields: + taxonomy: 1 + parent: null + value: Igbo + external_id: ig +- model: oel_tagging.tag + pk: 69 + fields: + taxonomy: 1 + parent: null + value: Sichuan Yi + external_id: ii +- model: oel_tagging.tag + pk: 70 + fields: + taxonomy: 1 + parent: null + value: Inupiaq + external_id: ik +- model: oel_tagging.tag + pk: 71 + fields: + taxonomy: 1 + parent: null + value: Ido + external_id: io +- model: oel_tagging.tag + pk: 72 + fields: + taxonomy: 1 + parent: null + value: Icelandic + external_id: is +- model: oel_tagging.tag + pk: 73 + fields: + taxonomy: 1 + parent: null + value: Italian + external_id: it +- model: oel_tagging.tag + pk: 74 + fields: + taxonomy: 1 + parent: null + value: Inuktitut + external_id: iu +- model: oel_tagging.tag + pk: 75 + fields: + taxonomy: 1 + parent: null + value: Japanese + external_id: ja +- model: oel_tagging.tag + pk: 76 + fields: + taxonomy: 1 + parent: null + value: Javanese + external_id: jv +- model: oel_tagging.tag + pk: 77 + fields: + taxonomy: 1 + parent: null + value: Georgian + external_id: ka +- model: oel_tagging.tag + pk: 78 + fields: + taxonomy: 1 + parent: null + value: Kongo + external_id: kg +- model: oel_tagging.tag + pk: 79 + fields: + taxonomy: 1 + parent: null + value: Kikuyu + external_id: ki +- model: oel_tagging.tag + pk: 80 + fields: + taxonomy: 1 + parent: null + value: Kuanyama + external_id: kj +- model: oel_tagging.tag + pk: 81 + fields: + taxonomy: 1 + parent: null + value: Kazakh + external_id: kk +- model: oel_tagging.tag + pk: 82 + fields: + taxonomy: 1 + parent: null + value: Kalaallisut + external_id: kl +- model: oel_tagging.tag + pk: 83 + fields: + taxonomy: 1 + parent: null + value: Central Khmer + external_id: km +- model: oel_tagging.tag + pk: 84 + fields: + taxonomy: 1 + parent: null + value: Kannada + external_id: kn +- model: oel_tagging.tag + pk: 85 + fields: + taxonomy: 1 + parent: null + value: Korean + external_id: ko +- model: oel_tagging.tag + pk: 86 + fields: + taxonomy: 1 + parent: null + value: Kanuri + external_id: kr +- model: oel_tagging.tag + pk: 87 + fields: + taxonomy: 1 + parent: null + value: Kashmiri + external_id: ks +- model: oel_tagging.tag + pk: 88 + fields: + taxonomy: 1 + parent: null + value: Kurdish + external_id: ku +- model: oel_tagging.tag + pk: 89 + fields: + taxonomy: 1 + parent: null + value: Komi + external_id: kv +- model: oel_tagging.tag + pk: 90 + fields: + taxonomy: 1 + parent: null + value: Cornish + external_id: kw +- model: oel_tagging.tag + pk: 91 + fields: + taxonomy: 1 + parent: null + value: Kirghiz + external_id: ky +- model: oel_tagging.tag + pk: 92 + fields: + taxonomy: 1 + parent: null + value: Latin + external_id: la +- model: oel_tagging.tag + pk: 93 + fields: + taxonomy: 1 + parent: null + value: Luxembourgish + external_id: lb +- model: oel_tagging.tag + pk: 94 + fields: + taxonomy: 1 + parent: null + value: Ganda + external_id: lg +- model: oel_tagging.tag + pk: 95 + fields: + taxonomy: 1 + parent: null + value: Limburgan + external_id: li +- model: oel_tagging.tag + pk: 96 + fields: + taxonomy: 1 + parent: null + value: Lingala + external_id: ln +- model: oel_tagging.tag + pk: 97 + fields: + taxonomy: 1 + parent: null + value: Lao + external_id: lo +- model: oel_tagging.tag + pk: 98 + fields: + taxonomy: 1 + parent: null + value: Lithuanian + external_id: lt +- model: oel_tagging.tag + pk: 99 + fields: + taxonomy: 1 + parent: null + value: Luba-Katanga + external_id: lu +- model: oel_tagging.tag + pk: 100 + fields: + taxonomy: 1 + parent: null + value: Latvian + external_id: lv +- model: oel_tagging.tag + pk: 101 + fields: + taxonomy: 1 + parent: null + value: Malagasy + external_id: mg +- model: oel_tagging.tag + pk: 102 + fields: + taxonomy: 1 + parent: null + value: Marshallese + external_id: mh +- model: oel_tagging.tag + pk: 103 + fields: + taxonomy: 1 + parent: null + value: Maori + external_id: mi +- model: oel_tagging.tag + pk: 104 + fields: + taxonomy: 1 + parent: null + value: Macedonian + external_id: mk +- model: oel_tagging.tag + pk: 105 + fields: + taxonomy: 1 + parent: null + value: Malayalam + external_id: ml +- model: oel_tagging.tag + pk: 106 + fields: + taxonomy: 1 + parent: null + value: Mongolian + external_id: mn +- model: oel_tagging.tag + pk: 107 + fields: + taxonomy: 1 + parent: null + value: Marathi + external_id: mr +- model: oel_tagging.tag + pk: 108 + fields: + taxonomy: 1 + parent: null + value: Malay + external_id: ms +- model: oel_tagging.tag + pk: 109 + fields: + taxonomy: 1 + parent: null + value: Maltese + external_id: mt +- model: oel_tagging.tag + pk: 110 + fields: + taxonomy: 1 + parent: null + value: Burmese + external_id: my +- model: oel_tagging.tag + pk: 111 + fields: + taxonomy: 1 + parent: null + value: Nauru + external_id: na +- model: oel_tagging.tag + pk: 112 + fields: + taxonomy: 1 + parent: null + value: Bokmål, Norwegian + external_id: nb +- model: oel_tagging.tag + pk: 113 + fields: + taxonomy: 1 + parent: null + value: Ndebele, North + external_id: nd +- model: oel_tagging.tag + pk: 114 + fields: + taxonomy: 1 + parent: null + value: Nepali + external_id: ne +- model: oel_tagging.tag + pk: 115 + fields: + taxonomy: 1 + parent: null + value: Ndonga + external_id: ng +- model: oel_tagging.tag + pk: 116 + fields: + taxonomy: 1 + parent: null + value: Dutch + external_id: nl +- model: oel_tagging.tag + pk: 117 + fields: + taxonomy: 1 + parent: null + value: Norwegian Nynorsk + external_id: nn +- model: oel_tagging.tag + pk: 118 + fields: + taxonomy: 1 + parent: null + value: Norwegian + external_id: no +- model: oel_tagging.tag + pk: 119 + fields: + taxonomy: 1 + parent: null + value: Ndebele, South + external_id: nr +- model: oel_tagging.tag + pk: 120 + fields: + taxonomy: 1 + parent: null + value: Navajo + external_id: nv +- model: oel_tagging.tag + pk: 121 + fields: + taxonomy: 1 + parent: null + value: Chichewa + external_id: ny +- model: oel_tagging.tag + pk: 122 + fields: + taxonomy: 1 + parent: null + value: Occitan (post 1500) + external_id: oc +- model: oel_tagging.tag + pk: 123 + fields: + taxonomy: 1 + parent: null + value: Ojibwa + external_id: oj +- model: oel_tagging.tag + pk: 124 + fields: + taxonomy: 1 + parent: null + value: Oromo + external_id: om +- model: oel_tagging.tag + pk: 125 + fields: + taxonomy: 1 + parent: null + value: Oriya + external_id: or +- model: oel_tagging.tag + pk: 126 + fields: + taxonomy: 1 + parent: null + value: Ossetian + external_id: os +- model: oel_tagging.tag + pk: 127 + fields: + taxonomy: 1 + parent: null + value: Panjabi + external_id: pa +- model: oel_tagging.tag + pk: 128 + fields: + taxonomy: 1 + parent: null + value: Pali + external_id: pi +- model: oel_tagging.tag + pk: 129 + fields: + taxonomy: 1 + parent: null + value: Polish + external_id: pl +- model: oel_tagging.tag + pk: 130 + fields: + taxonomy: 1 + parent: null + value: Pushto + external_id: ps +- model: oel_tagging.tag + pk: 131 + fields: + taxonomy: 1 + parent: null + value: Portuguese + external_id: pt +- model: oel_tagging.tag + pk: 132 + fields: + taxonomy: 1 + parent: null + value: Quechua + external_id: qu +- model: oel_tagging.tag + pk: 133 + fields: + taxonomy: 1 + parent: null + value: Romansh + external_id: rm +- model: oel_tagging.tag + pk: 134 + fields: + taxonomy: 1 + parent: null + value: Rundi + external_id: rn +- model: oel_tagging.tag + pk: 135 + fields: + taxonomy: 1 + parent: null + value: Romanian + external_id: ro +- model: oel_tagging.tag + pk: 136 + fields: + taxonomy: 1 + parent: null + value: Russian + external_id: ru +- model: oel_tagging.tag + pk: 137 + fields: + taxonomy: 1 + parent: null + value: Kinyarwanda + external_id: rw +- model: oel_tagging.tag + pk: 138 + fields: + taxonomy: 1 + parent: null + value: Sanskrit + external_id: sa +- model: oel_tagging.tag + pk: 139 + fields: + taxonomy: 1 + parent: null + value: Sardinian + external_id: sc +- model: oel_tagging.tag + pk: 140 + fields: + taxonomy: 1 + parent: null + value: Sindhi + external_id: sd +- model: oel_tagging.tag + pk: 141 + fields: + taxonomy: 1 + parent: null + value: Northern Sami + external_id: se +- model: oel_tagging.tag + pk: 142 + fields: + taxonomy: 1 + parent: null + value: Sango + external_id: sg +- model: oel_tagging.tag + pk: 143 + fields: + taxonomy: 1 + parent: null + value: Sinhala + external_id: si +- model: oel_tagging.tag + pk: 144 + fields: + taxonomy: 1 + parent: null + value: Slovak + external_id: sk +- model: oel_tagging.tag + pk: 145 + fields: + taxonomy: 1 + parent: null + value: Slovenian + external_id: sl +- model: oel_tagging.tag + pk: 146 + fields: + taxonomy: 1 + parent: null + value: Samoan + external_id: sm +- model: oel_tagging.tag + pk: 147 + fields: + taxonomy: 1 + parent: null + value: Shona + external_id: sn +- model: oel_tagging.tag + pk: 148 + fields: + taxonomy: 1 + parent: null + value: Somali + external_id: so +- model: oel_tagging.tag + pk: 149 + fields: + taxonomy: 1 + parent: null + value: Albanian + external_id: sq +- model: oel_tagging.tag + pk: 150 + fields: + taxonomy: 1 + parent: null + value: Serbian + external_id: sr +- model: oel_tagging.tag + pk: 151 + fields: + taxonomy: 1 + parent: null + value: Swati + external_id: ss +- model: oel_tagging.tag + pk: 152 + fields: + taxonomy: 1 + parent: null + value: Sotho, Southern + external_id: st +- model: oel_tagging.tag + pk: 153 + fields: + taxonomy: 1 + parent: null + value: Sundanese + external_id: su +- model: oel_tagging.tag + pk: 154 + fields: + taxonomy: 1 + parent: null + value: Swedish + external_id: sv +- model: oel_tagging.tag + pk: 155 + fields: + taxonomy: 1 + parent: null + value: Swahili + external_id: sw +- model: oel_tagging.tag + pk: 156 + fields: + taxonomy: 1 + parent: null + value: Tamil + external_id: ta +- model: oel_tagging.tag + pk: 157 + fields: + taxonomy: 1 + parent: null + value: Telugu + external_id: te +- model: oel_tagging.tag + pk: 158 + fields: + taxonomy: 1 + parent: null + value: Tajik + external_id: tg +- model: oel_tagging.tag + pk: 159 + fields: + taxonomy: 1 + parent: null + value: Thai + external_id: th +- model: oel_tagging.tag + pk: 160 + fields: + taxonomy: 1 + parent: null + value: Tigrinya + external_id: ti +- model: oel_tagging.tag + pk: 161 + fields: + taxonomy: 1 + parent: null + value: Turkmen + external_id: tk +- model: oel_tagging.tag + pk: 162 + fields: + taxonomy: 1 + parent: null + value: Tagalog + external_id: tl +- model: oel_tagging.tag + pk: 163 + fields: + taxonomy: 1 + parent: null + value: Tswana + external_id: tn +- model: oel_tagging.tag + pk: 164 + fields: + taxonomy: 1 + parent: null + value: Tonga (Tonga Islands) + external_id: to +- model: oel_tagging.tag + pk: 165 + fields: + taxonomy: 1 + parent: null + value: Turkish + external_id: tr +- model: oel_tagging.tag + pk: 166 + fields: + taxonomy: 1 + parent: null + value: Tsonga + external_id: ts +- model: oel_tagging.tag + pk: 167 + fields: + taxonomy: 1 + parent: null + value: Tatar + external_id: tt +- model: oel_tagging.tag + pk: 168 + fields: + taxonomy: 1 + parent: null + value: Twi + external_id: tw +- model: oel_tagging.tag + pk: 169 + fields: + taxonomy: 1 + parent: null + value: Tahitian + external_id: ty +- model: oel_tagging.tag + pk: 170 + fields: + taxonomy: 1 + parent: null + value: Uighur + external_id: ug +- model: oel_tagging.tag + pk: 171 + fields: + taxonomy: 1 + parent: null + value: Ukrainian + external_id: uk +- model: oel_tagging.tag + pk: 172 + fields: + taxonomy: 1 + parent: null + value: Urdu + external_id: ur +- model: oel_tagging.tag + pk: 173 + fields: + taxonomy: 1 + parent: null + value: Uzbek + external_id: uz +- model: oel_tagging.tag + pk: 174 + fields: + taxonomy: 1 + parent: null + value: Venda + external_id: ve +- model: oel_tagging.tag + pk: 175 + fields: + taxonomy: 1 + parent: null + value: Vietnamese + external_id: vi +- model: oel_tagging.tag + pk: 176 + fields: + taxonomy: 1 + parent: null + value: Volapük + external_id: vo +- model: oel_tagging.tag + pk: 177 + fields: + taxonomy: 1 + parent: null + value: Walloon + external_id: wa +- model: oel_tagging.tag + pk: 178 + fields: + taxonomy: 1 + parent: null + value: Wolof + external_id: wo +- model: oel_tagging.tag + pk: 179 + fields: + taxonomy: 1 + parent: null + value: Xhosa + external_id: xh +- model: oel_tagging.tag + pk: 180 + fields: + taxonomy: 1 + parent: null + value: Yiddish + external_id: yi +- model: oel_tagging.tag + pk: 181 + fields: + taxonomy: 1 + parent: null + value: Yoruba + external_id: yo +- model: oel_tagging.tag + pk: 182 + fields: + taxonomy: 1 + parent: null + value: Zhuang + external_id: za +- model: oel_tagging.tag + pk: 183 + fields: + taxonomy: 1 + parent: null + value: Chinese + external_id: zh +- model: oel_tagging.tag + pk: 184 + fields: + taxonomy: 1 + parent: null + value: Zulu + external_id: zu diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py b/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py new file mode 100644 index 000000000..f500d5f5c --- /dev/null +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py @@ -0,0 +1,29 @@ +from openedx_tagging.core.tagging.models import SystemTaxonomy, SystemDefinedTaxonomyTagsType + + +class LanguageTaxonomy(SystemTaxonomy): + + class Meta: + proxy = True + + @property + def is_visible(self) -> bool: + raise True + + @property + def creation_type (self) -> SystemDefinedTaxonomyTagsType: + return SystemDefinedTaxonomyTagsType.closed + + +class AuthorTaxonomy(SystemTaxonomy): + + class Meta: + proxy = True + + @property + def is_visible(self) -> bool: + raise False + + @property + def creation_type (self) -> SystemDefinedTaxonomyTagsType: + return SystemDefinedTaxonomyTagsType.free_form diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py new file mode 100644 index 000000000..438a63942 --- /dev/null +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -0,0 +1,39 @@ +""" Test the System-defined taxonomies """ + +import ddt +from django.test.testcases import TestCase + +from openedx_tagging.core.tagging.models import Taxonomy + + +class TestSystemDefinedTaxonomyMixin: + """ + Mixin used on system-defined taxonomy tests + """ + + fixtures = ["openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml"] + + def setUp(self): + super().setUp() + self.language_taxonomy = Taxonomy.objects.get(pk=1) + + +@ddt.ddt +class TestLanguageTaxonomy(TestSystemDefinedTaxonomyMixin, TestCase): + """ + Test the Language Taxonomy + """ + + @ddt.data( + ('en', 'English'), + ('az', 'Azerbaijani'), + ('id', 'Indonesian'), + ('qu', 'Quechua'), + ('zu', 'Zulu'), + ) + @ddt.unpack + def test_fixture(self, lang_code, lang_value): + self.assertEqual(self.language_taxonomy.name, 'Language') + lang = self.language_taxonomy.tag_set.get(external_id=lang_code) + self.assertEqual(lang.value, lang_value) + \ No newline at end of file From 55636f7d39c1591fa8ce11d761229332d8971241 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 5 Jul 2023 16:31:41 -0500 Subject: [PATCH 02/27] feat: Language and Author taxonomies --- .../commands/build_language_fixture.py | 12 -- .../fixtures/language_taxonomy.yaml | 9 - .../fixtures/taxonomies.yaml | 18 ++ .../system_defined.py | 161 ++++++++++++++++- .../core/tagging/test_system_defined.py | 163 ++++++++++++++++-- 5 files changed, 319 insertions(+), 44 deletions(-) create mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index 1b5f26977..fef118a5c 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -12,17 +12,6 @@ endpoint = "https://pkgstore.datahub.io/core/language-codes/language-codes_json/data/97607046542b532c395cf83df5185246/language-codes_json.json" output = "./openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml" -taxonomy = """\ -- model: oel_tagging.taxonomy - pk: 1 - fields: - name: Language - description: ISO 639-1 Languages - enabled: true - required: true - allow_multiple: false - allow_free_text: false -""" class Command(BaseCommand): @@ -38,7 +27,6 @@ def download_json(self): def build_fixture(self, json_data): tag_pk = 1 with open(output, 'w') as output_file: - output_file.writelines(taxonomy) for lang_data in json_data: lang_value = self.get_lang_value(lang_data) lang_code = lang_data["alpha2"] diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml index bb10d6c96..efe84cdbb 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml @@ -1,12 +1,3 @@ -- model: oel_tagging.taxonomy - pk: 1 - fields: - name: Language - description: ISO 639-1 Languages - enabled: true - required: true - allow_multiple: false - allow_free_text: false - model: oel_tagging.tag pk: 1 fields: diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml new file mode 100644 index 000000000..692001f7e --- /dev/null +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml @@ -0,0 +1,18 @@ +- model: oel_tagging.taxonomy + pk: 1 + fields: + name: Language + description: ISO 639-1 Languages + enabled: true + required: true + allow_multiple: false + allow_free_text: false +- model: oel_tagging.taxonomy + pk: 2 + fields: + name: Author + description: User Ids of the platform + enabled: true + required: true + allow_multiple: false + allow_free_text: true diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py b/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py index f500d5f5c..f25300c5a 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py @@ -1,7 +1,63 @@ -from openedx_tagging.core.tagging.models import SystemTaxonomy, SystemDefinedTaxonomyTagsType +from typing import List + +from django.conf import settings +from django.contrib.auth import get_user_model + +from openedx_tagging.core.tagging.models import ObjectTag, Tag, SystemTaxonomy, SystemDefinedTaxonomyTagsType + +class IdBasedSystemTaxonomyMixin: + """ + Mixin for ID based taxonomies. + + Used to build tags with a prefix. E.x user: + """ + + prefix = "" + separator = ":" + + @property + def creation_type(self) -> SystemDefinedTaxonomyTagsType: + return SystemDefinedTaxonomyTagsType.free_form + + def get_id(self, tag_value): + """ + Get the id part of the tag + """ + return tag_value.split(self.separator)[1] + + def build_tag(self, id): + """ + Build the tag with the prefix and the separator + """ + return f"{self.prefix}{self.separator}{id}" + + def tag_object( + self, tags: List, object_id: str, object_type: str + ) -> List["ObjectTag"]: + """ + Overwriten the tag_object of the Taxonomy model + + Preprocess and build the new tags + """ + new_tags = [] + for tag in tags: + new_tags.append(self.build_tag(tag)) + + return super().tag_object(tags, object_id, object_type) class LanguageTaxonomy(SystemTaxonomy): + """ + Language system-defined taxonomy. + + Tag creation: Hardcoded by fixtures + Visible: True + Allow multiple: False + Tags: ISO 639-1 Languages + + The tags are filtered and validated taking into account the + languages available in Django LANGUAGES settings var + """ class Meta: proxy = True @@ -11,19 +67,108 @@ def is_visible(self) -> bool: raise True @property - def creation_type (self) -> SystemDefinedTaxonomyTagsType: + def creation_type(self) -> SystemDefinedTaxonomyTagsType: return SystemDefinedTaxonomyTagsType.closed + + def get_available_languages(self) -> 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 get_tags(self) -> List[Tag]: + """ + Returns a list of tags of the available languages. + """ + tags = super().get_tags() + result = [] + available_langs = self.get_available_languages() + for tag in tags: + if tag.external_id in available_langs: + result.append(tag) + return result -class AuthorTaxonomy(SystemTaxonomy): + def validate_object_tag( + self, + object_tag: "ObjectTag", + check_taxonomy=True, + check_tag=True, + check_object=True, + ) -> bool: + """ + Makes the normal object validation and validates if the + tag is an available language + """ + validation = super().validate_object_tag( + object_tag, + check_taxonomy, + check_tag, + check_object, + ) + if not validation: + return False + + available_langs = self.get_available_languages() + + # Must be linked to a tag and must be an available language + if not object_tag.tag or not object_tag.tag.external_id in available_langs: + return False + + return True + + +class AuthorTaxonomy(SystemTaxonomy, IdBasedSystemTaxonomyMixin): + """ + Author system-defined taxonomy + + Tag creation: Free form + Visible: False + Allow multiple: False + Tags: Id's of users in the form: author: + """ class Meta: proxy = True - + + prefix = "author" + @property def is_visible(self) -> bool: raise False - - @property - def creation_type (self) -> SystemDefinedTaxonomyTagsType: - return SystemDefinedTaxonomyTagsType.free_form + + def validate_object_tag( + self, + object_tag: "ObjectTag", + check_taxonomy=True, + check_tag=True, + check_object=True, + ) -> bool: + """ + Makes the normal object validation and validates if the user exists + """ + validation = super().validate_object_tag( + object_tag, + check_taxonomy, + check_tag, + check_object, + ) + if not validation: + return False + + user_id = self.get_id(object_tag.value) + + # The user must exists + if not self._user_exists(user_id): + return False + + return True + + def _user_exists(self, user_id): + """ + Verify if an user exists + """ + return get_user_model().objects.filter(id=user_id).exists() diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index 438a63942..94b934223 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -1,39 +1,172 @@ """ Test the System-defined taxonomies """ import ddt -from django.test.testcases import TestCase -from openedx_tagging.core.tagging.models import Taxonomy +from django.contrib.auth import get_user_model +from django.test.testcases import TestCase, override_settings +from openedx_tagging.core.tagging.models import Taxonomy, ObjectTag, Tag +from openedx_tagging.core.tagging.system_defined_taxonomies.system_defined import ( + LanguageTaxonomy, + AuthorTaxonomy +) + +fixtures_path = "openedx_tagging/core/tagging/system_defined_taxonomies/fixtures" +test_languages = [ + ('en', 'English'), + ('az', 'Azerbaijani'), + ('id', 'Indonesian'), + ('qu', 'Quechua'), + ('zu', 'Zulu'), +] class TestSystemDefinedTaxonomyMixin: """ Mixin used on system-defined taxonomy tests """ - fixtures = ["openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml"] + fixtures = [f"{fixtures_path}/taxonomies.yaml"] def setUp(self): super().setUp() - self.language_taxonomy = Taxonomy.objects.get(pk=1) + temp_taxonomy = Taxonomy.objects.get(pk=1) + self.language_taxonomy = LanguageTaxonomy() + self.language_taxonomy.__dict__ = temp_taxonomy.__dict__ + temp_taxonomy = Taxonomy.objects.get(pk=2) + self.author_taxonomy = AuthorTaxonomy() + self.author_taxonomy.__dict__ = temp_taxonomy.__dict__ @ddt.ddt +@override_settings(LANGUAGES=test_languages) class TestLanguageTaxonomy(TestSystemDefinedTaxonomyMixin, TestCase): """ Test the Language Taxonomy """ - @ddt.data( - ('en', 'English'), - ('az', 'Azerbaijani'), - ('id', 'Indonesian'), - ('qu', 'Quechua'), - ('zu', 'Zulu'), + def setUp(self): + super().setUp() + 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") + + fixtures = ( + TestSystemDefinedTaxonomyMixin.fixtures + + [f"{fixtures_path}/language_taxonomy.yaml"] ) - @ddt.unpack - def test_fixture(self, lang_code, lang_value): + + def test_fixture(self): self.assertEqual(self.language_taxonomy.name, 'Language') - lang = self.language_taxonomy.tag_set.get(external_id=lang_code) - self.assertEqual(lang.value, lang_value) - \ No newline at end of file + for lang_code, lang_value in test_languages: + lang = self.language_taxonomy.tag_set.get(external_id=lang_code) + self.assertEqual(lang.value, lang_value) + + def test_get_available_languages(self): + langs = self.language_taxonomy.get_available_languages() + self.assertEqual(sorted(langs), self.expected_langs_ids) + + def test_get_tags(self): + tags = self.language_taxonomy.get_tags() + for tag in tags: + self.assertIn(tag.value, self.expected_langs_values) + self.assertEqual(tag.annotated_field, 0) # Checking depth + + def test_validate_object_tag(self): + object_id = 'object_id' + invalid_object_tag_1 = ObjectTag( + taxonomy=self.language_taxonomy, + object_id=object_id, + tag=self.spanish_tag, + ) + invalid_object_tag_2 = ObjectTag( + object_id=object_id, + ) + valid_object_tag = ObjectTag( + taxonomy=self.language_taxonomy, + object_id=object_id, + tag=self.english_tag, + ) + + # Invalid. Language is not available + self.language_taxonomy.validate_object_tag( + object_tag=invalid_object_tag_1, + check_taxonomy=False, + check_tag=False, + check_object=False, + ) + + # Invalid. Testing normal object validations + self.language_taxonomy.validate_object_tag( + object_tag=invalid_object_tag_2, + check_taxonomy=True, + check_tag=False, + check_object=False, + ) + + # Valid + self.language_taxonomy.validate_object_tag( + object_tag=valid_object_tag, + check_taxonomy=False, + check_tag=False, + check_object=False, + ) + + +class TestAuthorTaxonomy(TestSystemDefinedTaxonomyMixin, TestCase): + """ + Test the Author Taxonomy + """ + + def setUp(self): + super().setUp() + self.user = get_user_model()( + username='test', + email='test@test.com', + ) + self.user.save() + + def test_user_exists(self): + self.assertFalse(self.author_taxonomy._user_exists(100)) + self.assertTrue(self.author_taxonomy._user_exists(self.user.id)) + + def test_validate_object_tag(self): + object_id = 'object_id' + invalid_object_tag_1 = ObjectTag( + taxonomy=self.author_taxonomy, + object_id=object_id, + _value="author:100" + ) + invalid_object_tag_2 = ObjectTag( + object_id=object_id, + _value="author:100" + ) + valid_object_tag = ObjectTag( + taxonomy=self.author_taxonomy, + object_id=object_id, + _value=f"author:{self.user.id}" + ) + + # Invalid. User doesn't exist + self.assertFalse(self.author_taxonomy.validate_object_tag( + object_tag=invalid_object_tag_1, + check_taxonomy=False, + check_tag=False, + check_object=False, + )) + + # Invalid. Testing normal object validations + self.assertFalse(self.author_taxonomy.validate_object_tag( + object_tag=invalid_object_tag_2, + check_taxonomy=True, + check_tag=False, + check_object=False, + )) + + # Valid + self.assertTrue(self.author_taxonomy.validate_object_tag( + object_tag=valid_object_tag, + check_taxonomy=False, + check_tag=False, + check_object=False, + )) From 327bb58ae562aa29cd0211d4a2cbb3df3bcf4f38 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 7 Jul 2023 18:04:54 -0500 Subject: [PATCH 03/27] feat: Generic system defined object tags and Language object tag added --- .../tagging/migrations/0002_systemtaxonomy.py | 24 -- openedx_tagging/core/tagging/models.py | 40 +-- .../fixtures/taxonomies.yaml | 2 +- .../system_defined_taxonomies/object_tags.py | 172 ++++++++++ .../system_defined.py | 174 ----------- .../core/fixtures/system_defined.yaml | 10 + .../core/fixtures/tagging.yaml | 10 + .../openedx_tagging/core/tagging/test_api.py | 15 +- .../core/tagging/test_models.py | 1 + .../core/tagging/test_system_defined.py | 294 +++++++++++------- 10 files changed, 385 insertions(+), 357 deletions(-) delete mode 100644 openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py create mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py delete mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py create mode 100644 tests/openedx_tagging/core/fixtures/system_defined.yaml diff --git a/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py b/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py deleted file mode 100644 index 4c4ff74b6..000000000 --- a/openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.19 on 2023-07-04 20:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('oel_tagging', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='SystemTaxonomy', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('oel_tagging.taxonomy',), - ), - ] diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index f16338ee6..add3ac33a 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -19,13 +19,6 @@ # Will contain 0...TAXONOMY_MAX_DEPTH elements. Lineage = List[str] -class SystemDefinedTaxonomyTagsType(Enum): - """ - Tag types of system-defined taxonomies - """ - closed = 'closed' # Tags are created by fixtures/migrations - free_form = 'free_form' # Tags are free form - class Tag(models.Model): """ @@ -644,6 +637,9 @@ def _check_object(self): """ # Must have a valid object id/type: return self.object_id and self.object_type + + def _check_tag(self): + return self.value class ClosedObjectTag(OpenObjectTag): @@ -699,33 +695,3 @@ def is_valid(self, check_taxonomy=True, check_tag=True, check_object=True) -> bo register_object_tag_class(OpenObjectTag) register_object_tag_class(ClosedObjectTag) -class SystemTaxonomy(Taxonomy): - """ - System-defined taxonomies are not editable by normal users; they're defined by fixtures/migrations, and may have - dynamically-determined Tags and ObjectTags. - """ - - @property - def system_defined(self) -> bool: - """ - This is a system-defined taxonomy. - """ - return True - - @property - def is_visible(self) -> bool: - """ - Controls the visibility of this taxonomy to content authors. - - This value is static and must be implemented per each system-defined taxonomy. - """ - raise NotImplementedError - - @property - def creation_type (self) -> SystemDefinedTaxonomyTagsType: - """ - Controls the behaviour of the tags on this taxonomy - - This value is static and must be implemented per each system-defined taxonomy. - """ - raise NotImplementedError diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml index 692001f7e..12a2b43be 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml @@ -1,7 +1,7 @@ - model: oel_tagging.taxonomy pk: 1 fields: - name: Language + name: Languages description: ISO 639-1 Languages enabled: true required: true diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py new file mode 100644 index 000000000..d4cc50798 --- /dev/null +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py @@ -0,0 +1,172 @@ +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 ( + Tag, + Taxonomy, + OpenObjectTag, + ClosedObjectTag, +) +from openedx_tagging.core.tagging.registry import register_object_tag_class + + +class OpenSystemObjectTag(OpenObjectTag): + """ + Free-text object tag used on system-defined taxonomies + """ + + class Meta: + proxy = True + + @classmethod + def valid_for(cls, taxonomy: Taxonomy = None, **kwars): + """ + Returns True if the the taxonomy is system-defined + """ + return super().valid_for(taxonomy=taxonomy) and taxonomy.system_defined + +class ClosedSystemObjectTag(ClosedObjectTag): + """ + Object tags linked to a closed system-taxonomy + """ + + class Meta: + proxy = True + + @classmethod + def valid_for(cls, taxonomy: Taxonomy = None, tag: Tag = None, **kargs): + """ + Returns True if the the taxonomy is system-defined + """ + return super().valid_for(taxonomy=taxonomy, tag=tag) and taxonomy.system_defined + + +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 + + @classmethod + def valid_for(cls, taxonomy: Taxonomy = None, **kwars): + """ + Validates if has an associated Django model that has an Id + """ + if not super().valid_for(taxonomy=taxonomy) or not cls.tag_class_model: + return False + + # Verify that is a Django model + if not issubclass(cls.tag_class_model, models.Model): + return False + + # Verify that the model has 'id' field + try: + cls.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 + """ + + class Meta: + proxy = True + + @classmethod + def get_tags(cls, taxonomy: Taxonomy) -> List[Tag]: + """ + Returns a list of tags of the available languages. + """ + # TODO we need to overweite this + # tags = super().get_tags() + tags = taxonomy.tag_set.objects().all() + result = [] + available_langs = cls.get_available_languages() + for tag in tags: + if tag.external_id in available_langs: + result.append(tag) + return result + + @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) +register_object_tag_class(UserObjectTag) diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py b/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py deleted file mode 100644 index f25300c5a..000000000 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/system_defined.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import List - -from django.conf import settings -from django.contrib.auth import get_user_model - -from openedx_tagging.core.tagging.models import ObjectTag, Tag, SystemTaxonomy, SystemDefinedTaxonomyTagsType - -class IdBasedSystemTaxonomyMixin: - """ - Mixin for ID based taxonomies. - - Used to build tags with a prefix. E.x user: - """ - - prefix = "" - separator = ":" - - @property - def creation_type(self) -> SystemDefinedTaxonomyTagsType: - return SystemDefinedTaxonomyTagsType.free_form - - def get_id(self, tag_value): - """ - Get the id part of the tag - """ - return tag_value.split(self.separator)[1] - - def build_tag(self, id): - """ - Build the tag with the prefix and the separator - """ - return f"{self.prefix}{self.separator}{id}" - - def tag_object( - self, tags: List, object_id: str, object_type: str - ) -> List["ObjectTag"]: - """ - Overwriten the tag_object of the Taxonomy model - - Preprocess and build the new tags - """ - new_tags = [] - for tag in tags: - new_tags.append(self.build_tag(tag)) - - return super().tag_object(tags, object_id, object_type) - - -class LanguageTaxonomy(SystemTaxonomy): - """ - Language system-defined taxonomy. - - Tag creation: Hardcoded by fixtures - Visible: True - Allow multiple: False - Tags: ISO 639-1 Languages - - The tags are filtered and validated taking into account the - languages available in Django LANGUAGES settings var - """ - - class Meta: - proxy = True - - @property - def is_visible(self) -> bool: - raise True - - @property - def creation_type(self) -> SystemDefinedTaxonomyTagsType: - return SystemDefinedTaxonomyTagsType.closed - - def get_available_languages(self) -> 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 get_tags(self) -> List[Tag]: - """ - Returns a list of tags of the available languages. - """ - tags = super().get_tags() - result = [] - available_langs = self.get_available_languages() - for tag in tags: - if tag.external_id in available_langs: - result.append(tag) - return result - - def validate_object_tag( - self, - object_tag: "ObjectTag", - check_taxonomy=True, - check_tag=True, - check_object=True, - ) -> bool: - """ - Makes the normal object validation and validates if the - tag is an available language - """ - validation = super().validate_object_tag( - object_tag, - check_taxonomy, - check_tag, - check_object, - ) - if not validation: - return False - - available_langs = self.get_available_languages() - - # Must be linked to a tag and must be an available language - if not object_tag.tag or not object_tag.tag.external_id in available_langs: - return False - - return True - - -class AuthorTaxonomy(SystemTaxonomy, IdBasedSystemTaxonomyMixin): - """ - Author system-defined taxonomy - - Tag creation: Free form - Visible: False - Allow multiple: False - Tags: Id's of users in the form: author: - """ - - class Meta: - proxy = True - - prefix = "author" - - @property - def is_visible(self) -> bool: - raise False - - def validate_object_tag( - self, - object_tag: "ObjectTag", - check_taxonomy=True, - check_tag=True, - check_object=True, - ) -> bool: - """ - Makes the normal object validation and validates if the user exists - """ - validation = super().validate_object_tag( - object_tag, - check_taxonomy, - check_tag, - check_object, - ) - if not validation: - return False - - user_id = self.get_id(object_tag.value) - - # The user must exists - if not self._user_exists(user_id): - return False - - return True - - def _user_exists(self, user_id): - """ - Verify if an user exists - """ - return get_user_model().objects.filter(id=user_id).exists() diff --git a/tests/openedx_tagging/core/fixtures/system_defined.yaml b/tests/openedx_tagging/core/fixtures/system_defined.yaml new file mode 100644 index 000000000..3db3ec000 --- /dev/null +++ b/tests/openedx_tagging/core/fixtures/system_defined.yaml @@ -0,0 +1,10 @@ +- 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 \ No newline at end of file diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index a56ece81a..259d63d18 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -165,3 +165,13 @@ allow_multiple: false allow_free_text: false system_defined: true +- model: oel_tagging.taxonomy + pk: 3 + fields: + name: User Authors + description: Allows tags for any User on the instance. + enabled: true + required: false + allow_multiple: false + allow_free_text: true + system_defined: true \ No newline at end of file diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 798b4826b..b7d7ff7ef 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -47,7 +47,12 @@ 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] + assert enabled == [ + tax1, + self.taxonomy, + self.system_taxonomy, + self.user_system_taxonomy + ] assert str(enabled[0]) == f" ({tax1.id}) Enabled" assert str(enabled[1]) == " (1) Life on Earth" assert str(enabled[2]) == " (2) System Languages" @@ -59,7 +64,13 @@ def test_get_taxonomies(self): with self.assertNumQueries(1): both = list(tagging_api.get_taxonomies(enabled=None)) - assert both == [tax2, tax1, self.taxonomy, self.system_taxonomy] + assert both == [ + tax2, + tax1, + self.taxonomy, + self.system_taxonomy, + self.user_system_taxonomy + ] def test_get_tags(self): self.setup_tag_depths() diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 65af41eaa..4e78a8f4a 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -24,6 +24,7 @@ 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.archaea = get_tag("Archaea") self.archaebacteria = get_tag("Archaebacteria") self.bacteria = get_tag("Bacteria") diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index 94b934223..d0abf3bfa 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -1,17 +1,20 @@ -""" Test the System-defined taxonomies """ - -import ddt +""" Test the System-defined taxonomies and object tags """ 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.models import Taxonomy, ObjectTag, Tag -from openedx_tagging.core.tagging.system_defined_taxonomies.system_defined import ( - LanguageTaxonomy, - AuthorTaxonomy +from openedx_tagging.core.tagging.system_defined_taxonomies.object_tags import ( + OpenSystemObjectTag, + ClosedSystemObjectTag, + ModelObjectTag, + UserObjectTag, + LanguageObjectTag, ) +from openedx_tagging.core.tagging.models import Tag, Taxonomy + +from .test_models import TestTagTaxonomyMixin -fixtures_path = "openedx_tagging/core/tagging/system_defined_taxonomies/fixtures" test_languages = [ ('en', 'English'), ('az', 'Azerbaijani'), @@ -20,153 +23,206 @@ ('zu', 'Zulu'), ] -class TestSystemDefinedTaxonomyMixin: + +class EmptyTestClass: """ - Mixin used on system-defined taxonomy tests + Empty class used for testing """ - fixtures = [f"{fixtures_path}/taxonomies.yaml"] - def setUp(self): - super().setUp() - temp_taxonomy = Taxonomy.objects.get(pk=1) - self.language_taxonomy = LanguageTaxonomy() - self.language_taxonomy.__dict__ = temp_taxonomy.__dict__ - temp_taxonomy = Taxonomy.objects.get(pk=2) - self.author_taxonomy = AuthorTaxonomy() - self.author_taxonomy.__dict__ = temp_taxonomy.__dict__ +class EmptyTestModel(models.Model): + """ + Model used for testing + """ + mi_id = models.AutoField(primary_key=True) + class Meta: + managed = False + app_label = 'oel_tagging' -@ddt.ddt -@override_settings(LANGUAGES=test_languages) -class TestLanguageTaxonomy(TestSystemDefinedTaxonomyMixin, TestCase): + +class EmptyObjectTag(ModelObjectTag): """ - Test the Language Taxonomy + Model Object tag used for testing """ + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' - def setUp(self): - super().setUp() - 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") + tag_class_model = EmptyTestClass - fixtures = ( - TestSystemDefinedTaxonomyMixin.fixtures - + [f"{fixtures_path}/language_taxonomy.yaml"] - ) - def test_fixture(self): - self.assertEqual(self.language_taxonomy.name, 'Language') - for lang_code, lang_value in test_languages: - lang = self.language_taxonomy.tag_set.get(external_id=lang_code) - self.assertEqual(lang.value, lang_value) +class EmptyModelObjectTag(ModelObjectTag): + """ + Model Object tag used for testing + """ + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' - def test_get_available_languages(self): - langs = self.language_taxonomy.get_available_languages() - self.assertEqual(sorted(langs), self.expected_langs_ids) + tag_class_model = EmptyTestModel - def test_get_tags(self): - tags = self.language_taxonomy.get_tags() - for tag in tags: - self.assertIn(tag.value, self.expected_langs_values) - self.assertEqual(tag.annotated_field, 0) # Checking depth - - def test_validate_object_tag(self): - object_id = 'object_id' - invalid_object_tag_1 = ObjectTag( - taxonomy=self.language_taxonomy, - object_id=object_id, - tag=self.spanish_tag, + +class TestSystemDefinedObjectTags(TestTagTaxonomyMixin, TestCase): + """ + Test for generic system defined object tags + """ + def test_open_valid_for(self): + #Valid + assert OpenSystemObjectTag.valid_for(taxonomy=self.user_system_taxonomy) + + # Not open system taxonomy + assert not OpenSystemObjectTag.valid_for(taxonomy=self.system_taxonomy) + + # Not system taxonomy + assert not OpenSystemObjectTag.valid_for(taxonomy=self.taxonomy) + + def test_closed_valid_for(self): + #Valid + assert ClosedSystemObjectTag.valid_for(taxonomy=self.system_taxonomy, tag=self.archaea) + + # Not closed system taxonomy + assert not ClosedSystemObjectTag.valid_for(taxonomy=self.user_system_taxonomy, tag=self.archaea) + + # Not system taxonomy + assert not ClosedSystemObjectTag.valid_for(taxonomy=self.taxonomy, tag=self.archaea) + + def test_model_valid_for(self): + # Without associated model + assert not ModelObjectTag.valid_for(self.user_system_taxonomy) + + # Associated class is not a Django model + assert not EmptyObjectTag.valid_for(self.user_system_taxonomy) + + # Associated model has not 'id' field + assert not EmptyModelObjectTag.valid_for(self.user_system_taxonomy) + + #Valid + assert UserObjectTag.valid_for(self.user_system_taxonomy) + + def test_model_is_valid(self): + user = get_user_model()( + username='username', + email='email' ) - invalid_object_tag_2 = ObjectTag( - object_id=object_id, + user.save() + valid_object_tag = UserObjectTag( + taxonomy=self.user_system_taxonomy, + object_id='id 1', + object_type='object', + value=user.id, ) - valid_object_tag = ObjectTag( - taxonomy=self.language_taxonomy, - object_id=object_id, - tag=self.english_tag, + invalid_object_tag_1 = UserObjectTag( + taxonomy=self.user_system_taxonomy, + object_id='id 2', + object_type='object', + value='user_id', + ) + invalid_object_tag_2 = UserObjectTag( + taxonomy=self.user_system_taxonomy, + object_id='id 2', + object_type='object', + value='10000', + ) + invalid_object_tag_3 = UserObjectTag( + taxonomy=self.user_system_taxonomy, + object_id='id 3', + object_type='object', ) - # Invalid. Language is not available - self.language_taxonomy.validate_object_tag( - object_tag=invalid_object_tag_1, - check_taxonomy=False, - check_tag=False, - check_object=False, + # Invalid user id + assert not invalid_object_tag_1.is_valid( + check_taxonomy=True, + check_object=True, + check_tag=True, ) - # Invalid. Testing normal object validations - self.language_taxonomy.validate_object_tag( - object_tag=invalid_object_tag_2, + # User don't exits + assert not invalid_object_tag_2.is_valid( check_taxonomy=True, - check_tag=False, - check_object=False, + check_object=True, + check_tag=True, + ) + + # Testing parent class validations + assert not invalid_object_tag_3.is_valid( + check_taxonomy=True, + check_object=True, + check_tag=True, ) # Valid - self.language_taxonomy.validate_object_tag( - object_tag=valid_object_tag, - check_taxonomy=False, - check_tag=False, - check_object=False, + assert valid_object_tag.is_valid( + check_taxonomy=True, + check_object=True, + check_tag=True, ) -class TestAuthorTaxonomy(TestSystemDefinedTaxonomyMixin, TestCase): +@override_settings(LANGUAGES=test_languages) +class TestLanguageObjectClass(TestCase): """ - Test the Author Taxonomy + Test for Language object class """ + fixtures = [ + "tests/openedx_tagging/core/fixtures/system_defined.yaml", + "openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml" + ] + def setUp(self): super().setUp() - self.user = get_user_model()( - username='test', - email='test@test.com', + 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, ) - self.user.save() - - def test_user_exists(self): - self.assertFalse(self.author_taxonomy._user_exists(100)) - self.assertTrue(self.author_taxonomy._user_exists(self.user.id)) - - def test_validate_object_tag(self): - object_id = 'object_id' - invalid_object_tag_1 = ObjectTag( - taxonomy=self.author_taxonomy, - object_id=object_id, - _value="author:100" + + invalid_onbject_tag_1 = LanguageObjectTag( + taxonomy=self.taxonomy, + object_id='id 2', + object_type='object', + tag=self.spanish_tag, ) - invalid_object_tag_2 = ObjectTag( - object_id=object_id, - _value="author:100" + + invalid_onbject_tag_2 = LanguageObjectTag( + taxonomy=self.taxonomy, + object_id='id 2', + object_type='object', ) - valid_object_tag = ObjectTag( - taxonomy=self.author_taxonomy, - object_id=object_id, - _value=f"author:{self.user.id}" + + # Tag is not in available languages + assert not invalid_onbject_tag_1.is_valid( + check_taxonomy=True, + check_object=True, + check_tag=True, ) - # Invalid. User doesn't exist - self.assertFalse(self.author_taxonomy.validate_object_tag( - object_tag=invalid_object_tag_1, - check_taxonomy=False, - check_tag=False, - check_object=False, - )) - - # Invalid. Testing normal object validations - self.assertFalse(self.author_taxonomy.validate_object_tag( - object_tag=invalid_object_tag_2, + # Testing parent class validations + assert not invalid_onbject_tag_2.is_valid( check_taxonomy=True, - check_tag=False, - check_object=False, - )) + check_object=True, + check_tag=True, + ) # Valid - self.assertTrue(self.author_taxonomy.validate_object_tag( - object_tag=valid_object_tag, - check_taxonomy=False, - check_tag=False, - check_object=False, - )) + assert valid_object_tag.is_valid( + check_taxonomy=True, + check_object=True, + check_tag=True, + ) From 58fc1e8cbaad3522b7d71cc9a2c9bd1b67f8c94c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 10 Jul 2023 14:42:17 -0500 Subject: [PATCH 04/27] chore: get_tags_query_set added to LanguageObjectTag --- .../system_defined_taxonomies/object_tags.py | 20 ++++++------------- .../core/tagging/test_system_defined.py | 6 +++++- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py index d4cc50798..783d60cee 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py @@ -125,19 +125,12 @@ class Meta: proxy = True @classmethod - def get_tags(cls, taxonomy: Taxonomy) -> List[Tag]: - """ - Returns a list of tags of the available languages. - """ - # TODO we need to overweite this - # tags = super().get_tags() - tags = taxonomy.tag_set.objects().all() - result = [] - available_langs = cls.get_available_languages() - for tag in tags: - if tag.external_id in available_langs: - result.append(tag) - return result + 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]: @@ -150,7 +143,6 @@ def _get_available_languages(cls) -> List[str]: langs.add(django_lang[0].split('-')[0]) return langs - def _check_tag(self): """ Validates if the language tag is on the available languages diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index d0abf3bfa..a3aa53c6d 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -184,7 +184,6 @@ 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, @@ -226,3 +225,8 @@ def test_is_valid(self): 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) From 9e7ed4e5e127a8008c8c0f4f72abf942183b89bf Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 10 Jul 2023 16:26:35 -0500 Subject: [PATCH 05/27] chore: Adding _validate_taxonomy function to all system defined object tags --- .../system_defined_taxonomies/object_tags.py | 46 ++++++++++++-- .../core/tagging/test_system_defined.py | 63 ++++++++++++++++--- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py index 783d60cee..b80677bdf 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py @@ -15,7 +15,41 @@ from openedx_tagging.core.tagging.registry import register_object_tag_class -class OpenSystemObjectTag(OpenObjectTag): +class SystemDefinedObjectTagMixin: + """ + Mixing for ObjectTags used on all system defined taxonomies + + `system_defined_taxonomy_name``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. + """ + + system_defined_taxonomy_name = None + + @classmethod + def _validate_taxonomy(cls, 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.name == cls.system_defined_taxonomy_name + ) + + +class OpenSystemObjectTag(OpenObjectTag, SystemDefinedObjectTagMixin): """ Free-text object tag used on system-defined taxonomies """ @@ -28,9 +62,10 @@ def valid_for(cls, taxonomy: Taxonomy = None, **kwars): """ Returns True if the the taxonomy is system-defined """ - return super().valid_for(taxonomy=taxonomy) and taxonomy.system_defined + return super().valid_for(taxonomy=taxonomy) and cls._validate_taxonomy(taxonomy) + -class ClosedSystemObjectTag(ClosedObjectTag): +class ClosedSystemObjectTag(ClosedObjectTag, SystemDefinedObjectTagMixin): """ Object tags linked to a closed system-taxonomy """ @@ -43,7 +78,7 @@ def valid_for(cls, taxonomy: Taxonomy = None, tag: Tag = None, **kargs): """ Returns True if the the taxonomy is system-defined """ - return super().valid_for(taxonomy=taxonomy, tag=tag) and taxonomy.system_defined + return super().valid_for(taxonomy=taxonomy, tag=tag) and cls._validate_taxonomy(taxonomy) class ModelObjectTag(OpenSystemObjectTag): @@ -121,6 +156,8 @@ class LanguageObjectTag(ClosedSystemObjectTag): languages available in Django LANGUAGES settings var """ + system_defined_taxonomy_name = "System Languages" + class Meta: proxy = True @@ -161,4 +198,3 @@ def _check_tag(self): # Register the object tag classes in reverse order for how we want them considered register_object_tag_class(LanguageObjectTag) -register_object_tag_class(UserObjectTag) diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index a3aa53c6d..2f84ac077 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -43,8 +43,11 @@ class Meta: class EmptyObjectTag(ModelObjectTag): """ - Model Object tag used for testing + Model ObjectTag used for testing """ + + system_defined_taxonomy_name = "User Authors" + class Meta: proxy = True managed = False @@ -55,8 +58,11 @@ class Meta: class EmptyModelObjectTag(ModelObjectTag): """ - Model Object tag used for testing + Model ObjectTag used for testing """ + + system_defined_taxonomy_name = "User Authors" + class Meta: proxy = True managed = False @@ -65,29 +71,68 @@ class Meta: tag_class_model = EmptyTestModel +class TestOpenObjectTag(OpenSystemObjectTag): + """ + Open ObjectTag used for testing + """ + + system_defined_taxonomy_name = "User Authors" + + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' + + +class TestClosedObjectTag(ClosedSystemObjectTag): + """ + Closed ObjectTag used for testing + """ + + system_defined_taxonomy_name = "System Languages" + + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' + + +class TestUserObjectTag(UserObjectTag): + """ + User ObjectTag used for testing + """ + + system_defined_taxonomy_name = "User Authors" + + class Meta: + proxy = True + managed = False + app_label = 'oel_tagging' + + class TestSystemDefinedObjectTags(TestTagTaxonomyMixin, TestCase): """ Test for generic system defined object tags """ def test_open_valid_for(self): #Valid - assert OpenSystemObjectTag.valid_for(taxonomy=self.user_system_taxonomy) + assert TestOpenObjectTag.valid_for(taxonomy=self.user_system_taxonomy) # Not open system taxonomy - assert not OpenSystemObjectTag.valid_for(taxonomy=self.system_taxonomy) + assert not TestOpenObjectTag.valid_for(taxonomy=self.system_taxonomy) # Not system taxonomy - assert not OpenSystemObjectTag.valid_for(taxonomy=self.taxonomy) + assert not TestOpenObjectTag.valid_for(taxonomy=self.taxonomy) def test_closed_valid_for(self): #Valid - assert ClosedSystemObjectTag.valid_for(taxonomy=self.system_taxonomy, tag=self.archaea) + assert TestClosedObjectTag.valid_for(taxonomy=self.system_taxonomy, tag=self.archaea) # Not closed system taxonomy - assert not ClosedSystemObjectTag.valid_for(taxonomy=self.user_system_taxonomy, tag=self.archaea) + assert not TestClosedObjectTag.valid_for(taxonomy=self.user_system_taxonomy, tag=self.archaea) # Not system taxonomy - assert not ClosedSystemObjectTag.valid_for(taxonomy=self.taxonomy, tag=self.archaea) + assert not TestClosedObjectTag.valid_for(taxonomy=self.taxonomy, tag=self.archaea) def test_model_valid_for(self): # Without associated model @@ -100,7 +145,7 @@ def test_model_valid_for(self): assert not EmptyModelObjectTag.valid_for(self.user_system_taxonomy) #Valid - assert UserObjectTag.valid_for(self.user_system_taxonomy) + assert TestUserObjectTag.valid_for(self.user_system_taxonomy) def test_model_is_valid(self): user = get_user_model()( From 899d61f0bdd30e589715052afec8721e0bb8c7ce Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 11 Jul 2023 18:19:59 -0500 Subject: [PATCH 06/27] chore: Updating system_defined_taxonomy_id --- .../fixtures/language_taxonomy.yaml | 0 .../core/tagging/fixtures/system_defined.yaml | 9 +++++++++ .../fixtures/taxonomies.yaml | 18 ------------------ .../system_defined_taxonomies/object_tags.py | 19 +++++++++++++++---- .../core/tagging/test_system_defined.py | 12 ++++++------ 5 files changed, 30 insertions(+), 28 deletions(-) rename openedx_tagging/core/tagging/{system_defined_taxonomies => }/fixtures/language_taxonomy.yaml (100%) create mode 100644 openedx_tagging/core/tagging/fixtures/system_defined.yaml delete mode 100644 openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml similarity index 100% rename from openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml rename to openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml diff --git a/openedx_tagging/core/tagging/fixtures/system_defined.yaml b/openedx_tagging/core/tagging/fixtures/system_defined.yaml new file mode 100644 index 000000000..727d625d7 --- /dev/null +++ b/openedx_tagging/core/tagging/fixtures/system_defined.yaml @@ -0,0 +1,9 @@ +- model: oel_tagging.taxonomy + pk: 1 + fields: + name: Languages + description: ISO 639-1 Languages. Allows tags for any language configured for use on the instance + enabled: true + required: true + allow_multiple: false + allow_free_text: false diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml b/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml deleted file mode 100644 index 12a2b43be..000000000 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/taxonomies.yaml +++ /dev/null @@ -1,18 +0,0 @@ -- model: oel_tagging.taxonomy - pk: 1 - fields: - name: Languages - description: ISO 639-1 Languages - enabled: true - required: true - allow_multiple: false - allow_free_text: false -- model: oel_tagging.taxonomy - pk: 2 - fields: - name: Author - description: User Ids of the platform - enabled: true - required: true - allow_multiple: false - allow_free_text: true diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py index b80677bdf..191eb3f9c 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py @@ -1,3 +1,7 @@ +""" +ObjectTags for System-defined Taxonomies +""" +from enum import Enum from typing import List from django.conf import settings @@ -15,11 +19,18 @@ 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_name``is used to connect the + `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: @@ -34,7 +45,7 @@ class SystemDefinedObjectTagMixin: and system defined taxonomy as hardcoded. """ - system_defined_taxonomy_name = None + system_defined_taxonomy_id = None @classmethod def _validate_taxonomy(cls, taxonomy: Taxonomy = None): @@ -45,7 +56,7 @@ def _validate_taxonomy(cls, taxonomy: Taxonomy = None): return ( bool(taxonomy) and taxonomy.system_defined and - taxonomy.name == cls.system_defined_taxonomy_name + taxonomy.id == cls.system_defined_taxonomy_id ) @@ -156,7 +167,7 @@ class LanguageObjectTag(ClosedSystemObjectTag): languages available in Django LANGUAGES settings var """ - system_defined_taxonomy_name = "System Languages" + system_defined_taxonomy_id = SystemDefinedIds.LanguageTaxonomy class Meta: proxy = True diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index 2f84ac077..2996596ee 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -46,7 +46,7 @@ class EmptyObjectTag(ModelObjectTag): Model ObjectTag used for testing """ - system_defined_taxonomy_name = "User Authors" + system_defined_taxonomy_id = 3 class Meta: proxy = True @@ -61,7 +61,7 @@ class EmptyModelObjectTag(ModelObjectTag): Model ObjectTag used for testing """ - system_defined_taxonomy_name = "User Authors" + system_defined_taxonomy_id = 3 class Meta: proxy = True @@ -76,7 +76,7 @@ class TestOpenObjectTag(OpenSystemObjectTag): Open ObjectTag used for testing """ - system_defined_taxonomy_name = "User Authors" + system_defined_taxonomy_id = 3 class Meta: proxy = True @@ -89,7 +89,7 @@ class TestClosedObjectTag(ClosedSystemObjectTag): Closed ObjectTag used for testing """ - system_defined_taxonomy_name = "System Languages" + system_defined_taxonomy_id = 2 class Meta: proxy = True @@ -102,7 +102,7 @@ class TestUserObjectTag(UserObjectTag): User ObjectTag used for testing """ - system_defined_taxonomy_name = "User Authors" + system_defined_taxonomy_id = 3 class Meta: proxy = True @@ -214,7 +214,7 @@ class TestLanguageObjectClass(TestCase): fixtures = [ "tests/openedx_tagging/core/fixtures/system_defined.yaml", - "openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml" + "openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml" ] def setUp(self): From 1e68687be4c2100f5b64f3a5bcc4bcea7c740571 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 12 Jul 2023 13:57:06 -0500 Subject: [PATCH 07/27] refactor: consolidates ObjectTag validation --- MANIFEST.in | 2 +- .../core/tagging/fixtures/system_defined.yaml | 2 + .../commands/build_language_fixture.py | 2 +- openedx_tagging/core/tagging/models.py | 3 - .../system_defined_taxonomies/object_tags.py | 51 +++--- .../core/fixtures/system_defined.yaml | 2 +- .../core/fixtures/tagging.yaml | 2 +- .../core/tagging/test_system_defined.py | 163 +++++++----------- 8 files changed, 92 insertions(+), 135 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8cbdc3a65..0a2abd084 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,4 @@ include LICENSE.txt include README.rst include requirements/base.in recursive-include openedx_learning *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py -recursive-include openedx_tagging *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py +recursive-include openedx_tagging *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py *.yaml diff --git a/openedx_tagging/core/tagging/fixtures/system_defined.yaml b/openedx_tagging/core/tagging/fixtures/system_defined.yaml index 727d625d7..c5737b645 100644 --- a/openedx_tagging/core/tagging/fixtures/system_defined.yaml +++ b/openedx_tagging/core/tagging/fixtures/system_defined.yaml @@ -7,3 +7,5 @@ required: true allow_multiple: false allow_free_text: false + system_defined: true + visible_to_authors: true diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index fef118a5c..0db724950 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand endpoint = "https://pkgstore.datahub.io/core/language-codes/language-codes_json/data/97607046542b532c395cf83df5185246/language-codes_json.json" -output = "./openedx_tagging/core/tagging/system_defined_taxonomies/fixtures/language_taxonomy.yaml" +output = "./openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml" class Command(BaseCommand): diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index add3ac33a..3bd42bfa7 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -637,9 +637,6 @@ def _check_object(self): """ # Must have a valid object id/type: return self.object_id and self.object_type - - def _check_tag(self): - return self.value class ClosedObjectTag(OpenObjectTag): diff --git a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py index 191eb3f9c..a54bf05cf 100644 --- a/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py +++ b/openedx_tagging/core/tagging/system_defined_taxonomies/object_tags.py @@ -11,7 +11,6 @@ from openedx_tagging.core.tagging.models import ( - Tag, Taxonomy, OpenObjectTag, ClosedObjectTag, @@ -42,21 +41,20 @@ class SystemDefinedObjectTagMixin: taxonomy but with different objects. Using this approach makes the connection between the ObjectTag - and system defined taxonomy as hardcoded. + and system defined taxonomy as hardcoded and can't be changed. """ system_defined_taxonomy_id = None - @classmethod - def _validate_taxonomy(cls, taxonomy: Taxonomy = 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 == cls.system_defined_taxonomy_id + bool(taxonomy) and + taxonomy.system_defined and + taxonomy.id == self.system_defined_taxonomy_id ) @@ -68,12 +66,11 @@ class OpenSystemObjectTag(OpenObjectTag, SystemDefinedObjectTagMixin): class Meta: proxy = True - @classmethod - def valid_for(cls, taxonomy: Taxonomy = None, **kwars): - """ - Returns True if the the taxonomy is system-defined - """ - return super().valid_for(taxonomy=taxonomy) and cls._validate_taxonomy(taxonomy) + def _check_taxonomy(self): + return ( + super()._check_taxonomy() and + self._check_system_taxonomy(self.taxonomy) + ) class ClosedSystemObjectTag(ClosedObjectTag, SystemDefinedObjectTagMixin): @@ -84,12 +81,11 @@ class ClosedSystemObjectTag(ClosedObjectTag, SystemDefinedObjectTagMixin): class Meta: proxy = True - @classmethod - def valid_for(cls, taxonomy: Taxonomy = None, tag: Tag = None, **kargs): - """ - Returns True if the the taxonomy is system-defined - """ - return super().valid_for(taxonomy=taxonomy, tag=tag) and cls._validate_taxonomy(taxonomy) + def _check_taxonomy(self): + return ( + super()._check_taxonomy() and + self._check_system_taxonomy(self.taxonomy) + ) class ModelObjectTag(OpenSystemObjectTag): @@ -104,21 +100,24 @@ class Meta: tag_class_model = None - @classmethod - def valid_for(cls, taxonomy: Taxonomy = None, **kwars): + + def _check_taxonomy(self): """ Validates if has an associated Django model that has an Id """ - if not super().valid_for(taxonomy=taxonomy) or not cls.tag_class_model: + if not super()._check_taxonomy(): return False - + + if not self.tag_class_model: + return False + # Verify that is a Django model - if not issubclass(cls.tag_class_model, models.Model): + if not issubclass(self.tag_class_model, models.Model): return False # Verify that the model has 'id' field try: - cls.tag_class_model._meta.get_field('id') + self.tag_class_model._meta.get_field('id') except FieldDoesNotExist: return False @@ -167,7 +166,7 @@ class LanguageObjectTag(ClosedSystemObjectTag): languages available in Django LANGUAGES settings var """ - system_defined_taxonomy_id = SystemDefinedIds.LanguageTaxonomy + system_defined_taxonomy_id = SystemDefinedIds.LanguageTaxonomy.value class Meta: proxy = True diff --git a/tests/openedx_tagging/core/fixtures/system_defined.yaml b/tests/openedx_tagging/core/fixtures/system_defined.yaml index 3db3ec000..9fade1907 100644 --- a/tests/openedx_tagging/core/fixtures/system_defined.yaml +++ b/tests/openedx_tagging/core/fixtures/system_defined.yaml @@ -7,4 +7,4 @@ required: false allow_multiple: false allow_free_text: false - system_defined: true \ No newline at end of file + system_defined: true diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index 259d63d18..4174fb4f5 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -174,4 +174,4 @@ required: false allow_multiple: false allow_free_text: true - system_defined: true \ No newline at end of file + system_defined: true diff --git a/tests/openedx_tagging/core/tagging/test_system_defined.py b/tests/openedx_tagging/core/tagging/test_system_defined.py index 2996596ee..9de7fb3c2 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined.py @@ -1,12 +1,12 @@ """ 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 ( - OpenSystemObjectTag, - ClosedSystemObjectTag, ModelObjectTag, UserObjectTag, LanguageObjectTag, @@ -41,7 +41,7 @@ class Meta: app_label = 'oel_tagging' -class EmptyObjectTag(ModelObjectTag): +class EmptyModelObjectTag(ModelObjectTag): """ Model ObjectTag used for testing """ @@ -53,10 +53,8 @@ class Meta: managed = False app_label = 'oel_tagging' - tag_class_model = EmptyTestClass - -class EmptyModelObjectTag(ModelObjectTag): +class NotDjangoModelObjectTag(ModelObjectTag): """ Model ObjectTag used for testing """ @@ -68,12 +66,12 @@ class Meta: managed = False app_label = 'oel_tagging' - tag_class_model = EmptyTestModel + tag_class_model = EmptyTestClass -class TestOpenObjectTag(OpenSystemObjectTag): +class NotIdModelObjectTag(ModelObjectTag): """ - Open ObjectTag used for testing + Model ObjectTag used for testing """ system_defined_taxonomy_id = 3 @@ -83,18 +81,7 @@ class Meta: managed = False app_label = 'oel_tagging' - -class TestClosedObjectTag(ClosedSystemObjectTag): - """ - Closed ObjectTag used for testing - """ - - system_defined_taxonomy_id = 2 - - class Meta: - proxy = True - managed = False - app_label = 'oel_tagging' + tag_class_model = EmptyTestModel class TestUserObjectTag(UserObjectTag): @@ -110,100 +97,72 @@ class Meta: app_label = 'oel_tagging' +@ddt.ddt class TestSystemDefinedObjectTags(TestTagTaxonomyMixin, TestCase): """ Test for generic system defined object tags """ - def test_open_valid_for(self): - #Valid - assert TestOpenObjectTag.valid_for(taxonomy=self.user_system_taxonomy) - - # Not open system taxonomy - assert not TestOpenObjectTag.valid_for(taxonomy=self.system_taxonomy) - - # Not system taxonomy - assert not TestOpenObjectTag.valid_for(taxonomy=self.taxonomy) - - def test_closed_valid_for(self): - #Valid - assert TestClosedObjectTag.valid_for(taxonomy=self.system_taxonomy, tag=self.archaea) - - # Not closed system taxonomy - assert not TestClosedObjectTag.valid_for(taxonomy=self.user_system_taxonomy, tag=self.archaea) - - # Not system taxonomy - assert not TestClosedObjectTag.valid_for(taxonomy=self.taxonomy, tag=self.archaea) - - def test_model_valid_for(self): - # Without associated model - assert not ModelObjectTag.valid_for(self.user_system_taxonomy) - - # Associated class is not a Django model - assert not EmptyObjectTag.valid_for(self.user_system_taxonomy) - - # Associated model has not 'id' field - assert not EmptyModelObjectTag.valid_for(self.user_system_taxonomy) - - #Valid - assert TestUserObjectTag.valid_for(self.user_system_taxonomy) - - def test_model_is_valid(self): - user = get_user_model()( - username='username', - email='email' - ) - user.save() - valid_object_tag = UserObjectTag( - taxonomy=self.user_system_taxonomy, - object_id='id 1', - object_type='object', - value=user.id, - ) - invalid_object_tag_1 = UserObjectTag( - taxonomy=self.user_system_taxonomy, - object_id='id 2', - object_type='object', - value='user_id', - ) - invalid_object_tag_2 = UserObjectTag( - taxonomy=self.user_system_taxonomy, - object_id='id 2', - object_type='object', - value='10000', - ) - invalid_object_tag_3 = UserObjectTag( + 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 3', + object_id='id', object_type='object', + value=value, ) - # Invalid user id - assert not invalid_object_tag_1.is_valid( + result = object_tag.is_valid( check_taxonomy=True, check_object=True, check_tag=True, ) - # User don't exits - assert not invalid_object_tag_2.is_valid( - check_taxonomy=True, - check_object=True, - check_tag=True, - ) - - # Testing parent class validations - assert not invalid_object_tag_3.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, - ) + self.assertEqual(result, assert_value) @override_settings(LANGUAGES=test_languages) From 9e5d854cedbb15b13e9b0c949c321df6e30012c9 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 19 Jul 2023 18:29:32 -0500 Subject: [PATCH 08/27] 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 000000000..d4c094254 --- /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 3bd42bfa7..eccf40f20 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 a54bf05cf..000000000 --- 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 9fade1907..000000000 --- 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 4174fb4f5..90bca3978 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 b7d7ff7ef..c6bb4805e 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 4e78a8f4a..4197f9d16 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 9de7fb3c2..000000000 --- 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) From 9b72fec5450ab5d6823d9ab14bf8c68774b49b05 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jul 2023 02:22:05 +0930 Subject: [PATCH 09/27] style: linting and formatting --- .../commands/build_language_fixture.py | 9 +- .../tagging/migrations/0003_system_defined.py | 55 +++++----- openedx_tagging/core/tagging/models.py | 101 +++++------------- .../openedx_tagging/core/tagging/test_api.py | 2 +- .../core/tagging/test_models.py | 86 +++++++-------- 5 files changed, 100 insertions(+), 153 deletions(-) diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index 0db724950..5735ac167 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -5,16 +5,16 @@ This function is intended to be used only once, but can be edited in the future if more data needs to be added to the fixture. """ -import urllib.request import json +import urllib.request from django.core.management.base import BaseCommand endpoint = "https://pkgstore.datahub.io/core/language-codes/language-codes_json/data/97607046542b532c395cf83df5185246/language-codes_json.json" output = "./openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml" -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, **options): json_data = self.download_json() self.build_fixture(json_data) @@ -26,7 +26,7 @@ def download_json(self): def build_fixture(self, json_data): tag_pk = 1 - with open(output, 'w') as output_file: + with open(output, "w") as output_file: for lang_data in json_data: lang_value = self.get_lang_value(lang_data) lang_code = lang_data["alpha2"] @@ -43,6 +43,5 @@ def get_lang_value(self, lang_data): """ Gets the lang value. Some languages has many values. """ - lang_list = lang_data["English"].split(';') + lang_list = lang_data["English"].split(";") return lang_list[0] - \ No newline at end of file diff --git a/openedx_tagging/core/tagging/migrations/0003_system_defined.py b/openedx_tagging/core/tagging/migrations/0003_system_defined.py index d4c094254..99637a49c 100644 --- a/openedx_tagging/core/tagging/migrations/0003_system_defined.py +++ b/openedx_tagging/core/tagging/migrations/0003_system_defined.py @@ -4,54 +4,49 @@ class Migration(migrations.Migration): - dependencies = [ - ('oel_tagging', '0002_auto_20230718_2026'), + ("oel_tagging", "0002_auto_20230718_2026"), ] operations = [ migrations.CreateModel( - name='SystemDefinedTaxonomy', - fields=[ - ], + name="SystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.taxonomy',), + bases=("oel_tagging.taxonomy",), ), migrations.CreateModel( - name='LanguageTaxonomy', - fields=[ - ], + name="LanguageTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.systemdefinedtaxonomy',), + bases=("oel_tagging.systemdefinedtaxonomy",), ), migrations.CreateModel( - name='ModelSystemDefinedTaxonomy', - fields=[ - ], + name="ModelSystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.systemdefinedtaxonomy',), + bases=("oel_tagging.systemdefinedtaxonomy",), ), migrations.CreateModel( - name='UserSystemDefinedTaxonomy', - fields=[ - ], + name="UserSystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.modelsystemdefinedtaxonomy',), + bases=("oel_tagging.modelsystemdefinedtaxonomy",), ), ] diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index eccf40f20..0f0494d6a 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -245,7 +245,7 @@ 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()` @@ -414,7 +414,7 @@ def tag_object( ) 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}") @@ -652,48 +652,37 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": class SystemDefinedTaxonomy(Taxonomy): - class Meta: proxy = True - def _check_taxonomy( - self, - object_tag: ObjectTag - ) -> bool: + def _check_taxonomy(self, object_tag: ObjectTag) -> bool: """ Returns True if this is a system defined taxonomy """ - return ( - super()._check_taxonomy(object_tag) - and self.system_defined - ) + return super()._check_taxonomy(object_tag) and self.system_defined class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): """ 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. + 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`). + (e.g. `username`). The subclasses can override this behavior, to choose the right field. - When an ObjectTag is created with an existing Tag, + When an ObjectTag is created with an existing Tag, the Tag is re-synchronized with its instance. """ class Meta: proxy = True - def __init__( - self, - *args: Any, - **kwargs: Any - ) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """ Checks if the `tag_class_model` is correct """ @@ -707,11 +696,8 @@ def tag_class_model(self) -> Type: class referenced by these object tags. """ raise NotImplementedError - - def _check_instance( - self, - object_tag: ObjectTag - ) -> bool: + + def _check_instance(self, object_tag: ObjectTag) -> bool: """ Returns True if the instance exists @@ -720,22 +706,13 @@ def _check_instance( intance_id = object_tag.tag.external_id return self.tag_class_model.objects.filter(pk=intance_id).exists() - def _check_tag( - self, - object_tag: ObjectTag - ) -> bool: + def _check_tag(self, object_tag: ObjectTag) -> bool: """ Returns True if pass the instance validation checks """ - return ( - super()._check_tag(object_tag) - and self._check_instance(object_tag) - ) - - def _get_external_id_from_instance( - self, - instance - ) -> str: + 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 @@ -743,17 +720,14 @@ def _get_external_id_from_instance( """ return str(instance.pk) - def _get_value_from_instance( - self, - instance - ) -> str: + def _get_value_from_instance(self, instance) -> str: """ Returns the tag value from the instance Subclasses can override this method to get the value from their own models. """ return str(instance.pk) - + def _get_instance(self, pk): """ Gets the instance from `pk` @@ -762,7 +736,7 @@ def _get_instance(self, pk): 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 @@ -775,16 +749,12 @@ def _resync_tag(self, tag: Tag) -> Tag: tag.save() return tag - def _set_tag( - self, - tag_ref, - object_tag: ObjectTag - ) -> ObjectTag: + def _set_tag(self, tag_ref, object_tag: ObjectTag) -> ObjectTag: """ Set or create the respective Tag on `object_tag` This function is used on `tag_object`. - For this to work it is necessary that `tag_ref` + For this to work it is necessary that `tag_ref` is always the `pk` of the model to which you want to associate it. """ @@ -810,6 +780,7 @@ def _set_tag( return object_tag + class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): """ User based system taxonomy class. @@ -825,10 +796,7 @@ def tag_class_model(self) -> Type: """ return get_user_model() - def _get_value_from_instance( - self, - instance - ) -> str: + def _get_value_from_instance(self, instance) -> str: """ Returns the username as tag value """ @@ -839,7 +807,7 @@ class LanguageTaxonomy(SystemDefinedTaxonomy): """ Language System-defined taxonomy - The tags are filtered and validated taking into account the + The tags are filtered and validated taking into account the languages available in Django LANGUAGES settings var """ @@ -860,29 +828,18 @@ def _get_available_languages(cls) -> List[str]: langs = set() for django_lang in settings.LANGUAGES: # Split to get the language part - langs.add(django_lang[0].split('-')[0]) + langs.add(django_lang[0].split("-")[0]) return langs - - def _check_valid_language( - self, - object_tag: ObjectTag - ) -> bool: + + 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 - ) + return object_tag.tag.external_id in available_langs - def _check_tag( - self, - object_tag: ObjectTag - ) -> bool: + 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) - ) + return super()._check_tag(object_tag) and self._check_valid_language(object_tag) diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index c6bb4805e..521c48140 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -52,7 +52,7 @@ def test_get_taxonomies(self): tax1, self.taxonomy, self.system_taxonomy, - self.language_taxonomy, + self.language_taxonomy, self.user_taxonomy, ] assert str(enabled[0]) == f" ({tax1.id}) Enabled" diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 4197f9d16..dadeb5f7b 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -1,25 +1,24 @@ """ Test the tagging models """ import ddt - from django.contrib.auth import get_user_model from django.test.testcases import TestCase, override_settings from openedx_tagging.core.tagging.models import ( + ModelSystemDefinedTaxonomy, ObjectTag, + SystemDefinedTaxonomy, Tag, Taxonomy, - SystemDefinedTaxonomy, - ModelSystemDefinedTaxonomy, UserSystemDefinedTaxonomy, ) test_languages = [ - ('en', 'English'), - ('az', 'Azerbaijani'), - ('id', 'Indonesian'), - ('qu', 'Quechua'), - ('zu', 'Zulu'), + ("en", "English"), + ("az", "Azerbaijani"), + ("id", "Indonesian"), + ("qu", "Quechua"), + ("zu", "Zulu"), ] @@ -33,6 +32,7 @@ class InvalidModelTaxonomy(ModelSystemDefinedTaxonomy): """ Model used for testing """ + @property def tag_class_model(self): return EmptyTestClass @@ -40,13 +40,14 @@ def tag_class_model(self): class Meta: proxy = True managed = False - app_label = 'oel_tagging' + app_label = "oel_tagging" class TestModelTaxonomy(ModelSystemDefinedTaxonomy): """ Model used for testing """ + @property def tag_class_model(self): return get_user_model() @@ -54,7 +55,7 @@ def tag_class_model(self): class Meta: proxy = True managed = False - app_label = 'oel_tagging' + app_label = "oel_tagging" def get_tag(value): @@ -74,7 +75,9 @@ class TestTagTaxonomyMixin: def setUp(self): super().setUp() self.taxonomy = Taxonomy.objects.get(name="Life on Earth") - self.system_taxonomy = Taxonomy.objects.get(name="System defined taxonomy").cast() + 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") @@ -86,12 +89,12 @@ def setUp(self): self.system_taxonomy_tag = get_tag("System Tag 1") self.user_1 = get_user_model()( id=1, - username='test_user_1', + username="test_user_1", ) self.user_1.save() self.user_2 = get_user_model()( id=2, - username='test_user_2', + username="test_user_2", ) self.user_2.save() @@ -487,15 +490,15 @@ def test_validations(self, taxonomy, second_taxonomy, expected): 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 + 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 @@ -520,18 +523,18 @@ def test_implementation_error(self, taxonomy_cls, expected_exception): @ddt.data( (1, "tag_id"), # Valid (0, "tag_id"), # Invalid user - (1, None) # Testing parent validations + (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', + value="_val", external_id=tag_external_id, ) object_tag = ObjectTag( - object_id='id', + object_id="id", tag=tag, ) @@ -539,21 +542,17 @@ def test_validations(self, tag_external_id, tag_id): object_tag=object_tag, check_object=False, check_taxonomy=False, - check_tag=True + 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' - ) + 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' + tags=[self.user_1.id], object_id="object_id" ) def test_tag_object_tag_creation(self): @@ -567,23 +566,19 @@ def test_tag_object_tag_creation(self): # Test parent functions taxonomy = TestModelTaxonomy( - name='Test', - description='Test', + 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' - ) + 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() @@ -613,9 +608,10 @@ def test_tag_object_delete_user(self): with self.assertRaises(ValueError): self.user_taxonomy.tag_object( tags=[user_1_id], - object_id='object_id', + object_id="object_id", ) + @ddt.ddt @override_settings(LANGUAGES=test_languages) class TestLanguageTaxonomy(TestTagTaxonomyMixin, TestCase): @@ -624,20 +620,20 @@ class TestLanguageTaxonomy(TestTagTaxonomyMixin, TestCase): """ @ddt.data( - ('en', 'tag_id'), # Valid - ('es', 'tag_id'), # Not available lang - ('en', None), # Test parent validations + ("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', + value="_val", external_id=lang, ) object_tag = ObjectTag( - object_id='id', + object_id="id", tag=tag, ) self.language_taxonomy.validate_object_tag( From 69eeece81494120a654a345dcfcb83a56c7f3880 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jul 2023 01:59:48 +0930 Subject: [PATCH 10/27] refactor: use negative numbers as primary keys for system taxonomies and tags --- .../tagging/fixtures/language_taxonomy.yaml | 368 +++++++++--------- .../core/tagging/fixtures/system_defined.yaml | 2 +- .../commands/build_language_fixture.py | 5 +- 3 files changed, 188 insertions(+), 187 deletions(-) diff --git a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml index efe84cdbb..06d51e150 100644 --- a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml +++ b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml @@ -1,1286 +1,1286 @@ - model: oel_tagging.tag - pk: 1 + pk: -1 fields: taxonomy: 1 parent: null value: Afar external_id: aa - model: oel_tagging.tag - pk: 2 + pk: -2 fields: taxonomy: 1 parent: null value: Abkhazian external_id: ab - model: oel_tagging.tag - pk: 3 + pk: -3 fields: taxonomy: 1 parent: null value: Avestan external_id: ae - model: oel_tagging.tag - pk: 4 + pk: -4 fields: taxonomy: 1 parent: null value: Afrikaans external_id: af - model: oel_tagging.tag - pk: 5 + pk: -5 fields: taxonomy: 1 parent: null value: Akan external_id: ak - model: oel_tagging.tag - pk: 6 + pk: -6 fields: taxonomy: 1 parent: null value: Amharic external_id: am - model: oel_tagging.tag - pk: 7 + pk: -7 fields: taxonomy: 1 parent: null value: Aragonese external_id: an - model: oel_tagging.tag - pk: 8 + pk: -8 fields: taxonomy: 1 parent: null value: Arabic external_id: ar - model: oel_tagging.tag - pk: 9 + pk: -9 fields: taxonomy: 1 parent: null value: Assamese external_id: as - model: oel_tagging.tag - pk: 10 + pk: -10 fields: taxonomy: 1 parent: null value: Avaric external_id: av - model: oel_tagging.tag - pk: 11 + pk: -11 fields: taxonomy: 1 parent: null value: Aymara external_id: ay - model: oel_tagging.tag - pk: 12 + pk: -12 fields: taxonomy: 1 parent: null value: Azerbaijani external_id: az - model: oel_tagging.tag - pk: 13 + pk: -13 fields: taxonomy: 1 parent: null value: Bashkir external_id: ba - model: oel_tagging.tag - pk: 14 + pk: -14 fields: taxonomy: 1 parent: null value: Belarusian external_id: be - model: oel_tagging.tag - pk: 15 + pk: -15 fields: taxonomy: 1 parent: null value: Bulgarian external_id: bg - model: oel_tagging.tag - pk: 16 + pk: -16 fields: taxonomy: 1 parent: null value: Bihari languages external_id: bh - model: oel_tagging.tag - pk: 17 + pk: -17 fields: taxonomy: 1 parent: null value: Bislama external_id: bi - model: oel_tagging.tag - pk: 18 + pk: -18 fields: taxonomy: 1 parent: null value: Bambara external_id: bm - model: oel_tagging.tag - pk: 19 + pk: -19 fields: taxonomy: 1 parent: null value: Bengali external_id: bn - model: oel_tagging.tag - pk: 20 + pk: -20 fields: taxonomy: 1 parent: null value: Tibetan external_id: bo - model: oel_tagging.tag - pk: 21 + pk: -21 fields: taxonomy: 1 parent: null value: Breton external_id: br - model: oel_tagging.tag - pk: 22 + pk: -22 fields: taxonomy: 1 parent: null value: Bosnian external_id: bs - model: oel_tagging.tag - pk: 23 + pk: -23 fields: taxonomy: 1 parent: null value: Catalan external_id: ca - model: oel_tagging.tag - pk: 24 + pk: -24 fields: taxonomy: 1 parent: null value: Chechen external_id: ce - model: oel_tagging.tag - pk: 25 + pk: -25 fields: taxonomy: 1 parent: null value: Chamorro external_id: ch - model: oel_tagging.tag - pk: 26 + pk: -26 fields: taxonomy: 1 parent: null value: Corsican external_id: co - model: oel_tagging.tag - pk: 27 + pk: -27 fields: taxonomy: 1 parent: null value: Cree external_id: cr - model: oel_tagging.tag - pk: 28 + pk: -28 fields: taxonomy: 1 parent: null value: Czech external_id: cs - model: oel_tagging.tag - pk: 29 + pk: -29 fields: taxonomy: 1 parent: null value: Church Slavic external_id: cu - model: oel_tagging.tag - pk: 30 + pk: -30 fields: taxonomy: 1 parent: null value: Chuvash external_id: cv - model: oel_tagging.tag - pk: 31 + pk: -31 fields: taxonomy: 1 parent: null value: Welsh external_id: cy - model: oel_tagging.tag - pk: 32 + pk: -32 fields: taxonomy: 1 parent: null value: Danish external_id: da - model: oel_tagging.tag - pk: 33 + pk: -33 fields: taxonomy: 1 parent: null value: German external_id: de - model: oel_tagging.tag - pk: 34 + pk: -34 fields: taxonomy: 1 parent: null value: Divehi external_id: dv - model: oel_tagging.tag - pk: 35 + pk: -35 fields: taxonomy: 1 parent: null value: Dzongkha external_id: dz - model: oel_tagging.tag - pk: 36 + pk: -36 fields: taxonomy: 1 parent: null value: Ewe external_id: ee - model: oel_tagging.tag - pk: 37 + pk: -37 fields: taxonomy: 1 parent: null value: Greek, Modern (1453-) external_id: el - model: oel_tagging.tag - pk: 38 + pk: -38 fields: taxonomy: 1 parent: null value: English external_id: en - model: oel_tagging.tag - pk: 39 + pk: -39 fields: taxonomy: 1 parent: null value: Esperanto external_id: eo - model: oel_tagging.tag - pk: 40 + pk: -40 fields: taxonomy: 1 parent: null value: Spanish external_id: es - model: oel_tagging.tag - pk: 41 + pk: -41 fields: taxonomy: 1 parent: null value: Estonian external_id: et - model: oel_tagging.tag - pk: 42 + pk: -42 fields: taxonomy: 1 parent: null value: Basque external_id: eu - model: oel_tagging.tag - pk: 43 + pk: -43 fields: taxonomy: 1 parent: null value: Persian external_id: fa - model: oel_tagging.tag - pk: 44 + pk: -44 fields: taxonomy: 1 parent: null value: Fulah external_id: ff - model: oel_tagging.tag - pk: 45 + pk: -45 fields: taxonomy: 1 parent: null value: Finnish external_id: fi - model: oel_tagging.tag - pk: 46 + pk: -46 fields: taxonomy: 1 parent: null value: Fijian external_id: fj - model: oel_tagging.tag - pk: 47 + pk: -47 fields: taxonomy: 1 parent: null value: Faroese external_id: fo - model: oel_tagging.tag - pk: 48 + pk: -48 fields: taxonomy: 1 parent: null value: French external_id: fr - model: oel_tagging.tag - pk: 49 + pk: -49 fields: taxonomy: 1 parent: null value: Western Frisian external_id: fy - model: oel_tagging.tag - pk: 50 + pk: -50 fields: taxonomy: 1 parent: null value: Irish external_id: ga - model: oel_tagging.tag - pk: 51 + pk: -51 fields: taxonomy: 1 parent: null value: Gaelic external_id: gd - model: oel_tagging.tag - pk: 52 + pk: -52 fields: taxonomy: 1 parent: null value: Galician external_id: gl - model: oel_tagging.tag - pk: 53 + pk: -53 fields: taxonomy: 1 parent: null value: Guarani external_id: gn - model: oel_tagging.tag - pk: 54 + pk: -54 fields: taxonomy: 1 parent: null value: Gujarati external_id: gu - model: oel_tagging.tag - pk: 55 + pk: -55 fields: taxonomy: 1 parent: null value: Manx external_id: gv - model: oel_tagging.tag - pk: 56 + pk: -56 fields: taxonomy: 1 parent: null value: Hausa external_id: ha - model: oel_tagging.tag - pk: 57 + pk: -57 fields: taxonomy: 1 parent: null value: Hebrew external_id: he - model: oel_tagging.tag - pk: 58 + pk: -58 fields: taxonomy: 1 parent: null value: Hindi external_id: hi - model: oel_tagging.tag - pk: 59 + pk: -59 fields: taxonomy: 1 parent: null value: Hiri Motu external_id: ho - model: oel_tagging.tag - pk: 60 + pk: -60 fields: taxonomy: 1 parent: null value: Croatian external_id: hr - model: oel_tagging.tag - pk: 61 + pk: -61 fields: taxonomy: 1 parent: null value: Haitian external_id: ht - model: oel_tagging.tag - pk: 62 + pk: -62 fields: taxonomy: 1 parent: null value: Hungarian external_id: hu - model: oel_tagging.tag - pk: 63 + pk: -63 fields: taxonomy: 1 parent: null value: Armenian external_id: hy - model: oel_tagging.tag - pk: 64 + pk: -64 fields: taxonomy: 1 parent: null value: Herero external_id: hz - model: oel_tagging.tag - pk: 65 + pk: -65 fields: taxonomy: 1 parent: null value: Interlingua (International Auxiliary Language Association) external_id: ia - model: oel_tagging.tag - pk: 66 + pk: -66 fields: taxonomy: 1 parent: null value: Indonesian external_id: id - model: oel_tagging.tag - pk: 67 + pk: -67 fields: taxonomy: 1 parent: null value: Interlingue external_id: ie - model: oel_tagging.tag - pk: 68 + pk: -68 fields: taxonomy: 1 parent: null value: Igbo external_id: ig - model: oel_tagging.tag - pk: 69 + pk: -69 fields: taxonomy: 1 parent: null value: Sichuan Yi external_id: ii - model: oel_tagging.tag - pk: 70 + pk: -70 fields: taxonomy: 1 parent: null value: Inupiaq external_id: ik - model: oel_tagging.tag - pk: 71 + pk: -71 fields: taxonomy: 1 parent: null value: Ido external_id: io - model: oel_tagging.tag - pk: 72 + pk: -72 fields: taxonomy: 1 parent: null value: Icelandic external_id: is - model: oel_tagging.tag - pk: 73 + pk: -73 fields: taxonomy: 1 parent: null value: Italian external_id: it - model: oel_tagging.tag - pk: 74 + pk: -74 fields: taxonomy: 1 parent: null value: Inuktitut external_id: iu - model: oel_tagging.tag - pk: 75 + pk: -75 fields: taxonomy: 1 parent: null value: Japanese external_id: ja - model: oel_tagging.tag - pk: 76 + pk: -76 fields: taxonomy: 1 parent: null value: Javanese external_id: jv - model: oel_tagging.tag - pk: 77 + pk: -77 fields: taxonomy: 1 parent: null value: Georgian external_id: ka - model: oel_tagging.tag - pk: 78 + pk: -78 fields: taxonomy: 1 parent: null value: Kongo external_id: kg - model: oel_tagging.tag - pk: 79 + pk: -79 fields: taxonomy: 1 parent: null value: Kikuyu external_id: ki - model: oel_tagging.tag - pk: 80 + pk: -80 fields: taxonomy: 1 parent: null value: Kuanyama external_id: kj - model: oel_tagging.tag - pk: 81 + pk: -81 fields: taxonomy: 1 parent: null value: Kazakh external_id: kk - model: oel_tagging.tag - pk: 82 + pk: -82 fields: taxonomy: 1 parent: null value: Kalaallisut external_id: kl - model: oel_tagging.tag - pk: 83 + pk: -83 fields: taxonomy: 1 parent: null value: Central Khmer external_id: km - model: oel_tagging.tag - pk: 84 + pk: -84 fields: taxonomy: 1 parent: null value: Kannada external_id: kn - model: oel_tagging.tag - pk: 85 + pk: -85 fields: taxonomy: 1 parent: null value: Korean external_id: ko - model: oel_tagging.tag - pk: 86 + pk: -86 fields: taxonomy: 1 parent: null value: Kanuri external_id: kr - model: oel_tagging.tag - pk: 87 + pk: -87 fields: taxonomy: 1 parent: null value: Kashmiri external_id: ks - model: oel_tagging.tag - pk: 88 + pk: -88 fields: taxonomy: 1 parent: null value: Kurdish external_id: ku - model: oel_tagging.tag - pk: 89 + pk: -89 fields: taxonomy: 1 parent: null value: Komi external_id: kv - model: oel_tagging.tag - pk: 90 + pk: -90 fields: taxonomy: 1 parent: null value: Cornish external_id: kw - model: oel_tagging.tag - pk: 91 + pk: -91 fields: taxonomy: 1 parent: null value: Kirghiz external_id: ky - model: oel_tagging.tag - pk: 92 + pk: -92 fields: taxonomy: 1 parent: null value: Latin external_id: la - model: oel_tagging.tag - pk: 93 + pk: -93 fields: taxonomy: 1 parent: null value: Luxembourgish external_id: lb - model: oel_tagging.tag - pk: 94 + pk: -94 fields: taxonomy: 1 parent: null value: Ganda external_id: lg - model: oel_tagging.tag - pk: 95 + pk: -95 fields: taxonomy: 1 parent: null value: Limburgan external_id: li - model: oel_tagging.tag - pk: 96 + pk: -96 fields: taxonomy: 1 parent: null value: Lingala external_id: ln - model: oel_tagging.tag - pk: 97 + pk: -97 fields: taxonomy: 1 parent: null value: Lao external_id: lo - model: oel_tagging.tag - pk: 98 + pk: -98 fields: taxonomy: 1 parent: null value: Lithuanian external_id: lt - model: oel_tagging.tag - pk: 99 + pk: -99 fields: taxonomy: 1 parent: null value: Luba-Katanga external_id: lu - model: oel_tagging.tag - pk: 100 + pk: -100 fields: taxonomy: 1 parent: null value: Latvian external_id: lv - model: oel_tagging.tag - pk: 101 + pk: -101 fields: taxonomy: 1 parent: null value: Malagasy external_id: mg - model: oel_tagging.tag - pk: 102 + pk: -102 fields: taxonomy: 1 parent: null value: Marshallese external_id: mh - model: oel_tagging.tag - pk: 103 + pk: -103 fields: taxonomy: 1 parent: null value: Maori external_id: mi - model: oel_tagging.tag - pk: 104 + pk: -104 fields: taxonomy: 1 parent: null value: Macedonian external_id: mk - model: oel_tagging.tag - pk: 105 + pk: -105 fields: taxonomy: 1 parent: null value: Malayalam external_id: ml - model: oel_tagging.tag - pk: 106 + pk: -106 fields: taxonomy: 1 parent: null value: Mongolian external_id: mn - model: oel_tagging.tag - pk: 107 + pk: -107 fields: taxonomy: 1 parent: null value: Marathi external_id: mr - model: oel_tagging.tag - pk: 108 + pk: -108 fields: taxonomy: 1 parent: null value: Malay external_id: ms - model: oel_tagging.tag - pk: 109 + pk: -109 fields: taxonomy: 1 parent: null value: Maltese external_id: mt - model: oel_tagging.tag - pk: 110 + pk: -110 fields: taxonomy: 1 parent: null value: Burmese external_id: my - model: oel_tagging.tag - pk: 111 + pk: -111 fields: taxonomy: 1 parent: null value: Nauru external_id: na - model: oel_tagging.tag - pk: 112 + pk: -112 fields: taxonomy: 1 parent: null value: Bokmål, Norwegian external_id: nb - model: oel_tagging.tag - pk: 113 + pk: -113 fields: taxonomy: 1 parent: null value: Ndebele, North external_id: nd - model: oel_tagging.tag - pk: 114 + pk: -114 fields: taxonomy: 1 parent: null value: Nepali external_id: ne - model: oel_tagging.tag - pk: 115 + pk: -115 fields: taxonomy: 1 parent: null value: Ndonga external_id: ng - model: oel_tagging.tag - pk: 116 + pk: -116 fields: taxonomy: 1 parent: null value: Dutch external_id: nl - model: oel_tagging.tag - pk: 117 + pk: -117 fields: taxonomy: 1 parent: null value: Norwegian Nynorsk external_id: nn - model: oel_tagging.tag - pk: 118 + pk: -118 fields: taxonomy: 1 parent: null value: Norwegian external_id: no - model: oel_tagging.tag - pk: 119 + pk: -119 fields: taxonomy: 1 parent: null value: Ndebele, South external_id: nr - model: oel_tagging.tag - pk: 120 + pk: -120 fields: taxonomy: 1 parent: null value: Navajo external_id: nv - model: oel_tagging.tag - pk: 121 + pk: -121 fields: taxonomy: 1 parent: null value: Chichewa external_id: ny - model: oel_tagging.tag - pk: 122 + pk: -122 fields: taxonomy: 1 parent: null value: Occitan (post 1500) external_id: oc - model: oel_tagging.tag - pk: 123 + pk: -123 fields: taxonomy: 1 parent: null value: Ojibwa external_id: oj - model: oel_tagging.tag - pk: 124 + pk: -124 fields: taxonomy: 1 parent: null value: Oromo external_id: om - model: oel_tagging.tag - pk: 125 + pk: -125 fields: taxonomy: 1 parent: null value: Oriya external_id: or - model: oel_tagging.tag - pk: 126 + pk: -126 fields: taxonomy: 1 parent: null value: Ossetian external_id: os - model: oel_tagging.tag - pk: 127 + pk: -127 fields: taxonomy: 1 parent: null value: Panjabi external_id: pa - model: oel_tagging.tag - pk: 128 + pk: -128 fields: taxonomy: 1 parent: null value: Pali external_id: pi - model: oel_tagging.tag - pk: 129 + pk: -129 fields: taxonomy: 1 parent: null value: Polish external_id: pl - model: oel_tagging.tag - pk: 130 + pk: -130 fields: taxonomy: 1 parent: null value: Pushto external_id: ps - model: oel_tagging.tag - pk: 131 + pk: -131 fields: taxonomy: 1 parent: null value: Portuguese external_id: pt - model: oel_tagging.tag - pk: 132 + pk: -132 fields: taxonomy: 1 parent: null value: Quechua external_id: qu - model: oel_tagging.tag - pk: 133 + pk: -133 fields: taxonomy: 1 parent: null value: Romansh external_id: rm - model: oel_tagging.tag - pk: 134 + pk: -134 fields: taxonomy: 1 parent: null value: Rundi external_id: rn - model: oel_tagging.tag - pk: 135 + pk: -135 fields: taxonomy: 1 parent: null value: Romanian external_id: ro - model: oel_tagging.tag - pk: 136 + pk: -136 fields: taxonomy: 1 parent: null value: Russian external_id: ru - model: oel_tagging.tag - pk: 137 + pk: -137 fields: taxonomy: 1 parent: null value: Kinyarwanda external_id: rw - model: oel_tagging.tag - pk: 138 + pk: -138 fields: taxonomy: 1 parent: null value: Sanskrit external_id: sa - model: oel_tagging.tag - pk: 139 + pk: -139 fields: taxonomy: 1 parent: null value: Sardinian external_id: sc - model: oel_tagging.tag - pk: 140 + pk: -140 fields: taxonomy: 1 parent: null value: Sindhi external_id: sd - model: oel_tagging.tag - pk: 141 + pk: -141 fields: taxonomy: 1 parent: null value: Northern Sami external_id: se - model: oel_tagging.tag - pk: 142 + pk: -142 fields: taxonomy: 1 parent: null value: Sango external_id: sg - model: oel_tagging.tag - pk: 143 + pk: -143 fields: taxonomy: 1 parent: null value: Sinhala external_id: si - model: oel_tagging.tag - pk: 144 + pk: -144 fields: taxonomy: 1 parent: null value: Slovak external_id: sk - model: oel_tagging.tag - pk: 145 + pk: -145 fields: taxonomy: 1 parent: null value: Slovenian external_id: sl - model: oel_tagging.tag - pk: 146 + pk: -146 fields: taxonomy: 1 parent: null value: Samoan external_id: sm - model: oel_tagging.tag - pk: 147 + pk: -147 fields: taxonomy: 1 parent: null value: Shona external_id: sn - model: oel_tagging.tag - pk: 148 + pk: -148 fields: taxonomy: 1 parent: null value: Somali external_id: so - model: oel_tagging.tag - pk: 149 + pk: -149 fields: taxonomy: 1 parent: null value: Albanian external_id: sq - model: oel_tagging.tag - pk: 150 + pk: -150 fields: taxonomy: 1 parent: null value: Serbian external_id: sr - model: oel_tagging.tag - pk: 151 + pk: -151 fields: taxonomy: 1 parent: null value: Swati external_id: ss - model: oel_tagging.tag - pk: 152 + pk: -152 fields: taxonomy: 1 parent: null value: Sotho, Southern external_id: st - model: oel_tagging.tag - pk: 153 + pk: -153 fields: taxonomy: 1 parent: null value: Sundanese external_id: su - model: oel_tagging.tag - pk: 154 + pk: -154 fields: taxonomy: 1 parent: null value: Swedish external_id: sv - model: oel_tagging.tag - pk: 155 + pk: -155 fields: taxonomy: 1 parent: null value: Swahili external_id: sw - model: oel_tagging.tag - pk: 156 + pk: -156 fields: taxonomy: 1 parent: null value: Tamil external_id: ta - model: oel_tagging.tag - pk: 157 + pk: -157 fields: taxonomy: 1 parent: null value: Telugu external_id: te - model: oel_tagging.tag - pk: 158 + pk: -158 fields: taxonomy: 1 parent: null value: Tajik external_id: tg - model: oel_tagging.tag - pk: 159 + pk: -159 fields: taxonomy: 1 parent: null value: Thai external_id: th - model: oel_tagging.tag - pk: 160 + pk: -160 fields: taxonomy: 1 parent: null value: Tigrinya external_id: ti - model: oel_tagging.tag - pk: 161 + pk: -161 fields: taxonomy: 1 parent: null value: Turkmen external_id: tk - model: oel_tagging.tag - pk: 162 + pk: -162 fields: taxonomy: 1 parent: null value: Tagalog external_id: tl - model: oel_tagging.tag - pk: 163 + pk: -163 fields: taxonomy: 1 parent: null value: Tswana external_id: tn - model: oel_tagging.tag - pk: 164 + pk: -164 fields: taxonomy: 1 parent: null value: Tonga (Tonga Islands) external_id: to - model: oel_tagging.tag - pk: 165 + pk: -165 fields: taxonomy: 1 parent: null value: Turkish external_id: tr - model: oel_tagging.tag - pk: 166 + pk: -166 fields: taxonomy: 1 parent: null value: Tsonga external_id: ts - model: oel_tagging.tag - pk: 167 + pk: -167 fields: taxonomy: 1 parent: null value: Tatar external_id: tt - model: oel_tagging.tag - pk: 168 + pk: -168 fields: taxonomy: 1 parent: null value: Twi external_id: tw - model: oel_tagging.tag - pk: 169 + pk: -169 fields: taxonomy: 1 parent: null value: Tahitian external_id: ty - model: oel_tagging.tag - pk: 170 + pk: -170 fields: taxonomy: 1 parent: null value: Uighur external_id: ug - model: oel_tagging.tag - pk: 171 + pk: -171 fields: taxonomy: 1 parent: null value: Ukrainian external_id: uk - model: oel_tagging.tag - pk: 172 + pk: -172 fields: taxonomy: 1 parent: null value: Urdu external_id: ur - model: oel_tagging.tag - pk: 173 + pk: -173 fields: taxonomy: 1 parent: null value: Uzbek external_id: uz - model: oel_tagging.tag - pk: 174 + pk: -174 fields: taxonomy: 1 parent: null value: Venda external_id: ve - model: oel_tagging.tag - pk: 175 + pk: -175 fields: taxonomy: 1 parent: null value: Vietnamese external_id: vi - model: oel_tagging.tag - pk: 176 + pk: -176 fields: taxonomy: 1 parent: null value: Volapük external_id: vo - model: oel_tagging.tag - pk: 177 + pk: -177 fields: taxonomy: 1 parent: null value: Walloon external_id: wa - model: oel_tagging.tag - pk: 178 + pk: -178 fields: taxonomy: 1 parent: null value: Wolof external_id: wo - model: oel_tagging.tag - pk: 179 + pk: -179 fields: taxonomy: 1 parent: null value: Xhosa external_id: xh - model: oel_tagging.tag - pk: 180 + pk: -180 fields: taxonomy: 1 parent: null value: Yiddish external_id: yi - model: oel_tagging.tag - pk: 181 + pk: -181 fields: taxonomy: 1 parent: null value: Yoruba external_id: yo - model: oel_tagging.tag - pk: 182 + pk: -182 fields: taxonomy: 1 parent: null value: Zhuang external_id: za - model: oel_tagging.tag - pk: 183 + pk: -183 fields: taxonomy: 1 parent: null value: Chinese external_id: zh - model: oel_tagging.tag - pk: 184 + pk: -184 fields: taxonomy: 1 parent: null diff --git a/openedx_tagging/core/tagging/fixtures/system_defined.yaml b/openedx_tagging/core/tagging/fixtures/system_defined.yaml index c5737b645..6c085152e 100644 --- a/openedx_tagging/core/tagging/fixtures/system_defined.yaml +++ b/openedx_tagging/core/tagging/fixtures/system_defined.yaml @@ -1,5 +1,5 @@ - model: oel_tagging.taxonomy - pk: 1 + pk: -1 fields: name: Languages description: ISO 639-1 Languages. Allows tags for any language configured for use on the instance diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index 5735ac167..ed54bff98 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -25,7 +25,7 @@ def download_json(self): return json.loads(json_data) def build_fixture(self, json_data): - tag_pk = 1 + tag_pk = -1 with open(output, "w") as output_file: for lang_data in json_data: lang_value = self.get_lang_value(lang_data) @@ -37,7 +37,8 @@ def build_fixture(self, json_data): output_file.write(" parent: null\n") output_file.write(f" value: {lang_value}\n") output_file.write(f" external_id: {lang_code}\n") - tag_pk += 1 + # System tags are identified with negative numbers to avoid clashing with user-created tags. + tag_pk -= 1 def get_lang_value(self, lang_data): """ From 2a0b4b6c931a1e2f418216b96f346ae1df896a96 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jul 2023 02:35:58 +0930 Subject: [PATCH 11/27] refactor: use ObjectTag subclasses where possible * Adds Taxonomy.object_tag_class property, which can be overridden by Taxonomy subclasses to provide their own ObjectTag subclasses. * Replaces the Taxonomy._set_tag method with an ObjectTag.tag_ref property setter, so that ObjectTag subclasses can dynamically create Tags as needed during tag_object() * Moves the tag_class_model and tag_class_value properties to a new ModelObjectTag abstract class, so the ModelSystemDefinedTaxonomy can be simpler. * Updates tests for coverage --- openedx_tagging/core/tagging/api.py | 3 +- .../tagging/migrations/0003_system_defined.py | 22 +- openedx_tagging/core/tagging/models.py | 258 +++++++++++------- .../core/tagging/test_models.py | 39 ++- 4 files changed, 213 insertions(+), 109 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 5ca23c485..5d8ca1158 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -106,8 +106,9 @@ def get_object_tags( Pass valid_only=False when displaying tags to content authors, so they can see invalid tags too. Invalid tags will (probably) be hidden from learners. """ + ObjectTagClass = taxonomy.object_tag_class if taxonomy else ObjectTag tags = ( - ObjectTag.objects.filter( + ObjectTagClass.objects.filter( object_id=object_id, ) .select_related("tag", "taxonomy") diff --git a/openedx_tagging/core/tagging/migrations/0003_system_defined.py b/openedx_tagging/core/tagging/migrations/0003_system_defined.py index 99637a49c..8a8bf1d44 100644 --- a/openedx_tagging/core/tagging/migrations/0003_system_defined.py +++ b/openedx_tagging/core/tagging/migrations/0003_system_defined.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.19 on 2023-07-19 20:28 +# Generated by Django 3.2.19 on 2023-07-19 17:03 from django.db import migrations @@ -9,6 +9,16 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="ModelObjectTag", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("oel_tagging.objecttag",), + ), migrations.CreateModel( name="SystemDefinedTaxonomy", fields=[], @@ -39,6 +49,16 @@ class Migration(migrations.Migration): }, bases=("oel_tagging.systemdefinedtaxonomy",), ), + migrations.CreateModel( + name="UserModelObjectTag", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("oel_tagging.modelobjecttag",), + ), migrations.CreateModel( name="UserSystemDefinedTaxonomy", fields=[], diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index 0f0494d6a..30031d130 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -181,6 +181,15 @@ def __str__(self): """ return f"<{self.__class__.__name__}> ({self.id}) {self.name}" + @property + def object_tag_class(self) -> Type: + """ + Returns the ObjectTag subclass associated with this taxonomy, which is ObjectTag by default. + + Taxonomy subclasses may override this method to use different subclasses of ObjectTag. + """ + return ObjectTag + @property def taxonomy_class(self) -> Type: """ @@ -353,28 +362,6 @@ 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, @@ -396,9 +383,10 @@ def tag_object( _(f"Taxonomy ({self.id}) requires at least one tag per object.") ) + ObjectTagClass = self.object_tag_class current_tags = { tag.tag_ref: tag - for tag in ObjectTag.objects.filter( + for tag in ObjectTagClass.objects.filter( taxonomy=self, object_id=object_id, ) @@ -408,14 +396,14 @@ def tag_object( if tag_ref in current_tags: object_tag = current_tags.pop(tag_ref) else: - object_tag = ObjectTag( + object_tag = ObjectTagClass( taxonomy=self, object_id=object_id, ) - object_tag = self._set_tag(tag_ref, object_tag) - - if not object_tag or not self.validate_object_tag(object_tag): + object_tag.tag_ref = tag_ref + object_tag.resync() + if not self.validate_object_tag(object_tag): raise ValueError( _(f"Invalid object tag for taxonomy ({self.id}): {tag_ref}") ) @@ -553,6 +541,25 @@ def tag_ref(self) -> str: """ return self.tag.id if self.tag_id else self._value + @tag_ref.setter + def tag_ref(self, tag_ref: str): + """ + Sets the ObjectTag's Tag and/or value, depending on whether a valid Tag is found. + + Subclasses may override this method to dynamically create Tags. + """ + self.value = tag_ref + + if self.taxonomy_id: + try: + self.tag = self.taxonomy.tag_set.get(pk=tag_ref) + self.value = self.tag.value + return + except (ValueError, Tag.DoesNotExist): + # This might be ok, e.g. if our taxonomy.allow_free_text, so we just pass through here. + # We rely on the caller to validate before saving. + pass + def is_valid(self) -> bool: """ Returns True if this ObjectTag represents a valid taxonomy tag. @@ -652,6 +659,10 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": class SystemDefinedTaxonomy(Taxonomy): + """ + Simple subclass of Taxonomy which requires the system_defined flag to be set. + """ + class Meta: proxy = True @@ -662,21 +673,11 @@ def _check_taxonomy(self, object_tag: ObjectTag) -> bool: return super()._check_taxonomy(object_tag) and self.system_defined -class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): +class ModelObjectTag(ObjectTag): """ - 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. + Model-based ObjectTag, abstract class. - 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. + Used by ModelSystemDefinedTaxonomy to maintain dynamic Tags which are associated with a configured Model instance. """ class Meta: @@ -697,93 +698,119 @@ class referenced by these object tags. """ raise NotImplementedError - def _check_instance(self, object_tag: ObjectTag) -> bool: + @property + def tag_class_value(self) -> str: """ - Returns True if the instance exists + Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. - Subclasses can override this method to perform their own instance validation checks. + Subclasses may override this method to use different fields. """ - intance_id = object_tag.tag.external_id - return self.tag_class_model.objects.filter(pk=intance_id).exists() + return "pk" - def _check_tag(self, object_tag: ObjectTag) -> bool: + def get_instance(self) -> Union[models.Model, None]: """ - Returns True if pass the instance validation checks + Returns the instance of tag_class_model associated with this object tag, or None if not found. """ - return super()._check_tag(object_tag) and self._check_instance(object_tag) + instance_id = self.tag.external_id if self.tag else None + if instance_id: + try: + return self.tag_class_model.objects.get(pk=instance_id) + except self.tag_class_model.DoesNotExist: + log.exception( + f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist." + ) - def _get_external_id_from_instance(self, instance) -> str: - """ - Returns the tag external id from the instance + return None - Subclasses can override this method to get the external id from their own models. + def _resync_tag(self) -> bool: """ - return str(instance.pk) + Resync our tag's value with the value from the instance. - def _get_value_from_instance(self, instance) -> str: - """ - Returns the tag value from the instance + If the instance associated with the tag no longer exists, we unset our tag, because it's no longer valid. - Subclasses can override this method to get the value from their own models. + Returns True if the given tag was changed, False otherwise. """ - return str(instance.pk) + instance = self.get_instance() + if instance: + value = getattr(instance, self.tag_class_value) + self.value = value + if self.tag and self.tag.value != value: + self.tag.value = value + self.tag.save() + return True + else: + self.tag = None + + return False - def _get_instance(self, pk): + @property + def tag_ref(self) -> str: + return (self.tag.external_id or self.tag.id) if self.tag_id else self._value + + @tag_ref.setter + def tag_ref(self, tag_ref: str): """ - Gets the instance from `pk` + Sets the ObjectTag's Tag and/or value, depending on whether a valid Tag is found, or can be created. + + Creates a Tag for the given tag_ref value, if one containing that external_id not already exist. """ - try: - return self.tag_class_model.objects.get(pk=pk) - except self.tag_class_model.DoesNotExist: - return None + self.value = tag_ref - def _resync_tag(self, tag: Tag) -> Tag: + if self.taxonomy_id: + try: + self.tag = self.taxonomy.tag_set.get( + external_id=tag_ref, + ) + except (ValueError, Tag.DoesNotExist): + # Creates a new Tag for this instance + self.tag = Tag( + taxonomy=self.taxonomy, + external_id=tag_ref, + ) + + self._resync_tag() + + +class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): + """ + 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 __init__(self, *args: Any, **kwargs: Any) -> None: """ - Resync the tag value with the value from the instance + Checks if the `object_tag_class` is a subclass of ModelObjectTag. """ - 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 + assert issubclass(self.object_tag_class, ModelObjectTag) + super().__init__(*args, **kwargs) - def _set_tag(self, tag_ref, object_tag: ObjectTag) -> ObjectTag: + @property + def object_tag_class(self) -> Type: """ - Set or create the respective Tag on `object_tag` + Returns the ObjectTag subclass associated with this taxonomy. - 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. + Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. """ - 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 object_tag + raise NotImplementedError -class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): +class UserModelObjectTag(ModelObjectTag): """ - User based system taxonomy class. + ObjectTags for the UserSystemDefinedTaxonomy. """ class Meta: @@ -796,11 +823,32 @@ def tag_class_model(self) -> Type: """ return get_user_model() - def _get_value_from_instance(self, instance) -> str: + @property + def tag_class_value(self) -> str: + """ + Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. + + Subclasses may override this method to use different fields. """ - Returns the username as tag value + return "username" + + +class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): + """ + User based system taxonomy class. + """ + + class Meta: + proxy = True + + @property + def object_tag_class(self) -> Type: + """ + Returns the ObjectTag subclass associated with this taxonomy, which is ModelObjectTag by default. + + Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. """ - return instance.get_username() + return UserModelObjectTag class LanguageTaxonomy(SystemDefinedTaxonomy): diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index dadeb5f7b..e5c65b70d 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -5,6 +5,7 @@ from django.test.testcases import TestCase, override_settings from openedx_tagging.core.tagging.models import ( + ModelObjectTag, ModelSystemDefinedTaxonomy, ObjectTag, SystemDefinedTaxonomy, @@ -34,7 +35,7 @@ class InvalidModelTaxonomy(ModelSystemDefinedTaxonomy): """ @property - def tag_class_model(self): + def object_tag_class(self): return EmptyTestClass class Meta: @@ -43,7 +44,7 @@ class Meta: app_label = "oel_tagging" -class TestModelTaxonomy(ModelSystemDefinedTaxonomy): +class TestModelTag(ModelObjectTag): """ Model used for testing """ @@ -58,6 +59,21 @@ class Meta: app_label = "oel_tagging" +class TestModelTaxonomy(ModelSystemDefinedTaxonomy): + """ + Model used for testing + """ + + @property + def object_tag_class(self): + return TestModelTag + + class Meta: + proxy = True + managed = False + app_label = "oel_tagging" + + def get_tag(value): """ Fetches and returns the tag with the given value. @@ -360,6 +376,13 @@ def test_object_tag_lineage(self): object_tag.refresh_from_db() assert object_tag.get_lineage() == ["Another tag"] + def test_tag_ref(self): + object_tag = ObjectTag() + object_tag.tag_ref = 1 + object_tag.save() + assert object_tag.tag is None + assert object_tag.value == 1 + def test_object_tag_is_valid(self): open_taxonomy = Taxonomy.objects.create( name="Freetext Life", @@ -509,6 +532,7 @@ class TestModelSystemDefinedTaxonomy(TestTagTaxonomyMixin, TestCase): @ddt.data( (ModelSystemDefinedTaxonomy, NotImplementedError), + (ModelObjectTag, NotImplementedError), (InvalidModelTaxonomy, AssertionError), (UserSystemDefinedTaxonomy, None), ) @@ -611,6 +635,17 @@ def test_tag_object_delete_user(self): object_id="object_id", ) + def test_tag_ref(self): + object_tag = TestModelTag() + object_tag.tag_ref = 1 + object_tag.save() + assert object_tag.tag is None + assert object_tag.value == 1 + + def test_get_instance(self): + object_tag = TestModelTag() + assert object_tag.get_instance() is None + @ddt.ddt @override_settings(LANGUAGES=test_languages) From de3cc3e746b43abedec323a7b4a711dcdb0bdab7 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jul 2023 02:40:42 +0930 Subject: [PATCH 12/27] refactor: LanguageTaxonomy overrides get_tags removing the need for _get_tags_query_set --- openedx_tagging/core/tagging/models.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index 30031d130..3e8915661 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -255,13 +255,7 @@ def copy(self, taxonomy: "Taxonomy") -> "Taxonomy": 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]: + def get_tags(self, tag_set: models.QuerySet = None) -> List[Tag]: """ Returns a list of all Tags in the current taxonomy, from the root(s) down to TAXONOMY_MAX_DEPTH tags, in tree order. @@ -273,9 +267,12 @@ def get_tags(self) -> List[Tag]: if self.allow_free_text: return tags + if tag_set is None: + tag_set = self.tag_set + parents = None for depth in range(TAXONOMY_MAX_DEPTH): - filtered_tags = self._get_tags_query_set().prefetch_related("parent") + filtered_tags = tag_set.prefetch_related("parent") if parents is None: filtered_tags = filtered_tags.filter(parent=None) else: @@ -862,12 +859,13 @@ class LanguageTaxonomy(SystemDefinedTaxonomy): class Meta: proxy = True - def _get_tags_query_set(self) -> models.QuerySet: + def get_tags(self, tag_set: models.QuerySet = None) -> List[Tag]: """ - Returns a query set of available languages tags. + Returns a list of all the available Language Tags, annotated with ``depth`` = 0. """ available_langs = self._get_available_languages() - return self.tag_set.filter(external_id__in=available_langs) + tag_set = self.tag_set.filter(external_id__in=available_langs) + return super().get_tags(tag_set=tag_set) def _get_available_languages(cls) -> List[str]: """ From 437ae98f18d2cf352beff49dab2c4335d9e04c3e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jul 2023 12:06:52 -0500 Subject: [PATCH 13/27] docs: System taxonomy creation doc updated with Dynamic tags approach --- .../0012-system-taxonomy-creation.rst | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/decisions/0012-system-taxonomy-creation.rst b/docs/decisions/0012-system-taxonomy-creation.rst index 2ed17886f..0036432bf 100644 --- a/docs/decisions/0012-system-taxonomy-creation.rst +++ b/docs/decisions/0012-system-taxonomy-creation.rst @@ -18,7 +18,8 @@ System Tag lists and validation Each System-defined Taxonomy will have its own ``ObjectTag`` subclass which is used for tag validation (e.g. ``LanguageObjectTag``, ``OrganizationObjectTag``). Each subclass can overwrite ``get_tags``; to configure the valid tags, and ``is_valid``; to check if a list of tags are valid. Both functions are implemented on the ``ObjectTag`` base class, but can be overwritten to handle special cases. -We need to create an instance of each System-defined Taxonomy's ObjectTag in a fixture. This instances will be used on different APIs. +We need to create an instance of each System-defined Taxonomy in a fixture. With their respective characteristics and subclasses. +The ``pk`` of these instances must be negative so as not to affect the auto-incremented ``pk`` of Taxonomies. Later, we need to create content-side ObjectTags that live on ``openedx.features.content_tagging`` for each content and taxonomy to be used (eg. ``CourseLanguageObjectTag``, ``CourseOrganizationObjectTag``). This new class is used to configure the automatic content tagging. You can read the `document number 0013`_ to see this configuration. @@ -30,26 +31,28 @@ We have two ways to handle Tags creation and validation for System-defined Taxon **Hardcoded by fixtures/migrations** -#. If the tags don't change over the time, you can create all on a fixture (e.g Languages). +#. If the tags don't change over the time, you can create all on a fixture (e.g Languages). + The ``pk`` of these instances must be negative. #. If the tags change over the time, you can create all on a migration. If you edit, delete, or add new tags, you should also do it in a migration. -**Free-form tags** +**Dynamic tags** -This taxonomy depends on a core data model, but simplifies the creation of Tags by allowing free-form tags, -but we can validate the tags using the ``validate_object_tag`` method. For example we can put the ``AuthorSystemTaxonomy`` associated with -the ``User`` model and use the ``ID`` field as tags. Also we can validate if an ``User`` still exists or has been deleted over time. +Closed Taxonomies that depends on a core data model. Ex. AuthorTaxonomy with Users as Tags + +#. Tags are created on the fly when new ObjectTags are added. +#. Tag.external_id we store an identifier from the instance (eg. User.pk). +#. Tag.value we store a human readable representation of the instance (eg. User.username). +#. Resync the tags to re-fetch the value. Rejected Options ----------------- -Tags created by Auto-generated from the codebase +Free-form tags ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Taxonomies that depend on a core data model could create a Tag for each eligible object. -Maintaining this dynamic list of available Tags is cumbersome: we'd need triggers for creation, editing, and deletion. -And if it's a large list of objects (e.g. Users), then copying that list into the Tag table is overkill. -It is better to dynamically generate the list of available Tags, and/or dynamically validate a submitted object tag than -to store the options in the database. +Open Taxonomy that depends on a core data model, but simplifies the creation of Tags by allowing free-form tags, + +Rejected because it has been seen that using dynamic tags provides more functionality and more advantages. .. _document number 0013: https://github.com/openedx/openedx-learning/blob/main/docs/decisions/0013-system-taxonomy-auto-tagging.rst From 8ca16eab7ccdcf256fb368b10d3a05ee1a059b83 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jul 2023 12:38:38 -0500 Subject: [PATCH 14/27] style: Updating comments --- openedx_tagging/core/tagging/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models.py index 3e8915661..703fb0bd2 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models.py @@ -772,12 +772,13 @@ class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): """ 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. + This type of taxonomy has an associated Django model in `tag_class_model()`. + They are designed to create Tags when required for new ObjectTags, to maintain + their status as "closed" taxonomies. 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 + Tag.external_id stores an identifier from the instance (`pk` as default) + and Tag.value stores a human readable representation of the instance (e.g. `username`). The subclasses can override this behavior, to choose the right field. From 0c74bb4e2d86d66abad38158d508f6655c71dda5 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Jul 2023 13:15:52 -0500 Subject: [PATCH 15/27] style: Separating the models into base and system defined --- .../core/tagging/models/__init__.py | 10 + .../tagging/{models.py => models/base.py} | 243 +--------------- .../core/tagging/models/system_defined.py | 248 ++++++++++++++++ .../core/tagging/test_models.py | 259 +---------------- .../tagging/test_system_defined_models.py | 268 ++++++++++++++++++ 5 files changed, 530 insertions(+), 498 deletions(-) create mode 100644 openedx_tagging/core/tagging/models/__init__.py rename openedx_tagging/core/tagging/{models.py => models/base.py} (74%) create mode 100644 openedx_tagging/core/tagging/models/system_defined.py create mode 100644 tests/openedx_tagging/core/tagging/test_system_defined_models.py diff --git a/openedx_tagging/core/tagging/models/__init__.py b/openedx_tagging/core/tagging/models/__init__.py new file mode 100644 index 000000000..3dd7ff10f --- /dev/null +++ b/openedx_tagging/core/tagging/models/__init__.py @@ -0,0 +1,10 @@ +from .base import ( + Tag, + Taxonomy, + ObjectTag, +) + +from .system_defined import ( + UserSystemDefinedTaxonomy, + LanguageTaxonomy +) diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models/base.py similarity index 74% rename from openedx_tagging/core/tagging/models.py rename to openedx_tagging/core/tagging/models/base.py index 703fb0bd2..0c52caa12 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -1,9 +1,7 @@ -""" Tagging app data models """ +""" Tagging app base data models """ import logging -from typing import Any, List, Type, Union +from typing import 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 _ @@ -653,240 +651,3 @@ def copy(self, object_tag: "ObjectTag") -> "ObjectTag": self._value = object_tag._value self._name = object_tag._name return self - - -class SystemDefinedTaxonomy(Taxonomy): - """ - Simple subclass of Taxonomy which requires the system_defined flag to be set. - """ - - class Meta: - proxy = True - - def _check_taxonomy(self, object_tag: ObjectTag) -> bool: - """ - Returns True if this is a system defined taxonomy - """ - return super()._check_taxonomy(object_tag) and self.system_defined - - -class ModelObjectTag(ObjectTag): - """ - Model-based ObjectTag, abstract class. - - Used by ModelSystemDefinedTaxonomy to maintain dynamic Tags which are associated with a configured Model instance. - """ - - class Meta: - proxy = True - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """ - Checks if the `tag_class_model` is correct - """ - assert issubclass(self.tag_class_model, models.Model) - super().__init__(*args, **kwargs) - - @property - def tag_class_model(self) -> Type: - """ - Subclasses must implement this method to return the Django.model - class referenced by these object tags. - """ - raise NotImplementedError - - @property - def tag_class_value(self) -> str: - """ - Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. - - Subclasses may override this method to use different fields. - """ - return "pk" - - def get_instance(self) -> Union[models.Model, None]: - """ - Returns the instance of tag_class_model associated with this object tag, or None if not found. - """ - instance_id = self.tag.external_id if self.tag else None - if instance_id: - try: - return self.tag_class_model.objects.get(pk=instance_id) - except self.tag_class_model.DoesNotExist: - log.exception( - f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist." - ) - - return None - - def _resync_tag(self) -> bool: - """ - Resync our tag's value with the value from the instance. - - If the instance associated with the tag no longer exists, we unset our tag, because it's no longer valid. - - Returns True if the given tag was changed, False otherwise. - """ - instance = self.get_instance() - if instance: - value = getattr(instance, self.tag_class_value) - self.value = value - if self.tag and self.tag.value != value: - self.tag.value = value - self.tag.save() - return True - else: - self.tag = None - - return False - - @property - def tag_ref(self) -> str: - return (self.tag.external_id or self.tag.id) if self.tag_id else self._value - - @tag_ref.setter - def tag_ref(self, tag_ref: str): - """ - Sets the ObjectTag's Tag and/or value, depending on whether a valid Tag is found, or can be created. - - Creates a Tag for the given tag_ref value, if one containing that external_id not already exist. - """ - self.value = tag_ref - - if self.taxonomy_id: - try: - self.tag = self.taxonomy.tag_set.get( - external_id=tag_ref, - ) - except (ValueError, Tag.DoesNotExist): - # Creates a new Tag for this instance - self.tag = Tag( - taxonomy=self.taxonomy, - external_id=tag_ref, - ) - - self._resync_tag() - - -class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): - """ - Model based system taxonomy abstract class. - - This type of taxonomy has an associated Django model in `tag_class_model()`. - They are designed to create Tags when required for new ObjectTags, to maintain - their status as "closed" taxonomies. - The Tags are representations of the instances of the associated model. - - Tag.external_id stores an identifier from the instance (`pk` as default) - and Tag.value stores a 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 __init__(self, *args: Any, **kwargs: Any) -> None: - """ - Checks if the `object_tag_class` is a subclass of ModelObjectTag. - """ - assert issubclass(self.object_tag_class, ModelObjectTag) - super().__init__(*args, **kwargs) - - @property - def object_tag_class(self) -> Type: - """ - Returns the ObjectTag subclass associated with this taxonomy. - - Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. - """ - raise NotImplementedError - - -class UserModelObjectTag(ModelObjectTag): - """ - ObjectTags for the UserSystemDefinedTaxonomy. - """ - - class Meta: - proxy = True - - @property - def tag_class_model(self) -> Type: - """ - Associate the user model - """ - return get_user_model() - - @property - def tag_class_value(self) -> str: - """ - Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. - - Subclasses may override this method to use different fields. - """ - return "username" - - -class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): - """ - User based system taxonomy class. - """ - - class Meta: - proxy = True - - @property - def object_tag_class(self) -> Type: - """ - Returns the ObjectTag subclass associated with this taxonomy, which is ModelObjectTag by default. - - Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. - """ - return UserModelObjectTag - - -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(self, tag_set: models.QuerySet = None) -> List[Tag]: - """ - Returns a list of all the available Language Tags, annotated with ``depth`` = 0. - """ - available_langs = self._get_available_languages() - tag_set = self.tag_set.filter(external_id__in=available_langs) - return super().get_tags(tag_set=tag_set) - - 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/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py new file mode 100644 index 000000000..adb4059b6 --- /dev/null +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -0,0 +1,248 @@ +""" Tagging app system-defined taxonomies data models """ +import logging +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 .base import Tag, Taxonomy, ObjectTag + +log = logging.getLogger(__name__) + + +class SystemDefinedTaxonomy(Taxonomy): + """ + Simple subclass of Taxonomy which requires the system_defined flag to be set. + """ + + class Meta: + proxy = True + + def _check_taxonomy(self, object_tag: ObjectTag) -> bool: + """ + Returns True if this is a system defined taxonomy + """ + return super()._check_taxonomy(object_tag) and self.system_defined + + +class ModelObjectTag(ObjectTag): + """ + Model-based ObjectTag, abstract class. + + Used by ModelSystemDefinedTaxonomy to maintain dynamic Tags which are associated with a configured Model instance. + """ + + class Meta: + proxy = True + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """ + Checks if the `tag_class_model` is correct + """ + assert issubclass(self.tag_class_model, models.Model) + super().__init__(*args, **kwargs) + + @property + def tag_class_model(self) -> Type: + """ + Subclasses must implement this method to return the Django.model + class referenced by these object tags. + """ + raise NotImplementedError + + @property + def tag_class_value(self) -> str: + """ + Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. + + Subclasses may override this method to use different fields. + """ + return "pk" + + def get_instance(self) -> Union[models.Model, None]: + """ + Returns the instance of tag_class_model associated with this object tag, or None if not found. + """ + instance_id = self.tag.external_id if self.tag else None + if instance_id: + try: + return self.tag_class_model.objects.get(pk=instance_id) + except self.tag_class_model.DoesNotExist: + log.exception( + f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist." + ) + + return None + + def _resync_tag(self) -> bool: + """ + Resync our tag's value with the value from the instance. + + If the instance associated with the tag no longer exists, we unset our tag, because it's no longer valid. + + Returns True if the given tag was changed, False otherwise. + """ + instance = self.get_instance() + if instance: + value = getattr(instance, self.tag_class_value) + self.value = value + if self.tag and self.tag.value != value: + self.tag.value = value + self.tag.save() + return True + else: + self.tag = None + + return False + + @property + def tag_ref(self) -> str: + return (self.tag.external_id or self.tag.id) if self.tag_id else self._value + + @tag_ref.setter + def tag_ref(self, tag_ref: str): + """ + Sets the ObjectTag's Tag and/or value, depending on whether a valid Tag is found, or can be created. + + Creates a Tag for the given tag_ref value, if one containing that external_id not already exist. + """ + self.value = tag_ref + + if self.taxonomy_id: + try: + self.tag = self.taxonomy.tag_set.get( + external_id=tag_ref, + ) + except (ValueError, Tag.DoesNotExist): + # Creates a new Tag for this instance + self.tag = Tag( + taxonomy=self.taxonomy, + external_id=tag_ref, + ) + + self._resync_tag() + + +class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): + """ + Model based system taxonomy abstract class. + + This type of taxonomy has an associated Django model in `tag_class_model()`. + They are designed to create Tags when required for new ObjectTags, to maintain + their status as "closed" taxonomies. + The Tags are representations of the instances of the associated model. + + Tag.external_id stores an identifier from the instance (`pk` as default) + and Tag.value stores a 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 __init__(self, *args: Any, **kwargs: Any) -> None: + """ + Checks if the `object_tag_class` is a subclass of ModelObjectTag. + """ + assert issubclass(self.object_tag_class, ModelObjectTag) + super().__init__(*args, **kwargs) + + @property + def object_tag_class(self) -> Type: + """ + Returns the ObjectTag subclass associated with this taxonomy. + + Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. + """ + raise NotImplementedError + + +class UserModelObjectTag(ModelObjectTag): + """ + ObjectTags for the UserSystemDefinedTaxonomy. + """ + + class Meta: + proxy = True + + @property + def tag_class_model(self) -> Type: + """ + Associate the user model + """ + return get_user_model() + + @property + def tag_class_value(self) -> str: + """ + Returns the name of the tag_class_model field to use as the Tag.value when creating Tags for this taxonomy. + + Subclasses may override this method to use different fields. + """ + return "username" + + +class UserSystemDefinedTaxonomy(ModelSystemDefinedTaxonomy): + """ + User based system taxonomy class. + """ + + class Meta: + proxy = True + + @property + def object_tag_class(self) -> Type: + """ + Returns the ObjectTag subclass associated with this taxonomy, which is ModelObjectTag by default. + + Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. + """ + return UserModelObjectTag + + +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(self, tag_set: models.QuerySet = None) -> List[Tag]: + """ + Returns a list of all the available Language Tags, annotated with ``depth`` = 0. + """ + available_langs = self._get_available_languages() + tag_set = self.tag_set.filter(external_id__in=available_langs) + return super().get_tags(tag_set=tag_set) + + 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/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index e5c65b70d..8d84bf0b5 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -1,78 +1,14 @@ -""" Test the tagging models """ - +""" Test the tagging base models """ import ddt from django.contrib.auth import get_user_model -from django.test.testcases import TestCase, override_settings +from django.test.testcases import TestCase from openedx_tagging.core.tagging.models import ( - ModelObjectTag, - ModelSystemDefinedTaxonomy, ObjectTag, - SystemDefinedTaxonomy, Tag, Taxonomy, - 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 object_tag_class(self): - return EmptyTestClass - - class Meta: - proxy = True - managed = False - app_label = "oel_tagging" - - -class TestModelTag(ModelObjectTag): - """ - Model used for testing - """ - - @property - def tag_class_model(self): - return get_user_model() - - class Meta: - proxy = True - managed = False - app_label = "oel_tagging" - - -class TestModelTaxonomy(ModelSystemDefinedTaxonomy): - """ - Model used for testing - """ - - @property - def object_tag_class(self): - return TestModelTag - - class Meta: - proxy = True - managed = False - app_label = "oel_tagging" - def get_tag(value): """ @@ -492,194 +428,3 @@ 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), - (ModelObjectTag, 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", - ) - - def test_tag_ref(self): - object_tag = TestModelTag() - object_tag.tag_ref = 1 - object_tag.save() - assert object_tag.tag is None - assert object_tag.value == 1 - - def test_get_instance(self): - object_tag = TestModelTag() - assert object_tag.get_instance() is None - - -@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_models.py b/tests/openedx_tagging/core/tagging/test_system_defined_models.py new file mode 100644 index 000000000..80baf8888 --- /dev/null +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -0,0 +1,268 @@ +""" Test the tagging system-defined taxonomy models """ +import ddt + +from django.test.testcases import TestCase, override_settings +from django.contrib.auth import get_user_model + +from openedx_tagging.core.tagging.models import ( + ObjectTag, + Tag, +) +from openedx_tagging.core.tagging.models.system_defined import ( + ModelObjectTag, + ModelSystemDefinedTaxonomy, + SystemDefinedTaxonomy, + UserSystemDefinedTaxonomy, +) + +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 InvalidModelTaxonomy(ModelSystemDefinedTaxonomy): + """ + Model used for testing + """ + + @property + def object_tag_class(self): + return EmptyTestClass + + class Meta: + proxy = True + managed = False + app_label = "oel_tagging" + + +class TestModelTag(ModelObjectTag): + """ + Model used for testing + """ + + @property + def tag_class_model(self): + return get_user_model() + + class Meta: + proxy = True + managed = False + app_label = "oel_tagging" + + +class TestModelTaxonomy(ModelSystemDefinedTaxonomy): + """ + Model used for testing + """ + + @property + def object_tag_class(self): + return TestModelTag + + class Meta: + proxy = True + managed = False + app_label = "oel_tagging" + +@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), + (ModelObjectTag, 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", + ) + + def test_tag_ref(self): + object_tag = TestModelTag() + object_tag.tag_ref = 1 + object_tag.save() + assert object_tag.tag is None + assert object_tag.value == 1 + + def test_get_instance(self): + object_tag = TestModelTag() + assert object_tag.get_instance() is None + + +@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 \ No newline at end of file From 9a380476c50d35457ee5df1404fedc34cecc0332 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 21 Jul 2023 10:53:21 -0500 Subject: [PATCH 16/27] fix: Update language fixture to negative pk --- .../tagging/fixtures/language_taxonomy.yaml | 368 +++++++++--------- .../commands/build_language_fixture.py | 3 +- 2 files changed, 186 insertions(+), 185 deletions(-) diff --git a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml index 06d51e150..ac2c934e3 100644 --- a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml +++ b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml @@ -1,1288 +1,1288 @@ - model: oel_tagging.tag pk: -1 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Afar external_id: aa - model: oel_tagging.tag pk: -2 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Abkhazian external_id: ab - model: oel_tagging.tag pk: -3 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Avestan external_id: ae - model: oel_tagging.tag pk: -4 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Afrikaans external_id: af - model: oel_tagging.tag pk: -5 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Akan external_id: ak - model: oel_tagging.tag pk: -6 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Amharic external_id: am - model: oel_tagging.tag pk: -7 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Aragonese external_id: an - model: oel_tagging.tag pk: -8 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Arabic external_id: ar - model: oel_tagging.tag pk: -9 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Assamese external_id: as - model: oel_tagging.tag pk: -10 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Avaric external_id: av - model: oel_tagging.tag pk: -11 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Aymara external_id: ay - model: oel_tagging.tag pk: -12 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Azerbaijani external_id: az - model: oel_tagging.tag pk: -13 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bashkir external_id: ba - model: oel_tagging.tag pk: -14 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Belarusian external_id: be - model: oel_tagging.tag pk: -15 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bulgarian external_id: bg - model: oel_tagging.tag pk: -16 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bihari languages external_id: bh - model: oel_tagging.tag pk: -17 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bislama external_id: bi - model: oel_tagging.tag pk: -18 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bambara external_id: bm - model: oel_tagging.tag pk: -19 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bengali external_id: bn - model: oel_tagging.tag pk: -20 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tibetan external_id: bo - model: oel_tagging.tag pk: -21 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Breton external_id: br - model: oel_tagging.tag pk: -22 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bosnian external_id: bs - model: oel_tagging.tag pk: -23 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Catalan external_id: ca - model: oel_tagging.tag pk: -24 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Chechen external_id: ce - model: oel_tagging.tag pk: -25 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Chamorro external_id: ch - model: oel_tagging.tag pk: -26 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Corsican external_id: co - model: oel_tagging.tag pk: -27 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Cree external_id: cr - model: oel_tagging.tag pk: -28 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Czech external_id: cs - model: oel_tagging.tag pk: -29 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Church Slavic external_id: cu - model: oel_tagging.tag pk: -30 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Chuvash external_id: cv - model: oel_tagging.tag pk: -31 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Welsh external_id: cy - model: oel_tagging.tag pk: -32 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Danish external_id: da - model: oel_tagging.tag pk: -33 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: German external_id: de - model: oel_tagging.tag pk: -34 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Divehi external_id: dv - model: oel_tagging.tag pk: -35 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Dzongkha external_id: dz - model: oel_tagging.tag pk: -36 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ewe external_id: ee - model: oel_tagging.tag pk: -37 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Greek, Modern (1453-) external_id: el - model: oel_tagging.tag pk: -38 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: English external_id: en - model: oel_tagging.tag pk: -39 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Esperanto external_id: eo - model: oel_tagging.tag pk: -40 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Spanish external_id: es - model: oel_tagging.tag pk: -41 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Estonian external_id: et - model: oel_tagging.tag pk: -42 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Basque external_id: eu - model: oel_tagging.tag pk: -43 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Persian external_id: fa - model: oel_tagging.tag pk: -44 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Fulah external_id: ff - model: oel_tagging.tag pk: -45 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Finnish external_id: fi - model: oel_tagging.tag pk: -46 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Fijian external_id: fj - model: oel_tagging.tag pk: -47 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Faroese external_id: fo - model: oel_tagging.tag pk: -48 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: French external_id: fr - model: oel_tagging.tag pk: -49 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Western Frisian external_id: fy - model: oel_tagging.tag pk: -50 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Irish external_id: ga - model: oel_tagging.tag pk: -51 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Gaelic external_id: gd - model: oel_tagging.tag pk: -52 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Galician external_id: gl - model: oel_tagging.tag pk: -53 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Guarani external_id: gn - model: oel_tagging.tag pk: -54 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Gujarati external_id: gu - model: oel_tagging.tag pk: -55 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Manx external_id: gv - model: oel_tagging.tag pk: -56 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Hausa external_id: ha - model: oel_tagging.tag pk: -57 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Hebrew external_id: he - model: oel_tagging.tag pk: -58 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Hindi external_id: hi - model: oel_tagging.tag pk: -59 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Hiri Motu external_id: ho - model: oel_tagging.tag pk: -60 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Croatian external_id: hr - model: oel_tagging.tag pk: -61 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Haitian external_id: ht - model: oel_tagging.tag pk: -62 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Hungarian external_id: hu - model: oel_tagging.tag pk: -63 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Armenian external_id: hy - model: oel_tagging.tag pk: -64 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Herero external_id: hz - model: oel_tagging.tag pk: -65 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Interlingua (International Auxiliary Language Association) external_id: ia - model: oel_tagging.tag pk: -66 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Indonesian external_id: id - model: oel_tagging.tag pk: -67 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Interlingue external_id: ie - model: oel_tagging.tag pk: -68 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Igbo external_id: ig - model: oel_tagging.tag pk: -69 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sichuan Yi external_id: ii - model: oel_tagging.tag pk: -70 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Inupiaq external_id: ik - model: oel_tagging.tag pk: -71 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ido external_id: io - model: oel_tagging.tag pk: -72 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Icelandic external_id: is - model: oel_tagging.tag pk: -73 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Italian external_id: it - model: oel_tagging.tag pk: -74 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Inuktitut external_id: iu - model: oel_tagging.tag pk: -75 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Japanese external_id: ja - model: oel_tagging.tag pk: -76 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Javanese external_id: jv - model: oel_tagging.tag pk: -77 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Georgian external_id: ka - model: oel_tagging.tag pk: -78 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kongo external_id: kg - model: oel_tagging.tag pk: -79 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kikuyu external_id: ki - model: oel_tagging.tag pk: -80 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kuanyama external_id: kj - model: oel_tagging.tag pk: -81 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kazakh external_id: kk - model: oel_tagging.tag pk: -82 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kalaallisut external_id: kl - model: oel_tagging.tag pk: -83 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Central Khmer external_id: km - model: oel_tagging.tag pk: -84 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kannada external_id: kn - model: oel_tagging.tag pk: -85 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Korean external_id: ko - model: oel_tagging.tag pk: -86 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kanuri external_id: kr - model: oel_tagging.tag pk: -87 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kashmiri external_id: ks - model: oel_tagging.tag pk: -88 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kurdish external_id: ku - model: oel_tagging.tag pk: -89 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Komi external_id: kv - model: oel_tagging.tag pk: -90 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Cornish external_id: kw - model: oel_tagging.tag pk: -91 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kirghiz external_id: ky - model: oel_tagging.tag pk: -92 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Latin external_id: la - model: oel_tagging.tag pk: -93 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Luxembourgish external_id: lb - model: oel_tagging.tag pk: -94 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ganda external_id: lg - model: oel_tagging.tag pk: -95 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Limburgan external_id: li - model: oel_tagging.tag pk: -96 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Lingala external_id: ln - model: oel_tagging.tag pk: -97 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Lao external_id: lo - model: oel_tagging.tag pk: -98 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Lithuanian external_id: lt - model: oel_tagging.tag pk: -99 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Luba-Katanga external_id: lu - model: oel_tagging.tag pk: -100 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Latvian external_id: lv - model: oel_tagging.tag pk: -101 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Malagasy external_id: mg - model: oel_tagging.tag pk: -102 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Marshallese external_id: mh - model: oel_tagging.tag pk: -103 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Maori external_id: mi - model: oel_tagging.tag pk: -104 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Macedonian external_id: mk - model: oel_tagging.tag pk: -105 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Malayalam external_id: ml - model: oel_tagging.tag pk: -106 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Mongolian external_id: mn - model: oel_tagging.tag pk: -107 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Marathi external_id: mr - model: oel_tagging.tag pk: -108 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Malay external_id: ms - model: oel_tagging.tag pk: -109 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Maltese external_id: mt - model: oel_tagging.tag pk: -110 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Burmese external_id: my - model: oel_tagging.tag pk: -111 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Nauru external_id: na - model: oel_tagging.tag pk: -112 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Bokmål, Norwegian external_id: nb - model: oel_tagging.tag pk: -113 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ndebele, North external_id: nd - model: oel_tagging.tag pk: -114 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Nepali external_id: ne - model: oel_tagging.tag pk: -115 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ndonga external_id: ng - model: oel_tagging.tag pk: -116 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Dutch external_id: nl - model: oel_tagging.tag pk: -117 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Norwegian Nynorsk external_id: nn - model: oel_tagging.tag pk: -118 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Norwegian external_id: no - model: oel_tagging.tag pk: -119 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ndebele, South external_id: nr - model: oel_tagging.tag pk: -120 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Navajo external_id: nv - model: oel_tagging.tag pk: -121 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Chichewa external_id: ny - model: oel_tagging.tag pk: -122 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Occitan (post 1500) external_id: oc - model: oel_tagging.tag pk: -123 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ojibwa external_id: oj - model: oel_tagging.tag pk: -124 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Oromo external_id: om - model: oel_tagging.tag pk: -125 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Oriya external_id: or - model: oel_tagging.tag pk: -126 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ossetian external_id: os - model: oel_tagging.tag pk: -127 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Panjabi external_id: pa - model: oel_tagging.tag pk: -128 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Pali external_id: pi - model: oel_tagging.tag pk: -129 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Polish external_id: pl - model: oel_tagging.tag pk: -130 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Pushto external_id: ps - model: oel_tagging.tag pk: -131 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Portuguese external_id: pt - model: oel_tagging.tag pk: -132 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Quechua external_id: qu - model: oel_tagging.tag pk: -133 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Romansh external_id: rm - model: oel_tagging.tag pk: -134 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Rundi external_id: rn - model: oel_tagging.tag pk: -135 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Romanian external_id: ro - model: oel_tagging.tag pk: -136 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Russian external_id: ru - model: oel_tagging.tag pk: -137 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Kinyarwanda external_id: rw - model: oel_tagging.tag pk: -138 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sanskrit external_id: sa - model: oel_tagging.tag pk: -139 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sardinian external_id: sc - model: oel_tagging.tag pk: -140 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sindhi external_id: sd - model: oel_tagging.tag pk: -141 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Northern Sami external_id: se - model: oel_tagging.tag pk: -142 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sango external_id: sg - model: oel_tagging.tag pk: -143 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sinhala external_id: si - model: oel_tagging.tag pk: -144 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Slovak external_id: sk - model: oel_tagging.tag pk: -145 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Slovenian external_id: sl - model: oel_tagging.tag pk: -146 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Samoan external_id: sm - model: oel_tagging.tag pk: -147 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Shona external_id: sn - model: oel_tagging.tag pk: -148 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Somali external_id: so - model: oel_tagging.tag pk: -149 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Albanian external_id: sq - model: oel_tagging.tag pk: -150 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Serbian external_id: sr - model: oel_tagging.tag pk: -151 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Swati external_id: ss - model: oel_tagging.tag pk: -152 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sotho, Southern external_id: st - model: oel_tagging.tag pk: -153 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Sundanese external_id: su - model: oel_tagging.tag pk: -154 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Swedish external_id: sv - model: oel_tagging.tag pk: -155 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Swahili external_id: sw - model: oel_tagging.tag pk: -156 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tamil external_id: ta - model: oel_tagging.tag pk: -157 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Telugu external_id: te - model: oel_tagging.tag pk: -158 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tajik external_id: tg - model: oel_tagging.tag pk: -159 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Thai external_id: th - model: oel_tagging.tag pk: -160 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tigrinya external_id: ti - model: oel_tagging.tag pk: -161 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Turkmen external_id: tk - model: oel_tagging.tag pk: -162 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tagalog external_id: tl - model: oel_tagging.tag pk: -163 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tswana external_id: tn - model: oel_tagging.tag pk: -164 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tonga (Tonga Islands) external_id: to - model: oel_tagging.tag pk: -165 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Turkish external_id: tr - model: oel_tagging.tag pk: -166 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tsonga external_id: ts - model: oel_tagging.tag pk: -167 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tatar external_id: tt - model: oel_tagging.tag pk: -168 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Twi external_id: tw - model: oel_tagging.tag pk: -169 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Tahitian external_id: ty - model: oel_tagging.tag pk: -170 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Uighur external_id: ug - model: oel_tagging.tag pk: -171 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Ukrainian external_id: uk - model: oel_tagging.tag pk: -172 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Urdu external_id: ur - model: oel_tagging.tag pk: -173 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Uzbek external_id: uz - model: oel_tagging.tag pk: -174 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Venda external_id: ve - model: oel_tagging.tag pk: -175 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Vietnamese external_id: vi - model: oel_tagging.tag pk: -176 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Volapük external_id: vo - model: oel_tagging.tag pk: -177 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Walloon external_id: wa - model: oel_tagging.tag pk: -178 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Wolof external_id: wo - model: oel_tagging.tag pk: -179 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Xhosa external_id: xh - model: oel_tagging.tag pk: -180 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Yiddish external_id: yi - model: oel_tagging.tag pk: -181 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Yoruba external_id: yo - model: oel_tagging.tag pk: -182 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Zhuang external_id: za - model: oel_tagging.tag pk: -183 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Chinese external_id: zh - model: oel_tagging.tag pk: -184 fields: - taxonomy: 1 + taxonomy: -1 parent: null value: Zulu external_id: zu diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index ed54bff98..2acd178fa 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -33,12 +33,13 @@ def build_fixture(self, json_data): output_file.write("- model: oel_tagging.tag\n") output_file.write(f" pk: {tag_pk}\n") output_file.write(" fields:\n") - output_file.write(" taxonomy: 1\n") + output_file.write(" taxonomy: -1\n") output_file.write(" parent: null\n") output_file.write(f" value: {lang_value}\n") output_file.write(f" external_id: {lang_code}\n") # System tags are identified with negative numbers to avoid clashing with user-created tags. tag_pk -= 1 + def get_lang_value(self, lang_data): """ From 1847248b48176014b989a25e03ec00b9dda5f26e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 21 Jul 2023 12:39:23 -0500 Subject: [PATCH 17/27] feat: Updates on Taxonomy and Tag admins - Taxonomy.system_defined updated to be a hardcoded property. - TaxonomyAdmin updated to avoid edit and delete system taxonomies. - TagAdmin updated to avoid edit and delete tags from system taxonomies. - TagAmdin updated to avoid create Tags on system taxonomies. --- openedx_tagging/core/tagging/admin.py | 95 ++++++++++++++++++- .../core/tagging/fixtures/system_defined.yaml | 11 --- .../migrations/0003_auto_20230721_1238.py | 83 ++++++++++++++++ .../tagging/migrations/0003_system_defined.py | 72 -------------- openedx_tagging/core/tagging/models/base.py | 25 +++-- .../core/tagging/models/system_defined.py | 8 +- .../core/fixtures/tagging.yaml | 10 +- .../openedx_tagging/core/tagging/test_api.py | 2 +- .../tagging/test_system_defined_models.py | 33 +------ 9 files changed, 202 insertions(+), 137 deletions(-) delete mode 100644 openedx_tagging/core/tagging/fixtures/system_defined.yaml create mode 100644 openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py delete mode 100644 openedx_tagging/core/tagging/migrations/0003_system_defined.py diff --git a/openedx_tagging/core/tagging/admin.py b/openedx_tagging/core/tagging/admin.py index 91b1d753d..39363af5e 100644 --- a/openedx_tagging/core/tagging/admin.py +++ b/openedx_tagging/core/tagging/admin.py @@ -1,8 +1,99 @@ """ Tagging app admin """ +from django import forms from django.contrib import admin from .models import ObjectTag, Tag, Taxonomy -admin.site.register(Taxonomy) -admin.site.register(Tag) + +def check_taxonomy(taxonomy: Taxonomy): + """ + Checks if the taxonomy is valid to edit or delete + """ + taxonomy = taxonomy.cast() + return not taxonomy.system_defined + + +class TaxonomyAdmin(admin.ModelAdmin): + """ + Admin for Taxonomy Model + """ + + def has_change_permission(self, request, obj=None): + """ + Avoid edit system-defined taxonomies + """ + if obj is not None: + return check_taxonomy(taxonomy=obj) + return super().has_change_permission(request, obj) + + def has_delete_permission(self, request, obj=None): + """ + Avoid delete system-defined taxonomies + """ + if obj is not None: + return check_taxonomy(taxonomy=obj) + return super().has_change_permission(request, obj) + + +class TagForm(forms.ModelForm): + """ + Form for create a Tag + """ + class Meta: + model = Tag + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + print(self.fields) + if 'taxonomy' in self.fields: + self.fields['taxonomy'].queryset = self._filter_taxonomies() + + def _filter_taxonomies(self): + """ + Returns taxonomies that allows Tag creation + + - Not allow free text + - Not system defined + """ + taxonomy_queryset = Taxonomy.objects.filter( + allow_free_text=False + ) + valid_taxonomy_ids = [ + taxonomy.id for taxonomy + in taxonomy_queryset if check_taxonomy(taxonomy) + ] + + return taxonomy_queryset.filter(id__in=valid_taxonomy_ids) + + +class TagAdmin(admin.ModelAdmin): + """ + Admin for Tag Model + """ + form = TagForm + + def has_change_permission(self, request, obj=None): + """ + Avoid edit system-defined taxonomies + """ + if obj is not None: + taxonomy = obj.taxonomy + if taxonomy: + return check_taxonomy(taxonomy) + return super().has_change_permission(request, obj) + + def has_delete_permission(self, request, obj=None): + """ + Avoid delete system-defined taxonomies + """ + if obj is not None: + taxonomy = obj.taxonomy + if taxonomy: + return check_taxonomy(taxonomy) + return super().has_change_permission(request, obj) + + +admin.site.register(Taxonomy, TaxonomyAdmin) +admin.site.register(Tag, TagAdmin) admin.site.register(ObjectTag) diff --git a/openedx_tagging/core/tagging/fixtures/system_defined.yaml b/openedx_tagging/core/tagging/fixtures/system_defined.yaml deleted file mode 100644 index 6c085152e..000000000 --- a/openedx_tagging/core/tagging/fixtures/system_defined.yaml +++ /dev/null @@ -1,11 +0,0 @@ -- model: oel_tagging.taxonomy - pk: -1 - fields: - name: Languages - description: ISO 639-1 Languages. Allows tags for any language configured for use on the instance - enabled: true - required: true - allow_multiple: false - allow_free_text: false - system_defined: true - visible_to_authors: true diff --git a/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py b/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py new file mode 100644 index 000000000..16920a1b0 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py @@ -0,0 +1,83 @@ +# Generated by Django 3.2.19 on 2023-07-21 17:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0002_auto_20230718_2026'), + ] + + operations = [ + migrations.CreateModel( + name='ModelObjectTag', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.objecttag',), + ), + migrations.CreateModel( + name='SystemDefinedTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.taxonomy',), + ), + migrations.RemoveField( + model_name='taxonomy', + name='system_defined', + ), + 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='UserModelObjectTag', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.modelobjecttag',), + ), + migrations.CreateModel( + name='UserSystemDefinedTaxonomy', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('oel_tagging.modelsystemdefinedtaxonomy',), + ), + ] diff --git a/openedx_tagging/core/tagging/migrations/0003_system_defined.py b/openedx_tagging/core/tagging/migrations/0003_system_defined.py deleted file mode 100644 index 8a8bf1d44..000000000 --- a/openedx_tagging/core/tagging/migrations/0003_system_defined.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 3.2.19 on 2023-07-19 17:03 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("oel_tagging", "0002_auto_20230718_2026"), - ] - - operations = [ - migrations.CreateModel( - name="ModelObjectTag", - fields=[], - options={ - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("oel_tagging.objecttag",), - ), - 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="UserModelObjectTag", - fields=[], - options={ - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("oel_tagging.modelobjecttag",), - ), - migrations.CreateModel( - name="UserSystemDefinedTaxonomy", - fields=[], - options={ - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("oel_tagging.modelsystemdefinedtaxonomy",), - ), - ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 0c52caa12..8af03796e 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -140,14 +140,6 @@ class Taxonomy(models.Model): "Indicates that tags in this taxonomy need not be predefined; authors may enter their own tag values." ), ) - system_defined = models.BooleanField( - default=False, - editable=False, - help_text=_( - "Indicates that tags and metadata for this taxonomy are maintained by the system;" - " taxonomy admins will not be permitted to modify them.", - ), - ) visible_to_authors = models.BooleanField( default=True, editable=False, @@ -177,6 +169,14 @@ def __str__(self): """ User-facing string representation of a Taxonomy. """ + try: + if self._taxonomy_class: + return f"<{self.taxonomy_class.__name__}> ({self.id}) {self.name}" + except ImportError: + # Log error and continue + log.exception( + f"Unable to import taxonomy_class for {self.id}: {self._taxonomy_class}" + ) return f"<{self.__class__.__name__}> ({self.id}) {self.name}" @property @@ -198,6 +198,14 @@ def taxonomy_class(self) -> Type: if self._taxonomy_class: return import_string(self._taxonomy_class) return None + + @property + def system_defined(self) -> bool: + """ + Indicates that tags and metadata for this taxonomy are maintained by the system; + taxonomy admins will not be permitted to modify them. + """ + return False @taxonomy_class.setter def taxonomy_class(self, taxonomy_class: Union[Type, None]): @@ -248,7 +256,6 @@ def copy(self, taxonomy: "Taxonomy") -> "Taxonomy": self.required = taxonomy.required self.allow_multiple = taxonomy.allow_multiple self.allow_free_text = taxonomy.allow_free_text - self.system_defined = taxonomy.system_defined self.visible_to_authors = taxonomy.visible_to_authors self._taxonomy_class = taxonomy._taxonomy_class return self diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index adb4059b6..252a8b7d0 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -19,11 +19,13 @@ class SystemDefinedTaxonomy(Taxonomy): class Meta: proxy = True - def _check_taxonomy(self, object_tag: ObjectTag) -> bool: + @property + def system_defined(self) -> bool: """ - Returns True if this is a system defined taxonomy + Indicates that tags and metadata for this taxonomy are maintained by the system; + taxonomy admins will not be permitted to modify them. """ - return super()._check_taxonomy(object_tag) and self.system_defined + return True class ModelObjectTag(ObjectTag): diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index 90bca3978..4633901ad 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -161,7 +161,6 @@ required: false allow_multiple: false allow_free_text: false - system_defined: false - model: oel_tagging.taxonomy pk: 2 fields: @@ -171,8 +170,7 @@ required: false allow_multiple: false allow_free_text: false - system_defined: true - _taxonomy_class: openedx_tagging.core.tagging.models.LanguageTaxonomy + _taxonomy_class: openedx_tagging.core.tagging.models.system_defined.LanguageTaxonomy - model: oel_tagging.taxonomy pk: 3 fields: @@ -182,8 +180,7 @@ required: false allow_multiple: false allow_free_text: false - system_defined: true - _taxonomy_class: openedx_tagging.core.tagging.models.UserSystemDefinedTaxonomy + _taxonomy_class: openedx_tagging.core.tagging.models.system_defined.UserSystemDefinedTaxonomy - model: oel_tagging.taxonomy pk: 4 fields: @@ -193,5 +190,4 @@ required: false allow_multiple: false allow_free_text: false - system_defined: true - _taxonomy_class: openedx_tagging.core.tagging.models.SystemDefinedTaxonomy + _taxonomy_class: openedx_tagging.core.tagging.models.system_defined.SystemDefinedTaxonomy diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 521c48140..654da8460 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -57,7 +57,7 @@ def test_get_taxonomies(self): ] assert str(enabled[0]) == f" ({tax1.id}) Enabled" assert str(enabled[1]) == " (1) Life on Earth" - assert str(enabled[2]) == " (4) System defined taxonomy" + assert str(enabled[2]) == " (4) System defined taxonomy" with self.assertNumQueries(1): disabled = list(tagging_api.get_taxonomies(enabled=False)) 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 80baf8888..a361e01ea 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -11,7 +11,6 @@ from openedx_tagging.core.tagging.models.system_defined import ( ModelObjectTag, ModelSystemDefinedTaxonomy, - SystemDefinedTaxonomy, UserSystemDefinedTaxonomy, ) @@ -77,35 +76,6 @@ class Meta: managed = False app_label = "oel_tagging" -@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): @@ -175,7 +145,6 @@ def test_tag_object_tag_creation(self): taxonomy = TestModelTaxonomy( name="Test", description="Test", - system_defined=True, ) taxonomy.save() assert taxonomy.tag_set.count() == 0 @@ -265,4 +234,4 @@ 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 \ No newline at end of file + assert tag.annotated_field == 0 From eaaba78d33cacc2caf17d708ecb0b82806c44da4 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 21 Jul 2023 15:03:38 -0500 Subject: [PATCH 18/27] feat: Instance validations on ModelSystemDefinedTaxonomy --- .../core/tagging/models/__init__.py | 5 +++-- .../core/tagging/models/system_defined.py | 19 ++++++++++++++++++- .../tagging/test_system_defined_models.py | 12 ++++++------ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/openedx_tagging/core/tagging/models/__init__.py b/openedx_tagging/core/tagging/models/__init__.py index 3dd7ff10f..90640ddfc 100644 --- a/openedx_tagging/core/tagging/models/__init__.py +++ b/openedx_tagging/core/tagging/models/__init__.py @@ -3,8 +3,9 @@ Taxonomy, ObjectTag, ) - from .system_defined import ( + ModelObjectTag, + ModelSystemDefinedTaxonomy, UserSystemDefinedTaxonomy, - LanguageTaxonomy + LanguageTaxonomy, ) diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index 252a8b7d0..06db79795 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -6,6 +6,8 @@ from django.contrib.auth import get_user_model from django.db import models +from openedx_tagging.core.tagging.models.base import ObjectTag + from .base import Tag, Taxonomy, ObjectTag log = logging.getLogger(__name__) @@ -130,7 +132,7 @@ class ModelSystemDefinedTaxonomy(SystemDefinedTaxonomy): """ Model based system taxonomy abstract class. - This type of taxonomy has an associated Django model in `tag_class_model()`. + This type of taxonomy has an associated Django model in ModelObjectTag.tag_class_model(). They are designed to create Tags when required for new ObjectTags, to maintain their status as "closed" taxonomies. The Tags are representations of the instances of the associated model. @@ -162,6 +164,21 @@ def object_tag_class(self) -> Type: Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. """ raise NotImplementedError + + def _check_instance(self, object_tag: ObjectTag) -> bool: + """ + Returns True if the instance exists + + Subclasses can override this method to perform their own instance validation checks. + """ + object_tag = self.object_tag_class.cast(object_tag) + return bool(object_tag.get_instance()) + + def _check_tag(self, object_tag: ObjectTag) -> bool: + """ + Returns True if the instance is valid + """ + return super()._check_tag(object_tag) and self._check_instance(object_tag) class UserModelObjectTag(ModelObjectTag): 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 a361e01ea..6ca1ab43c 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -98,12 +98,12 @@ def test_implementation_error(self, taxonomy_cls, expected_exception): taxonomy_cls() @ddt.data( - (1, "tag_id"), # Valid - (0, "tag_id"), # Invalid user - (1, None), # Testing parent validations + (1, "tag_id", True), # Valid + (0, "tag_id", False), # Invalid user + (1, None, False), # Testing parent validations ) @ddt.unpack - def test_validations(self, tag_external_id, tag_id): + def test_validations(self, tag_external_id, tag_id, expected): tag = Tag( id=tag_id, taxonomy=self.user_taxonomy, @@ -115,12 +115,12 @@ def test_validations(self, tag_external_id, tag_id): tag=tag, ) - self.user_taxonomy.validate_object_tag( + assert self.user_taxonomy.validate_object_tag( object_tag=object_tag, check_object=False, check_taxonomy=False, check_tag=True, - ) + ) == expected def test_tag_object_invalid_user(self): # Test user that doesn't exist From bfc532ab45d6196b26fa82f860bf3d02e3056d95 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Mon, 24 Jul 2023 10:28:00 +0930 Subject: [PATCH 19/27] feat: use Taxonomy.cast and ObjectTag.cast in rules to ensure that we're using the correct Taxonomy and ObjectTag class types --- openedx_tagging/core/tagging/admin.py | 95 +------------------ openedx_tagging/core/tagging/rules.py | 25 ++--- projects/dev.py | 1 + .../core/tagging/test_models.py | 4 +- 4 files changed, 19 insertions(+), 106 deletions(-) diff --git a/openedx_tagging/core/tagging/admin.py b/openedx_tagging/core/tagging/admin.py index 39363af5e..91b1d753d 100644 --- a/openedx_tagging/core/tagging/admin.py +++ b/openedx_tagging/core/tagging/admin.py @@ -1,99 +1,8 @@ """ Tagging app admin """ -from django import forms from django.contrib import admin from .models import ObjectTag, Tag, Taxonomy - -def check_taxonomy(taxonomy: Taxonomy): - """ - Checks if the taxonomy is valid to edit or delete - """ - taxonomy = taxonomy.cast() - return not taxonomy.system_defined - - -class TaxonomyAdmin(admin.ModelAdmin): - """ - Admin for Taxonomy Model - """ - - def has_change_permission(self, request, obj=None): - """ - Avoid edit system-defined taxonomies - """ - if obj is not None: - return check_taxonomy(taxonomy=obj) - return super().has_change_permission(request, obj) - - def has_delete_permission(self, request, obj=None): - """ - Avoid delete system-defined taxonomies - """ - if obj is not None: - return check_taxonomy(taxonomy=obj) - return super().has_change_permission(request, obj) - - -class TagForm(forms.ModelForm): - """ - Form for create a Tag - """ - class Meta: - model = Tag - fields = '__all__' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - print(self.fields) - if 'taxonomy' in self.fields: - self.fields['taxonomy'].queryset = self._filter_taxonomies() - - def _filter_taxonomies(self): - """ - Returns taxonomies that allows Tag creation - - - Not allow free text - - Not system defined - """ - taxonomy_queryset = Taxonomy.objects.filter( - allow_free_text=False - ) - valid_taxonomy_ids = [ - taxonomy.id for taxonomy - in taxonomy_queryset if check_taxonomy(taxonomy) - ] - - return taxonomy_queryset.filter(id__in=valid_taxonomy_ids) - - -class TagAdmin(admin.ModelAdmin): - """ - Admin for Tag Model - """ - form = TagForm - - def has_change_permission(self, request, obj=None): - """ - Avoid edit system-defined taxonomies - """ - if obj is not None: - taxonomy = obj.taxonomy - if taxonomy: - return check_taxonomy(taxonomy) - return super().has_change_permission(request, obj) - - def has_delete_permission(self, request, obj=None): - """ - Avoid delete system-defined taxonomies - """ - if obj is not None: - taxonomy = obj.taxonomy - if taxonomy: - return check_taxonomy(taxonomy) - return super().has_change_permission(request, obj) - - -admin.site.register(Taxonomy, TaxonomyAdmin) -admin.site.register(Tag, TagAdmin) +admin.site.register(Taxonomy) +admin.site.register(Tag) admin.site.register(ObjectTag) diff --git a/openedx_tagging/core/tagging/rules.py b/openedx_tagging/core/tagging/rules.py index 72b9fa6b4..e77f59263 100644 --- a/openedx_tagging/core/tagging/rules.py +++ b/openedx_tagging/core/tagging/rules.py @@ -19,7 +19,7 @@ def can_view_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool: Anyone can view an enabled taxonomy, but only taxonomy admins can view a disabled taxonomy. """ - return (taxonomy and taxonomy.enabled) or is_taxonomy_admin(user) + return (taxonomy and taxonomy.cast().enabled) or is_taxonomy_admin(user) @rules.predicate @@ -28,23 +28,24 @@ def can_change_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool: Even taxonomy admins cannot change system taxonomies. """ return is_taxonomy_admin(user) and ( - not taxonomy or not taxonomy or (taxonomy and not taxonomy.system_defined) + not taxonomy or (taxonomy and not taxonomy.cast().system_defined) ) @rules.predicate -def can_change_taxonomy_tag(user: User, tag: Tag = None) -> bool: +def can_change_tag(user: User, tag: Tag = None) -> bool: """ Even taxonomy admins cannot add tags to system taxonomies (their tags are system-defined), or free-text taxonomies (these don't have predefined tags). """ + taxonomy = tag.taxonomy.cast() if (tag and tag.taxonomy_id) else None return is_taxonomy_admin(user) and ( not tag - or not tag.taxonomy + or not taxonomy or ( - tag.taxonomy - and not tag.taxonomy.allow_free_text - and not tag.taxonomy.system_defined + taxonomy + and not taxonomy.allow_free_text + and not taxonomy.system_defined ) ) @@ -54,10 +55,12 @@ def can_change_object_tag(user: User, object_tag: ObjectTag = None) -> bool: """ Taxonomy admins can create or modify object tags on enabled taxonomies. """ + taxonomy = object_tag.taxonomy.cast() if (object_tag and object_tag.taxonomy_id) else None + object_tag = taxonomy.object_tag_class.cast(object_tag) if taxonomy else object_tag return is_taxonomy_admin(user) and ( not object_tag - or not object_tag.taxonomy - or (object_tag.taxonomy and object_tag.taxonomy.enabled) + or not taxonomy + or (taxonomy and taxonomy.enabled) ) @@ -68,8 +71,8 @@ def can_change_object_tag(user: User, object_tag: ObjectTag = None) -> bool: rules.add_perm("oel_tagging.view_taxonomy", can_view_taxonomy) # Tag -rules.add_perm("oel_tagging.add_tag", can_change_taxonomy_tag) -rules.add_perm("oel_tagging.change_tag", can_change_taxonomy_tag) +rules.add_perm("oel_tagging.add_tag", can_change_tag) +rules.add_perm("oel_tagging.change_tag", can_change_tag) rules.add_perm("oel_tagging.delete_tag", is_taxonomy_admin) rules.add_perm("oel_tagging.view_tag", rules.always_allow) diff --git a/projects/dev.py b/projects/dev.py index a96473d4b..c65e2c34f 100644 --- a/projects/dev.py +++ b/projects/dev.py @@ -52,6 +52,7 @@ AUTHENTICATION_BACKENDS = [ 'rules.permissions.ObjectPermissionBackend', + 'django.contrib.auth.backends.ModelBackend', ] MIDDLEWARE = [ diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 8d84bf0b5..9c84020a5 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -29,7 +29,7 @@ def setUp(self): self.taxonomy = Taxonomy.objects.get(name="Life on Earth") 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") @@ -138,7 +138,7 @@ class TestModelTagTaxonomy(TestTagTaxonomyMixin, TestCase): def test_system_defined(self): assert not self.taxonomy.system_defined - assert self.system_taxonomy.system_defined + assert self.system_taxonomy.cast().system_defined def test_representations(self): assert ( From c54cdda7c1b2686cf94a806edbb543588cf6fe7a Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Mon, 24 Jul 2023 10:32:08 +0930 Subject: [PATCH 20/27] fix: adds unique_together and fixes nit --- .../migrations/0004_auto_20230723_2001.py | 35 +++++++++++++++++++ openedx_tagging/core/tagging/models/base.py | 9 ++--- 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py diff --git a/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py new file mode 100644 index 000000000..27e221863 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.19 on 2023-07-24 01:01 + +from django.db import migrations, models +import openedx_learning.lib.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('oel_tagging', '0003_auto_20230721_1238'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='tag', + name='oel_tagging_taxonom_89e779_idx', + ), + migrations.RemoveIndex( + model_name='tag', + name='oel_tagging_taxonom_44e355_idx', + ), + migrations.AlterField( + model_name='objecttag', + name='object_id', + field=openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, db_index=True, editable=False, help_text='Identifier for the object being tagged', max_length=255), + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together={('taxonomy', 'value'), ('taxonomy', 'external_id')}, + ), + 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 8af03796e..726821c47 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -62,9 +62,9 @@ class Tag(models.Model): ) class Meta: - indexes = [ - models.Index(fields=["taxonomy", "value"]), - models.Index(fields=["taxonomy", "external_id"]), + unique_together = [ + ["taxonomy", "external_id"], + ["taxonomy", "value"], ] def __repr__(self): @@ -443,6 +443,7 @@ class ObjectTag(models.Model): id = models.BigAutoField(primary_key=True) object_id = case_insensitive_char_field( max_length=255, + db_index=True, editable=False, help_text=_("Identifier for the object being tagged"), ) @@ -484,6 +485,7 @@ class ObjectTag(models.Model): class Meta: indexes = [ + models.Index(fields=["taxonomy", "object_id"]), models.Index(fields=["taxonomy", "_value"]), ] @@ -556,7 +558,6 @@ def tag_ref(self, tag_ref: str): try: self.tag = self.taxonomy.tag_set.get(pk=tag_ref) self.value = self.tag.value - return except (ValueError, Tag.DoesNotExist): # This might be ok, e.g. if our taxonomy.allow_free_text, so we just pass through here. # We rely on the caller to validate before saving. From 4f51683a4e2286e3f73a15fc3915ecc35daa87d8 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Mon, 24 Jul 2023 15:57:30 +0930 Subject: [PATCH 21/27] fix: indexes --- .../migrations/0004_auto_20230723_2001.py | 35 +++++++++---------- openedx_tagging/core/tagging/models/base.py | 4 +++ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py index 27e221863..4cca2c417 100644 --- a/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +++ b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py @@ -1,35 +1,34 @@ -# Generated by Django 3.2.19 on 2023-07-24 01:01 +# Generated by Django 3.2.19 on 2023-07-24 06:25 from django.db import migrations, models import openedx_learning.lib.fields class Migration(migrations.Migration): - dependencies = [ - ('oel_tagging', '0003_auto_20230721_1238'), + ("oel_tagging", "0003_auto_20230721_1238"), ] operations = [ - migrations.RemoveIndex( - model_name='tag', - name='oel_tagging_taxonom_89e779_idx', - ), - migrations.RemoveIndex( - model_name='tag', - name='oel_tagging_taxonom_44e355_idx', - ), migrations.AlterField( - model_name='objecttag', - name='object_id', - field=openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, db_index=True, editable=False, help_text='Identifier for the object being tagged', max_length=255), + model_name="objecttag", + name="object_id", + field=openedx_learning.lib.fields.MultiCollationCharField( + db_collations={"mysql": "utf8mb4_unicode_ci", "sqlite": "NOCASE"}, + db_index=True, + editable=False, + help_text="Identifier for the object being tagged", + max_length=255, + ), ), migrations.AlterUniqueTogether( - name='tag', - unique_together={('taxonomy', 'value'), ('taxonomy', 'external_id')}, + name="tag", + unique_together={("taxonomy", "external_id"), ("taxonomy", "value")}, ), migrations.AddIndex( - model_name='objecttag', - index=models.Index(fields=['taxonomy', 'object_id'], name='oel_tagging_taxonom_aa24e6_idx'), + 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 726821c47..76027184d 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -62,6 +62,10 @@ class Tag(models.Model): ) class Meta: + indexes = [ + models.Index(fields=["taxonomy", "value"]), + models.Index(fields=["taxonomy", "external_id"]), + ] unique_together = [ ["taxonomy", "external_id"], ["taxonomy", "value"], From a29d22730dd839a6bf97e9172a993121716bcaf2 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 24 Jul 2023 18:38:40 -0500 Subject: [PATCH 22/27] fix: Model pk validation on Model taxonomy --- openedx_tagging/core/tagging/models/system_defined.py | 4 ++++ .../core/tagging/test_system_defined_models.py | 1 + 2 files changed, 5 insertions(+) diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index 06db79795..238d7207e 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -72,6 +72,10 @@ def get_instance(self) -> Union[models.Model, None]: if instance_id: try: return self.tag_class_model.objects.get(pk=instance_id) + except ValueError as e: + log.exception( + f"{self}: {str(e)}" + ) except self.tag_class_model.DoesNotExist: log.exception( f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist." 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 6ca1ab43c..d99386e88 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -100,6 +100,7 @@ def test_implementation_error(self, taxonomy_cls, expected_exception): @ddt.data( (1, "tag_id", True), # Valid (0, "tag_id", False), # Invalid user + ("test_id", "tag_id", False), # Invalid user id (1, None, False), # Testing parent validations ) @ddt.unpack From a34d401173b1737315dece14bba31ca74eea3ccf Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 27 Jul 2023 12:04:22 -0500 Subject: [PATCH 23/27] style: comments and style --- .../commands/build_language_fixture.py | 1 - .../migrations/0003_auto_20230721_1238.py | 85 +++++++++---------- openedx_tagging/core/tagging/models/base.py | 9 +- .../core/tagging/models/system_defined.py | 8 +- openedx_tagging/core/tagging/rules.py | 14 ++- 5 files changed, 54 insertions(+), 63 deletions(-) diff --git a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py index 2acd178fa..9c1e0a85e 100644 --- a/openedx_tagging/core/tagging/management/commands/build_language_fixture.py +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -39,7 +39,6 @@ def build_fixture(self, json_data): output_file.write(f" external_id: {lang_code}\n") # System tags are identified with negative numbers to avoid clashing with user-created tags. tag_pk -= 1 - def get_lang_value(self, lang_data): """ diff --git a/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py b/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py index 16920a1b0..e44f21cf8 100644 --- a/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +++ b/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py @@ -4,80 +4,73 @@ class Migration(migrations.Migration): - dependencies = [ - ('oel_tagging', '0002_auto_20230718_2026'), + ("oel_tagging", "0002_auto_20230718_2026"), ] operations = [ migrations.CreateModel( - name='ModelObjectTag', - fields=[ - ], + name="ModelObjectTag", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.objecttag',), + bases=("oel_tagging.objecttag",), ), migrations.CreateModel( - name='SystemDefinedTaxonomy', - fields=[ - ], + name="SystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.taxonomy',), + bases=("oel_tagging.taxonomy",), ), migrations.RemoveField( - model_name='taxonomy', - name='system_defined', + model_name="taxonomy", + name="system_defined", ), migrations.CreateModel( - name='LanguageTaxonomy', - fields=[ - ], + name="LanguageTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.systemdefinedtaxonomy',), + bases=("oel_tagging.systemdefinedtaxonomy",), ), migrations.CreateModel( - name='ModelSystemDefinedTaxonomy', - fields=[ - ], + name="ModelSystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.systemdefinedtaxonomy',), + bases=("oel_tagging.systemdefinedtaxonomy",), ), migrations.CreateModel( - name='UserModelObjectTag', - fields=[ - ], + name="UserModelObjectTag", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.modelobjecttag',), + bases=("oel_tagging.modelobjecttag",), ), migrations.CreateModel( - name='UserSystemDefinedTaxonomy', - fields=[ - ], + name="UserSystemDefinedTaxonomy", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('oel_tagging.modelsystemdefinedtaxonomy',), + bases=("oel_tagging.modelsystemdefinedtaxonomy",), ), ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 76027184d..499149845 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -6,7 +6,10 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ -from openedx_learning.lib.fields import MultiCollationTextField, case_insensitive_char_field +from openedx_learning.lib.fields import ( + MultiCollationTextField, + case_insensitive_char_field, +) log = logging.getLogger(__name__) @@ -202,7 +205,7 @@ def taxonomy_class(self) -> Type: if self._taxonomy_class: return import_string(self._taxonomy_class) return None - + @property def system_defined(self) -> bool: """ @@ -268,6 +271,8 @@ def get_tags(self, tag_set: models.QuerySet = None) -> List[Tag]: """ Returns a list of all Tags in the current taxonomy, from the root(s) down to TAXONOMY_MAX_DEPTH tags, in tree order. + Use `tag_set` to do an initial filtering of the tags. + Annotates each returned Tag with its ``depth`` in the tree (starting at 0). Performance note: may perform as many as TAXONOMY_MAX_DEPTH select queries. diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index 238d7207e..7b39b9c2a 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -73,9 +73,7 @@ def get_instance(self) -> Union[models.Model, None]: try: return self.tag_class_model.objects.get(pk=instance_id) except ValueError as e: - log.exception( - f"{self}: {str(e)}" - ) + log.exception(f"{self}: {str(e)}") except self.tag_class_model.DoesNotExist: log.exception( f"{self}: {self.tag_class_model.__name__} pk={instance_id} does not exist." @@ -168,7 +166,7 @@ def object_tag_class(self) -> Type: Model Taxonomy subclasses must implement this to provide a ModelObjectTag subclass. """ raise NotImplementedError - + def _check_instance(self, object_tag: ObjectTag) -> bool: """ Returns True if the instance exists @@ -176,7 +174,7 @@ def _check_instance(self, object_tag: ObjectTag) -> bool: Subclasses can override this method to perform their own instance validation checks. """ object_tag = self.object_tag_class.cast(object_tag) - return bool(object_tag.get_instance()) + return bool(object_tag.get_instance()) def _check_tag(self, object_tag: ObjectTag) -> bool: """ diff --git a/openedx_tagging/core/tagging/rules.py b/openedx_tagging/core/tagging/rules.py index e77f59263..f1b19be9c 100644 --- a/openedx_tagging/core/tagging/rules.py +++ b/openedx_tagging/core/tagging/rules.py @@ -42,11 +42,7 @@ def can_change_tag(user: User, tag: Tag = None) -> bool: return is_taxonomy_admin(user) and ( not tag or not taxonomy - or ( - taxonomy - and not taxonomy.allow_free_text - and not taxonomy.system_defined - ) + or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined) ) @@ -55,12 +51,12 @@ def can_change_object_tag(user: User, object_tag: ObjectTag = None) -> bool: """ Taxonomy admins can create or modify object tags on enabled taxonomies. """ - taxonomy = object_tag.taxonomy.cast() if (object_tag and object_tag.taxonomy_id) else None + taxonomy = ( + object_tag.taxonomy.cast() if (object_tag and object_tag.taxonomy_id) else None + ) object_tag = taxonomy.object_tag_class.cast(object_tag) if taxonomy else object_tag return is_taxonomy_admin(user) and ( - not object_tag - or not taxonomy - or (taxonomy and taxonomy.enabled) + not object_tag or not taxonomy or (taxonomy and taxonomy.enabled) ) From 4b56ec8b5ba33ab870a8e2f5a051f1416e26eb8f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 31 Jul 2023 15:56:55 -0500 Subject: [PATCH 24/27] feat: Creating language taxonomy on fixtures --- .../tagging/fixtures/language_taxonomy.yaml | 10 +++++++ .../migrations/0005_language_taxonomy.py | 29 +++++++++++++++++++ .../core/fixtures/tagging.yaml | 10 ------- .../openedx_tagging/core/tagging/test_api.py | 9 +++--- .../core/tagging/test_models.py | 7 +++-- .../tagging/test_system_defined_models.py | 3 +- 6 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py diff --git a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml index ac2c934e3..38355b99b 100644 --- a/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml +++ b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml @@ -1286,3 +1286,13 @@ parent: null value: Zulu external_id: zu +- model: oel_tagging.taxonomy + pk: -1 + fields: + name: Languages + description: ISO 639-1 Languages. Allows tags for any language configured for use on the instance + enabled: true + required: true + allow_multiple: false + allow_free_text: false + visible_to_authors: true diff --git a/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py b/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py new file mode 100644 index 000000000..48bab478a --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.19 on 2023-07-28 13:33 + +from django.db import migrations +from django.core.management import call_command + + +def load_language_taxonomy(apps, schema_editor): + """ + Load language taxonomy and tags + """ + call_command("loaddata", "--app=oel_tagging", "language_taxonomy.yaml") + + +def revert(apps, schema_editor): + """ + Deletes language taxonomy an tags + """ + Taxonomy = apps.get_model("oel_tagging", "Taxonomy") + Taxonomy.objects.filter(id=-1).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("oel_tagging", "0004_auto_20230723_2001"), + ] + + operations = [ + migrations.RunPython(load_language_taxonomy, revert), + ] diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index 4633901ad..e8acbbc5c 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -161,16 +161,6 @@ required: false allow_multiple: false allow_free_text: false -- model: oel_tagging.taxonomy - pk: 2 - 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 - _taxonomy_class: openedx_tagging.core.tagging.models.system_defined.LanguageTaxonomy - model: oel_tagging.taxonomy pk: 3 fields: diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 654da8460..faf5779d4 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -50,14 +50,15 @@ def test_get_taxonomies(self): assert enabled == [ tax1, + self.language_taxonomy, self.taxonomy, self.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]) == " (4) System defined taxonomy" + assert str(enabled[1]) == " (-1) Languages" + assert str(enabled[2]) == " (1) Life on Earth" + assert str(enabled[3]) == " (4) System defined taxonomy" with self.assertNumQueries(1): disabled = list(tagging_api.get_taxonomies(enabled=False)) @@ -69,9 +70,9 @@ def test_get_taxonomies(self): assert both == [ tax2, tax1, + self.language_taxonomy, self.taxonomy, self.system_taxonomy, - self.language_taxonomy, self.user_taxonomy, ] diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 9c84020a5..71b013319 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -7,6 +7,7 @@ ObjectTag, Tag, Taxonomy, + LanguageTaxonomy, ) @@ -30,7 +31,9 @@ def setUp(self): self.system_taxonomy = Taxonomy.objects.get( name="System defined taxonomy" ) - self.language_taxonomy = Taxonomy.objects.get(name="System Languages").cast() + self.language_taxonomy = Taxonomy.objects.get(name="Languages") + self.language_taxonomy.taxonomy_class = LanguageTaxonomy + self.language_taxonomy = self.language_taxonomy.cast() self.user_taxonomy = Taxonomy.objects.get(name="User Authors").cast() self.archaea = get_tag("Archaea") self.archaebacteria = get_tag("Archaebacteria") @@ -147,7 +150,7 @@ def test_representations(self): assert ( str(self.language_taxonomy) == repr(self.language_taxonomy) - == " (2) System Languages" + == " (-1) Languages" ) assert str(self.bacteria) == repr(self.bacteria) == " (1) Bacteria" 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 d99386e88..432d07110 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -233,6 +233,7 @@ def test_validations(self, lang, tag_id): def test_get_tags(self): tags = self.language_taxonomy.get_tags() + expected_langs = [lang[0] for lang in test_languages] for tag in tags: - assert tag.external_id in test_languages + assert tag.external_id in expected_langs assert tag.annotated_field == 0 From 034726df9374ca549c4947b76556f41090a5a5cc Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 31 Jul 2023 17:03:30 -0500 Subject: [PATCH 25/27] test: Added system defined to api tests --- .../core/fixtures/tagging.yaml | 21 ++++ .../openedx_tagging/core/tagging/test_api.py | 110 +++++++++++++++++- .../core/tagging/test_models.py | 7 ++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/tests/openedx_tagging/core/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index e8acbbc5c..5bb3936c9 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -152,6 +152,27 @@ parent: null value: System Tag 1 external_id: 'tag_1' +- model: oel_tagging.tag + pk: 23 + fields: + taxonomy: 4 + parent: null + value: System Tag 2 + external_id: 'tag_2' +- model: oel_tagging.tag + pk: 24 + fields: + taxonomy: 4 + parent: null + value: System Tag 3 + external_id: 'tag_3' +- model: oel_tagging.tag + pk: 25 + fields: + taxonomy: 4 + parent: null + value: System Tag 4 + external_id: 'tag_4' - model: oel_tagging.taxonomy pk: 1 fields: diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index faf5779d4..a75aebb61 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -1,11 +1,19 @@ """ Test the tagging APIs """ -from django.test.testcases import TestCase +from django.test.testcases import TestCase, override_settings import openedx_tagging.core.tagging.api as tagging_api from openedx_tagging.core.tagging.models import ObjectTag, Tag -from .test_models import TestTagTaxonomyMixin +from .test_models import TestTagTaxonomyMixin, get_tag + +test_languages = [ + ("az", "Azerbaijani"), + ("en", "English"), + ("id", "Indonesian"), + ("qu", "Quechua"), + ("zu", "Zulu"), +] class TestApiTagging(TestTagTaxonomyMixin, TestCase): @@ -76,6 +84,7 @@ def test_get_taxonomies(self): self.user_taxonomy, ] + @override_settings(LANGUAGES=test_languages) def test_get_tags(self): self.setup_tag_depths() assert tagging_api.get_tags(self.taxonomy) == [ @@ -83,6 +92,11 @@ def test_get_tags(self): *self.kingdom_tags, *self.phylum_tags, ] + assert tagging_api.get_tags(self.system_taxonomy) == self.system_tags + tags = tagging_api.get_tags(self.language_taxonomy) + langs = [tag.external_id for tag in tags] + expected_langs = [lang[0] for lang in test_languages] + assert langs == expected_langs def check_object_tag(self, object_tag, taxonomy, tag, name, value): """ @@ -303,6 +317,98 @@ def test_tag_object_invalid_tag(self): exc.exception ) + @override_settings(LANGUAGES=test_languages) + def test_tag_object_language_taxonomy(self): + tags_list = [ + [get_tag("Azerbaijani").id], + [get_tag("English").id], + ] + + for tags in tags_list: + object_tags = tagging_api.tag_object( + self.language_taxonomy, + tags, + "biology101", + ) + + # Ensure the expected number of tags exist in the database + assert ( + list( + tagging_api.get_object_tags( + taxonomy=self.language_taxonomy, + object_id="biology101", + ) + ) + == object_tags + ) + # And the expected number of tags were returned + assert len(object_tags) == len(tags) + for index, object_tag in enumerate(object_tags): + assert object_tag.tag_id == tags[index] + assert object_tag.is_valid() + assert object_tag.taxonomy == self.language_taxonomy + assert object_tag.name == self.language_taxonomy.name + assert object_tag.object_id == "biology101" + + @override_settings(LANGUAGES=test_languages) + def test_tag_object_language_taxonomy_ivalid(self): + tags = [get_tag("Spanish").id] + with self.assertRaises(ValueError) as exc: + tagging_api.tag_object( + self.language_taxonomy, + tags, + "biology101", + ) + assert "Invalid object tag for taxonomy (-1): -40" in str( + exc.exception + ) + + def test_tag_object_model_system_taxonomy(self): + users = [ + self.user_1, + self.user_2, + ] + + for user in users: + tags = [user.id] + object_tags = tagging_api.tag_object( + self.user_taxonomy, + tags, + "biology101", + ) + + # Ensure the expected number of tags exist in the database + assert ( + list( + tagging_api.get_object_tags( + taxonomy=self.user_taxonomy, + object_id="biology101", + ) + ) + == object_tags + ) + # And the expected number of tags were returned + assert len(object_tags) == len(tags) + for object_tag in object_tags: + assert object_tag.tag.external_id == str(user.id) + assert object_tag.tag.value == user.username + assert object_tag.is_valid() + assert object_tag.taxonomy == self.user_taxonomy + assert object_tag.name == self.user_taxonomy.name + assert object_tag.object_id == "biology101" + + def test_tag_object_model_system_taxonomy_invalid(self): + tags = ["Invalid id"] + with self.assertRaises(ValueError) as exc: + tagging_api.tag_object( + self.user_taxonomy, + tags, + "biology101", + ) + assert "Invalid object tag for taxonomy (3): Invalid id" in str( + exc.exception + ) + def test_get_object_tags(self): # Alpha tag has no taxonomy alpha = ObjectTag(object_id="abc") diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 71b013319..2db7911af 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -88,6 +88,13 @@ def setUp(self): get_tag("Porifera"), ] + self.system_tags = [ + get_tag("System Tag 1"), + get_tag("System Tag 2"), + get_tag("System Tag 3"), + get_tag("System Tag 4"), + ] + def setup_tag_depths(self): """ Annotate our tags with depth so we can compare them. From cde323e0e66623ee9b1267e7373446df3b378539 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 1 Aug 2023 11:12:03 -0500 Subject: [PATCH 26/27] test: Fixing tests after merge with main --- .../core/tagging/rest_api/v1/permissions.py | 1 - .../core/tagging/rest_api/v1/serializers.py | 3 +- .../core/tagging/rest_api/v1/urls.py | 4 +- .../core/tagging/rest_api/v1/views.py | 1 - openedx_tagging/core/tagging/rules.py | 2 +- .../core/tagging/test_views.py | 50 ++++++++----------- 6 files changed, 25 insertions(+), 36 deletions(-) diff --git a/openedx_tagging/core/tagging/rest_api/v1/permissions.py b/openedx_tagging/core/tagging/rest_api/v1/permissions.py index b75c5ff79..e8915a62f 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/permissions.py +++ b/openedx_tagging/core/tagging/rest_api/v1/permissions.py @@ -15,4 +15,3 @@ class TaxonomyObjectPermissions(DjangoObjectPermissions): "PATCH": ["%(app_label)s.change_%(model_name)s"], "DELETE": ["%(app_label)s.delete_%(model_name)s"], } - diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index a46350a9a..53f647424 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -6,6 +6,7 @@ from openedx_tagging.core.tagging.models import Taxonomy + class TaxonomyListQueryParamsSerializer(serializers.Serializer): """ Serializer for the query params for the GET view @@ -13,6 +14,7 @@ class TaxonomyListQueryParamsSerializer(serializers.Serializer): enabled = serializers.BooleanField(required=False) + class TaxonomySerializer(serializers.ModelSerializer): class Meta: model = Taxonomy @@ -27,4 +29,3 @@ class Meta: "system_defined", "visible_to_authors", ] - diff --git a/openedx_tagging/core/tagging/rest_api/v1/urls.py b/openedx_tagging/core/tagging/rest_api/v1/urls.py index 97d653a28..80500990c 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/urls.py +++ b/openedx_tagging/core/tagging/rest_api/v1/urls.py @@ -11,6 +11,4 @@ router = DefaultRouter() router.register("taxonomies", views.TaxonomyView, basename="taxonomy") -urlpatterns = [ - path('', include(router.urls)) -] +urlpatterns = [path("", include(router.urls))] diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 1ea02aca9..37c3b9689 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -107,7 +107,6 @@ class TaxonomyView(ModelViewSet): """ - serializer_class = TaxonomySerializer permission_classes = [TaxonomyObjectPermissions] diff --git a/openedx_tagging/core/tagging/rules.py b/openedx_tagging/core/tagging/rules.py index c16abf62a..364178c70 100644 --- a/openedx_tagging/core/tagging/rules.py +++ b/openedx_tagging/core/tagging/rules.py @@ -19,7 +19,7 @@ def can_view_taxonomy(user: User, taxonomy: Taxonomy = None) -> bool: Anyone can view an enabled taxonomy or list all taxonomies, but only taxonomy admins can view a disabled taxonomy. """ - return (taxonomy and taxonomy.cast().enabled) or is_taxonomy_admin(user) + return not taxonomy or taxonomy.cast().enabled or is_taxonomy_admin(user) @rules.predicate diff --git a/tests/openedx_tagging/core/tagging/test_views.py b/tests/openedx_tagging/core/tagging/test_views.py index 0fd0d6840..6ed24246c 100644 --- a/tests/openedx_tagging/core/tagging/test_views.py +++ b/tests/openedx_tagging/core/tagging/test_views.py @@ -8,6 +8,7 @@ from rest_framework.test import APITestCase from openedx_tagging.core.tagging.models import Taxonomy +from openedx_tagging.core.tagging.models.system_defined import SystemDefinedTaxonomy User = get_user_model() @@ -54,14 +55,14 @@ def setUp(self): ) @ddt.data( - (None, status.HTTP_200_OK, 3), - (1, status.HTTP_200_OK, 2), + (None, status.HTTP_200_OK, 4), + (1, status.HTTP_200_OK, 3), (0, status.HTTP_200_OK, 1), - (True, status.HTTP_200_OK, 2), + (True, status.HTTP_200_OK, 3), (False, status.HTTP_200_OK, 1), - ("True", status.HTTP_200_OK, 2), + ("True", status.HTTP_200_OK, 3), ("False", status.HTTP_200_OK, 1), - ("1", status.HTTP_200_OK, 2), + ("1", status.HTTP_200_OK, 3), ("0", status.HTTP_200_OK, 1), (2, status.HTTP_400_BAD_REQUEST, None), ("invalid", status.HTTP_400_BAD_REQUEST, None), @@ -82,6 +83,7 @@ def test_list_taxonomy_queryparams(self, enabled, expected_status, expected_coun assert response.status_code == expected_status # If we were able to list the taxonomies, check that we got the expected number back + # We take into account the Language Taxonomy that is created by the system in a migration if status.is_success(expected_status): assert len(response.data) == expected_count @@ -231,34 +233,28 @@ def test_update_taxonomy(self, user_attr, expected_status): ) @ddt.data( - (False, False, status.HTTP_200_OK), - (False, True, status.HTTP_200_OK), - (True, False, status.HTTP_403_FORBIDDEN), - (True, True, status.HTTP_403_FORBIDDEN), + (False, status.HTTP_200_OK), + (True, status.HTTP_403_FORBIDDEN), ) @ddt.unpack def test_update_taxonomy_system_defined( - self, create_value, update_value, expected_status + self, system_defined, expected_status ): ''' Test that we can't update system_defined field ''' - taxonomy = Taxonomy.objects.create( - name="test system taxonomy", system_defined=create_value - ) + taxonomy = Taxonomy.objects.create(name="test system taxonomy") + if system_defined: + taxonomy.taxonomy_class = SystemDefinedTaxonomy taxonomy.save() url = TAXONOMY_DETAIL_URL.format(pk=taxonomy.pk) self.client.force_authenticate(user=self.staff) response = self.client.put( - url, {"name": "new name", "system_defined": update_value}, format="json" + url, {"name": "new name"}, format="json" ) assert response.status_code == expected_status - # Verify that system_defined has not changed - response = self.client.get(url) - assert response.data["system_defined"] == create_value - def test_update_taxonomy_404(self): url = TAXONOMY_DETAIL_URL.format(pk=123123) @@ -303,34 +299,30 @@ def test_patch_taxonomy(self, user_attr, expected_status): ) @ddt.data( - (False, False, status.HTTP_200_OK), - (False, True, status.HTTP_200_OK), - (True, False, status.HTTP_403_FORBIDDEN), - (True, True, status.HTTP_403_FORBIDDEN), + (False, status.HTTP_200_OK), + (True, status.HTTP_403_FORBIDDEN), ) @ddt.unpack def test_patch_taxonomy_system_defined( - self, create_value, update_value, expected_status + self, system_defined, expected_status ): ''' Test that we can't patch system_defined field ''' taxonomy = Taxonomy.objects.create( - name="test system taxonomy", system_defined=create_value + name="test system taxonomy" ) + if system_defined: + taxonomy.taxonomy_class = SystemDefinedTaxonomy taxonomy.save() url = TAXONOMY_DETAIL_URL.format(pk=taxonomy.pk) self.client.force_authenticate(user=self.staff) response = self.client.patch( - url, {"system_defined": update_value}, format="json" + url, {"name": "New name"}, format="json" ) assert response.status_code == expected_status - # Verify that system_defined has not changed - response = self.client.get(url) - assert response.data["system_defined"] == create_value - def test_patch_taxonomy_404(self): url = TAXONOMY_DETAIL_URL.format(pk=123123) From 8a9d241144c680f2026b0ac47ebe3ce684721810 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 1 Aug 2023 11:15:04 -0500 Subject: [PATCH 27/27] chore: Package version update --- openedx_learning/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 3dc1f76bc..485f44ac2 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1"