System defined taxonomies (#32869)
* feat: System defined taxonomies * style: models.py moved to models/base.py * feat: New Content System defined models * style: Lint and migration * fix: Fix migration error * chore: Rebase and compile requirements * refactor: adds ContentTaxonomyMixin for use when creating content system taxonomies Pulls the ContentTaxonomy-specific logic into a mixin class to bring the Content-specific logic into other Taxonony subclasses. * fix: Tests * test: System defined model validations * fix: Move language taxonomy creation to openedx-learning * style: Rename of OrganizationSystemDefinedTaxonomy * style: nits * chore: Update openedx-learning dependency --------- Co-authored-by: Jillian Vogel <jill@opencraft.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
- model: oel_tagging.taxonomy
|
||||
pk: -2
|
||||
fields:
|
||||
name: Organizations
|
||||
description: Allows tags for any organization ID created on the instance.
|
||||
enabled: true
|
||||
required: true
|
||||
allow_multiple: false
|
||||
allow_free_text: false
|
||||
visible_to_authors: false
|
||||
_taxonomy_class: openedx.features.content_tagging.models.ContentAuthorTaxonomy
|
||||
- model: oel_tagging.taxonomy
|
||||
pk: -3
|
||||
fields:
|
||||
name: Content Authors
|
||||
description: Allows tags for any user ID created on the instance.
|
||||
enabled: true
|
||||
required: true
|
||||
allow_multiple: false
|
||||
allow_free_text: false
|
||||
visible_to_authors: false
|
||||
_taxonomy_class: openedx.features.content_tagging.models.ContentOrganizationTaxonomy
|
||||
@@ -0,0 +1,59 @@
|
||||
# Generated by Django 3.2.20 on 2023-07-31 21:07
|
||||
|
||||
from django.db import migrations
|
||||
import openedx.features.content_tagging.models.base
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oel_tagging', '0005_language_taxonomy'),
|
||||
('content_tagging', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentAuthorTaxonomy',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=(openedx.features.content_tagging.models.base.ContentTaxonomyMixin, 'oel_tagging.usersystemdefinedtaxonomy'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContentLanguageTaxonomy',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=(openedx.features.content_tagging.models.base.ContentTaxonomyMixin, 'oel_tagging.languagetaxonomy'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContentOrganizationTaxonomy',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=(openedx.features.content_tagging.models.base.ContentTaxonomyMixin, 'oel_tagging.modelsystemdefinedtaxonomy'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizationModelObjectTag',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('oel_tagging.modelobjecttag',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 3.2.20 on 2023-07-11 22:57
|
||||
|
||||
from django.db import migrations
|
||||
from django.core.management import call_command
|
||||
from openedx.features.content_tagging.models import ContentLanguageTaxonomy
|
||||
|
||||
|
||||
def load_system_defined_taxonomies(apps, schema_editor):
|
||||
"""
|
||||
Creates system defined taxonomies
|
||||
"""
|
||||
|
||||
# Create system defined taxonomy instances
|
||||
call_command('loaddata', '--app=content_tagging', 'system_defined.yaml')
|
||||
|
||||
# Adding taxonomy class to the language taxonomy
|
||||
Taxonomy = apps.get_model('oel_tagging', 'Taxonomy')
|
||||
language_taxonomy = Taxonomy.objects.get(id=-1)
|
||||
language_taxonomy.taxonomy_class = ContentLanguageTaxonomy
|
||||
|
||||
|
||||
def revert_system_defined_taxonomies(apps, schema_editor):
|
||||
"""
|
||||
Deletes all system defined taxonomies
|
||||
"""
|
||||
Taxonomy = apps.get_model('oel_tagging', 'Taxonomy')
|
||||
Taxonomy.objects.get(id=-2).delete()
|
||||
Taxonomy.objects.get(id=-3).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('content_tagging', '0002_system_defined_taxonomies'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(load_system_defined_taxonomies, revert_system_defined_taxonomies),
|
||||
]
|
||||
13
openedx/features/content_tagging/models/__init__.py
Normal file
13
openedx/features/content_tagging/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Content Tagging and System defined models
|
||||
"""
|
||||
from .base import (
|
||||
TaxonomyOrg,
|
||||
ContentObjectTag,
|
||||
ContentTaxonomy,
|
||||
)
|
||||
from .system_defined import (
|
||||
ContentLanguageTaxonomy,
|
||||
ContentAuthorTaxonomy,
|
||||
ContentOrganizationTaxonomy,
|
||||
)
|
||||
@@ -104,16 +104,13 @@ class ContentObjectTag(ObjectTag):
|
||||
return BlockUsageLocator.from_string(str(self.object_id))
|
||||
|
||||
|
||||
class ContentTaxonomy(Taxonomy):
|
||||
class ContentTaxonomyMixin:
|
||||
"""
|
||||
Taxonomy which can only tag Content objects (e.g. XBlocks or Courses) via ContentObjectTag.
|
||||
|
||||
Also ensures a valid TaxonomyOrg owner relationship with the content object.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
@classmethod
|
||||
def taxonomies_for_org(
|
||||
cls,
|
||||
@@ -164,3 +161,13 @@ class ContentTaxonomy(Taxonomy):
|
||||
).exists():
|
||||
return False
|
||||
return super()._check_taxonomy(content_tag)
|
||||
|
||||
|
||||
class ContentTaxonomy(ContentTaxonomyMixin, Taxonomy):
|
||||
"""
|
||||
Taxonomy that accepts ContentTags,
|
||||
and ensures a valid TaxonomyOrg owner relationship with the content object.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
75
openedx/features/content_tagging/models/system_defined.py
Normal file
75
openedx/features/content_tagging/models/system_defined.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
System defined models
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from openedx_tagging.core.tagging.models import (
|
||||
ModelSystemDefinedTaxonomy,
|
||||
ModelObjectTag,
|
||||
UserSystemDefinedTaxonomy,
|
||||
LanguageTaxonomy,
|
||||
)
|
||||
|
||||
from organizations.models import Organization
|
||||
from .base import ContentTaxonomyMixin
|
||||
|
||||
|
||||
class OrganizationModelObjectTag(ModelObjectTag):
|
||||
"""
|
||||
ObjectTags for the OrganizationSystemDefinedTaxonomy.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
@property
|
||||
def tag_class_model(self) -> Type:
|
||||
"""
|
||||
Associate the organization model
|
||||
"""
|
||||
return Organization
|
||||
|
||||
@property
|
||||
def tag_class_value(self) -> str:
|
||||
"""
|
||||
Returns the organization name to use it on Tag.value when creating Tags for this taxonomy.
|
||||
"""
|
||||
return "name"
|
||||
|
||||
|
||||
class ContentOrganizationTaxonomy(ContentTaxonomyMixin, ModelSystemDefinedTaxonomy):
|
||||
"""
|
||||
Organization system-defined taxonomy that accepts ContentTags
|
||||
|
||||
Side note: The organization of an object is already encoded in its usage ID,
|
||||
but a Taxonomy with Organization as Tags is being used so that the objects can be
|
||||
indexed and can be filtered in the same tagging system, without any special casing.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
@property
|
||||
def object_tag_class(self) -> Type:
|
||||
"""
|
||||
Returns OrganizationModelObjectTag as ObjectTag subclass associated with this taxonomy.
|
||||
"""
|
||||
return OrganizationModelObjectTag
|
||||
|
||||
|
||||
class ContentLanguageTaxonomy(ContentTaxonomyMixin, LanguageTaxonomy):
|
||||
"""
|
||||
Language system-defined taxonomy that accepts ContentTags
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class ContentAuthorTaxonomy(ContentTaxonomyMixin, UserSystemDefinedTaxonomy):
|
||||
"""
|
||||
Author system-defined taxonomy that accepts ContentTags
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
71
openedx/features/content_tagging/tests/test_models.py
Normal file
71
openedx/features/content_tagging/tests/test_models.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Test for Content models
|
||||
"""
|
||||
import ddt
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from openedx_tagging.core.tagging.models import (
|
||||
ObjectTag,
|
||||
Tag,
|
||||
)
|
||||
from openedx_tagging.core.tagging.api import create_taxonomy
|
||||
from ..models import (
|
||||
ContentLanguageTaxonomy,
|
||||
ContentAuthorTaxonomy,
|
||||
ContentOrganizationTaxonomy,
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestSystemDefinedModels(TestCase):
|
||||
"""
|
||||
Test for System defined models
|
||||
"""
|
||||
|
||||
@ddt.data(
|
||||
(ContentLanguageTaxonomy, "taxonomy"), # Invalid object key
|
||||
(ContentLanguageTaxonomy, "tag"), # Invalid external_id, invalid language
|
||||
(ContentLanguageTaxonomy, "object"), # Invalid object key
|
||||
(ContentAuthorTaxonomy, "taxonomy"), # Invalid object key
|
||||
(ContentAuthorTaxonomy, "tag"), # Invalid external_id, User don't exits
|
||||
(ContentAuthorTaxonomy, "object"), # Invalid object key
|
||||
(ContentOrganizationTaxonomy, "taxonomy"), # Invalid object key
|
||||
(ContentOrganizationTaxonomy, "tag"), # Invalid external_id, Organization don't exits
|
||||
(ContentOrganizationTaxonomy, "object"), # Invalid object key
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_validations(
|
||||
self,
|
||||
taxonomy_cls,
|
||||
check,
|
||||
):
|
||||
"""
|
||||
Test that the respective validations are being called
|
||||
"""
|
||||
taxonomy = create_taxonomy(
|
||||
name='Test taxonomy',
|
||||
taxonomy_class=taxonomy_cls,
|
||||
)
|
||||
|
||||
tag = Tag(
|
||||
value="value",
|
||||
external_id="external_id",
|
||||
taxonomy=taxonomy,
|
||||
)
|
||||
tag.save()
|
||||
|
||||
object_tag = ObjectTag(
|
||||
object_id='object_id',
|
||||
taxonomy=taxonomy,
|
||||
tag=tag,
|
||||
)
|
||||
|
||||
check_taxonomy = check == 'taxonomy'
|
||||
check_object = check == 'object'
|
||||
check_tag = check == 'tag'
|
||||
assert not taxonomy.validate_object_tag(
|
||||
object_tag=object_tag,
|
||||
check_taxonomy=check_taxonomy,
|
||||
check_object=check_object,
|
||||
check_tag=check_tag,
|
||||
)
|
||||
@@ -3,7 +3,11 @@
|
||||
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 ObjectTag, Tag
|
||||
from openedx_tagging.core.tagging.models import (
|
||||
ObjectTag,
|
||||
Tag,
|
||||
UserSystemDefinedTaxonomy,
|
||||
)
|
||||
from organizations.models import Organization
|
||||
|
||||
from common.djangoapps.student.auth import add_users, update_org_role
|
||||
@@ -136,7 +140,8 @@ class TestRulesTaxonomy(TestTaxonomyMixin, TestCase):
|
||||
system_taxonomy = api.create_taxonomy(
|
||||
name="System Languages",
|
||||
)
|
||||
system_taxonomy.system_defined = True
|
||||
system_taxonomy.taxonomy_class = UserSystemDefinedTaxonomy
|
||||
system_taxonomy = system_taxonomy.cast()
|
||||
assert self.superuser.has_perm(perm, system_taxonomy)
|
||||
assert not self.staff.has_perm(perm, system_taxonomy)
|
||||
assert not self.user_all_orgs.has_perm(perm, system_taxonomy)
|
||||
|
||||
@@ -773,7 +773,7 @@ openedx-filters==1.4.0
|
||||
# -r requirements/edx/kernel.in
|
||||
# lti-consumer-xblock
|
||||
# skill-tagging
|
||||
openedx-learning==0.1.0
|
||||
openedx-learning==0.1.1
|
||||
# via -r requirements/edx/kernel.in
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
|
||||
@@ -1307,7 +1307,7 @@ openedx-filters==1.4.0
|
||||
# -r requirements/edx/testing.txt
|
||||
# lti-consumer-xblock
|
||||
# skill-tagging
|
||||
openedx-learning==0.1.0
|
||||
openedx-learning==0.1.1
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -2138,6 +2138,7 @@ walrus==0.9.3
|
||||
# edx-event-bus-redis
|
||||
watchdog==3.0.0
|
||||
# via
|
||||
# -r requirements/edx/development.in
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
wcwidth==0.2.6
|
||||
|
||||
@@ -556,7 +556,7 @@ edx-drf-extensions==8.8.0
|
||||
# edx-rbac
|
||||
# edx-when
|
||||
# edxval
|
||||
edx-enterprise==4.0.6
|
||||
edx-enterprise==4.0.7
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -916,7 +916,7 @@ openedx-filters==1.4.0
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
# skill-tagging
|
||||
openedx-learning==0.1.0
|
||||
openedx-learning==0.1.1
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
|
||||
@@ -115,7 +115,7 @@ openedx-calc # Library supporting mathematical calculatio
|
||||
openedx-django-require
|
||||
openedx-events>=8.3.0 # Open edX Events from Hooks Extension Framework (OEP-50)
|
||||
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
|
||||
openedx-learning<=0.1
|
||||
openedx-learning # Open edX Learning core (experimental)
|
||||
openedx-mongodbproxy
|
||||
openedx-django-wiki
|
||||
openedx-blockstore
|
||||
|
||||
@@ -985,7 +985,7 @@ openedx-filters==1.4.0
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
# skill-tagging
|
||||
openedx-learning==0.1.0
|
||||
openedx-learning==0.1.1
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
|
||||
Reference in New Issue
Block a user