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 #32869

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions openedx/features/content_tagging/fixtures/system_defined.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- model: oel_tagging.taxonomy
pk: -2
fields:
name: Organizations
description: Allows tags for any organization ID created on the instance.
bradenmacdonald marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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',),
),
]
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions openedx/features/content_tagging/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Content Tagging and System defined models
"""
from .base import (
TaxonomyOrg,
ContentObjectTag,
ContentTaxonomy,
)
from .system_defined import (
ContentLanguageTaxonomy,
ContentAuthorTaxonomy,
ContentOrganizationTaxonomy,
)
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,13 @@ def object_key(self) -> Union[BlockUsageLocator, LearningContextKey]:
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,
Expand Down Expand Up @@ -164,3 +161,13 @@ def _check_taxonomy(self, object_tag: ObjectTag) -> bool:
).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 changes: 75 additions & 0 deletions openedx/features/content_tagging/models/system_defined.py
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions openedx/features/content_tagging/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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,
)
9 changes: 7 additions & 2 deletions openedx/features/content_tagging/tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -136,7 +140,8 @@ def test_system_taxonomy(self, perm):
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)
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions requirements/edx/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/kernel.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading