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/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 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" 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/fixtures/language_taxonomy.yaml b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml new file mode 100644 index 000000000..38355b99b --- /dev/null +++ b/openedx_tagging/core/tagging/fixtures/language_taxonomy.yaml @@ -0,0 +1,1298 @@ +- 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 +- 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/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..9c1e0a85e --- /dev/null +++ b/openedx_tagging/core/tagging/management/commands/build_language_fixture.py @@ -0,0 +1,48 @@ +""" +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 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): + 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: + 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") + # System tags are identified with negative numbers to avoid clashing with user-created tags. + 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] 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..e44f21cf8 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py @@ -0,0 +1,76 @@ +# 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/0004_auto_20230723_2001.py b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py new file mode 100644 index 000000000..4cca2c417 --- /dev/null +++ b/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py @@ -0,0 +1,34 @@ +# 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"), + ] + + operations = [ + 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", "external_id"), ("taxonomy", "value")}, + ), + migrations.AddIndex( + model_name="objecttag", + index=models.Index( + fields=["taxonomy", "object_id"], name="oel_tagging_taxonom_aa24e6_idx" + ), + ), + ] diff --git a/openedx_tagging/core/tagging/migrations/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/openedx_tagging/core/tagging/models/__init__.py b/openedx_tagging/core/tagging/models/__init__.py new file mode 100644 index 000000000..90640ddfc --- /dev/null +++ b/openedx_tagging/core/tagging/models/__init__.py @@ -0,0 +1,11 @@ +from .base import ( + Tag, + Taxonomy, + ObjectTag, +) +from .system_defined import ( + ModelObjectTag, + ModelSystemDefinedTaxonomy, + UserSystemDefinedTaxonomy, + LanguageTaxonomy, +) diff --git a/openedx_tagging/core/tagging/models.py b/openedx_tagging/core/tagging/models/base.py similarity index 90% rename from openedx_tagging/core/tagging/models.py rename to openedx_tagging/core/tagging/models/base.py index 3c8222bcd..499149845 100644 --- a/openedx_tagging/core/tagging/models.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -1,4 +1,4 @@ -""" Tagging app data models """ +""" Tagging app base data models """ import logging from typing import List, Type, Union @@ -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__) @@ -66,6 +69,10 @@ class Meta: models.Index(fields=["taxonomy", "value"]), models.Index(fields=["taxonomy", "external_id"]), ] + unique_together = [ + ["taxonomy", "external_id"], + ["taxonomy", "value"], + ] def __repr__(self): """ @@ -140,14 +147,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,8 +176,25 @@ 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 + 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: """ @@ -190,6 +206,14 @@ def taxonomy_class(self) -> Type: 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]): """ @@ -239,15 +263,16 @@ 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 - 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. + 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. @@ -256,9 +281,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.tag_set.prefetch_related("parent") + filtered_tags = tag_set.prefetch_related("parent") if parents is None: filtered_tags = filtered_tags.filter(parent=None) else: @@ -366,9 +394,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, ) @@ -378,20 +407,12 @@ 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, ) - 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.tag_ref = tag_ref object_tag.resync() if not self.validate_object_tag(object_tag): raise ValueError( @@ -431,6 +452,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"), ) @@ -472,6 +494,7 @@ class ObjectTag(models.Model): class Meta: indexes = [ + models.Index(fields=["taxonomy", "object_id"]), models.Index(fields=["taxonomy", "_value"]), ] @@ -531,6 +554,24 @@ 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 + 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. 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..7b39b9c2a --- /dev/null +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -0,0 +1,269 @@ +""" 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 openedx_tagging.core.tagging.models.base import ObjectTag + +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 + + @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 True + + +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 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." + ) + + 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 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. + + 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 + + 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): + """ + 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/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 2cc94d0dc..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 not taxonomy or taxonomy.enabled or is_taxonomy_admin(user) + return not taxonomy or taxonomy.cast().enabled or is_taxonomy_admin(user) @rules.predicate @@ -28,24 +28,21 @@ 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 (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 ( - tag.taxonomy - and not tag.taxonomy.allow_free_text - and not tag.taxonomy.system_defined - ) + or not taxonomy + or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined) ) @@ -54,10 +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 + ) + 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) + not object_tag or not taxonomy or (taxonomy and taxonomy.enabled) ) @@ -68,8 +67,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/fixtures/tagging.yaml b/tests/openedx_tagging/core/fixtures/tagging.yaml index a56ece81a..5bb3936c9 100644 --- a/tests/openedx_tagging/core/fixtures/tagging.yaml +++ b/tests/openedx_tagging/core/fixtures/tagging.yaml @@ -145,6 +145,34 @@ 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.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: @@ -154,14 +182,23 @@ required: false allow_multiple: false allow_free_text: false - system_defined: false - model: oel_tagging.taxonomy - pk: 2 + 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: false + _taxonomy_class: openedx_tagging.core.tagging.models.system_defined.UserSystemDefinedTaxonomy +- model: oel_tagging.taxonomy + pk: 4 fields: - name: System Languages - description: Allows tags for any language configured for use on the instance. + 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.system_defined.SystemDefinedTaxonomy diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 798b4826b..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): @@ -47,10 +55,18 @@ 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.language_taxonomy, + self.taxonomy, + self.system_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[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)) @@ -59,8 +75,16 @@ 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.language_taxonomy, + self.taxonomy, + self.system_taxonomy, + self.user_taxonomy, + ] + @override_settings(LANGUAGES=test_languages) def test_get_tags(self): self.setup_tag_depths() assert tagging_api.get_tags(self.taxonomy) == [ @@ -68,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): """ @@ -288,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 65af41eaa..2db7911af 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -1,9 +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 -from openedx_tagging.core.tagging.models import ObjectTag, Tag, Taxonomy +from openedx_tagging.core.tagging.models import ( + ObjectTag, + Tag, + Taxonomy, + LanguageTaxonomy, +) def get_tag(value): @@ -23,13 +28,30 @@ 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.system_taxonomy = Taxonomy.objects.get( + name="System defined taxonomy" + ) + 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") 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) @@ -66,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. @@ -119,16 +148,16 @@ 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 ( 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) + == " (-1) Languages" ) assert str(self.bacteria) == repr(self.bacteria) == " (1) Bacteria" @@ -293,6 +322,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", 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..432d07110 --- /dev/null +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -0,0 +1,239 @@ +""" 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, + 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 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", True), # Valid + (0, "tag_id", False), # Invalid user + ("test_id", "tag_id", False), # Invalid user id + (1, None, False), # Testing parent validations + ) + @ddt.unpack + def test_validations(self, tag_external_id, tag_id, expected): + tag = Tag( + id=tag_id, + taxonomy=self.user_taxonomy, + value="_val", + external_id=tag_external_id, + ) + object_tag = ObjectTag( + object_id="id", + tag=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 + 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", + ) + 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() + expected_langs = [lang[0] for lang in test_languages] + for tag in tags: + assert tag.external_id in expected_langs + assert tag.annotated_field == 0 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)