Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System defined taxonomies #67

Merged
merged 28 commits into from
Aug 2, 2023
Merged
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ac21e1c
feat: System-defined taxonomies
ChrisChV Jul 3, 2023
55636f7
feat: Language and Author taxonomies
ChrisChV Jul 5, 2023
327bb58
feat: Generic system defined object tags and Language object tag added
ChrisChV Jul 7, 2023
58fc1e8
chore: get_tags_query_set added to LanguageObjectTag
ChrisChV Jul 10, 2023
9e7ed4e
chore: Adding _validate_taxonomy function to all system defined objec…
ChrisChV Jul 10, 2023
899d61f
chore: Updating system_defined_taxonomy_id
ChrisChV Jul 11, 2023
1e68687
refactor: consolidates ObjectTag validation
ChrisChV Jul 12, 2023
9e5d854
feat: System defined taxonomies subclasses
ChrisChV Jul 19, 2023
9b72fec
style: linting and formatting
pomegranited Jul 19, 2023
69eeece
refactor: use negative numbers as primary keys for system taxonomies …
pomegranited Jul 19, 2023
2a0b4b6
refactor: use ObjectTag subclasses where possible
pomegranited Jul 19, 2023
de3cc3e
refactor: LanguageTaxonomy overrides get_tags
pomegranited Jul 19, 2023
437ae98
docs: System taxonomy creation doc updated with Dynamic tags approach
ChrisChV Jul 20, 2023
8ca16ea
style: Updating comments
ChrisChV Jul 20, 2023
0c74bb4
style: Separating the models into base and system defined
ChrisChV Jul 20, 2023
9a38047
fix: Update language fixture to negative pk
ChrisChV Jul 21, 2023
1847248
feat: Updates on Taxonomy and Tag admins
ChrisChV Jul 21, 2023
eaaba78
feat: Instance validations on ModelSystemDefinedTaxonomy
ChrisChV Jul 21, 2023
bfc532a
feat: use Taxonomy.cast and ObjectTag.cast in rules
pomegranited Jul 24, 2023
c54cdda
fix: adds unique_together
pomegranited Jul 24, 2023
4f51683
fix: indexes
pomegranited Jul 24, 2023
a29d227
fix: Model pk validation on Model taxonomy
ChrisChV Jul 24, 2023
a34d401
style: comments and style
ChrisChV Jul 27, 2023
4b56ec8
feat: Creating language taxonomy on fixtures
ChrisChV Jul 31, 2023
034726d
test: Added system defined to api tests
ChrisChV Jul 31, 2023
8f1cac1
Merge branch 'main' into chris/system-defined-taxonomies
ChrisChV Jul 31, 2023
cde323e
test: Fixing tests after merge with main
ChrisChV Aug 1, 2023
8a9d241
chore: Package version update
ChrisChV Aug 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: System-defined taxonomies
ChrisChV committed Jul 21, 2023

Verified

This commit was signed with the committer’s verified signature.
ChrisChV Chris Chávez
commit ac21e1c66dc49b7d31fa6a9baad7020021346f07
Empty file.
Original file line number Diff line number Diff line change
@@ -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]

24 changes: 24 additions & 0 deletions openedx_tagging/core/tagging/migrations/0002_systemtaxonomy.py
Original file line number Diff line number Diff line change
@@ -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',),
),
]
102 changes: 102 additions & 0 deletions openedx_tagging/core/tagging/models.py
Original file line number Diff line number Diff line change
@@ -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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions tests/openedx_tagging/core/tagging/test_system_defined.py
Original file line number Diff line number Diff line change
@@ -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)