feat: add language auto-tagging with feature flag (#32907)

This commit is contained in:
Rômulo Penido
2023-09-01 15:09:46 -03:00
committed by GitHub
parent 6704901a77
commit 6e28ba329e
15 changed files with 636 additions and 102 deletions

View File

@@ -1,12 +1,13 @@
"""
Content Tagging APIs
"""
from typing import Iterator, List, Type, Union
from __future__ import annotations
from typing import Iterator, List, Type
import openedx_tagging.core.tagging.api as oel_tagging
from django.db.models import QuerySet
from opaque_keys.edx.keys import LearningContextKey
from opaque_keys.edx.locator import BlockUsageLocator
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_tagging.core.tagging.models import Taxonomy
from organizations.models import Organization
@@ -117,9 +118,9 @@ def get_content_tags(
def tag_content_object(
taxonomy: Taxonomy,
tags: List,
object_id: Union[BlockUsageLocator, LearningContextKey],
) -> List[ContentObjectTag]:
tags: list,
object_id: CourseKey | UsageKey,
) -> list[ContentObjectTag]:
"""
This is the main API to use when you want to add/update/delete tags from a content object (e.g. an XBlock or
course).
@@ -150,4 +151,5 @@ def tag_content_object(
get_taxonomy = oel_tagging.get_taxonomy
get_taxonomies = oel_tagging.get_taxonomies
get_tags = oel_tagging.get_tags
delete_object_tags = oel_tagging.delete_object_tags
resync_object_tags = oel_tagging.resync_object_tags

View File

@@ -10,3 +10,7 @@ class ContentTaggingConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "openedx.features.content_tagging"
def ready(self):
# Connect signal handlers
from . import handlers # pylint: disable=unused-import

View File

@@ -1,22 +0,0 @@
- 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

View File

@@ -0,0 +1,76 @@
"""
Automatic tagging of content
"""
import logging
from django.dispatch import receiver
from openedx_events.content_authoring.data import CourseData, XBlockData
from openedx_events.content_authoring.signals import COURSE_CREATED, XBLOCK_CREATED, XBLOCK_DELETED, XBLOCK_UPDATED
from .tasks import delete_course_tags
from .tasks import (
delete_xblock_tags,
update_course_tags,
update_xblock_tags
)
from .toggles import CONTENT_TAGGING_AUTO
log = logging.getLogger(__name__)
@receiver(COURSE_CREATED)
def auto_tag_course(**kwargs):
"""
Automatically tag course based on their metadata
"""
course_data = kwargs.get("course", None)
if not course_data or not isinstance(course_data, CourseData):
log.error("Received null or incorrect data for event")
return
if not CONTENT_TAGGING_AUTO.is_enabled(course_data.course_key):
return
update_course_tags.delay(str(course_data.course_key))
@receiver(XBLOCK_CREATED)
@receiver(XBLOCK_UPDATED)
def auto_tag_xblock(**kwargs):
"""
Automatically tag XBlock based on their metadata
"""
xblock_info = kwargs.get("xblock_info", None)
if not xblock_info or not isinstance(xblock_info, XBlockData):
log.error("Received null or incorrect data for event")
return
if not CONTENT_TAGGING_AUTO.is_enabled(xblock_info.usage_key.course_key):
return
if xblock_info.block_type == "course":
# Course update is handled by XBlock of course type
update_course_tags.delay(str(xblock_info.usage_key.course_key))
update_xblock_tags.delay(str(xblock_info.usage_key))
@receiver(XBLOCK_DELETED)
def delete_tag_xblock(**kwargs):
"""
Automatically delete XBlock auto tags.
"""
xblock_info = kwargs.get("xblock_info", None)
if not xblock_info or not isinstance(xblock_info, XBlockData):
log.error("Received null or incorrect data for event")
return
if not CONTENT_TAGGING_AUTO.is_enabled(xblock_info.usage_key.course_key):
return
if xblock_info.block_type == "course":
# Course deletion is handled by XBlock of course type
delete_course_tags.delay(str(xblock_info.usage_key.course_key))
delete_xblock_tags.delay(str(xblock_info.usage_key))

View File

@@ -1,37 +1,62 @@
# 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')
Taxonomy = apps.get_model("oel_tagging", "Taxonomy")
author_taxonomy = Taxonomy(
pk=-2,
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,
)
ContentAuthorTaxonomy = apps.get_model("content_tagging", "ContentAuthorTaxonomy")
author_taxonomy.taxonomy_class = ContentAuthorTaxonomy
author_taxonomy.save()
org_taxonomy = Taxonomy(
pk=-3,
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,
)
ContentOrganizationTaxonomy = apps.get_model("content_tagging", "ContentOrganizationTaxonomy")
org_taxonomy.taxonomy_class = ContentOrganizationTaxonomy
org_taxonomy.save()
# Adding taxonomy class to the language taxonomy
Taxonomy = apps.get_model('oel_tagging', 'Taxonomy')
language_taxonomy = Taxonomy.objects.get(id=-1)
ContentLanguageTaxonomy = apps.get_model("content_tagging", "ContentLanguageTaxonomy")
language_taxonomy.taxonomy_class = ContentLanguageTaxonomy
language_taxonomy.save()
def revert_system_defined_taxonomies(apps, schema_editor):
"""
Deletes all system defined taxonomies
"""
Taxonomy = apps.get_model('oel_tagging', 'Taxonomy')
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'),
("content_tagging", "0002_system_defined_taxonomies"),
]
operations = [

View File

@@ -0,0 +1,49 @@
from django.db import migrations
def load_system_defined_org_taxonomies(apps, _schema_editor):
"""
Associates the system defined taxonomy Language (id=-1) to all orgs and
removes the ContentOrganizationTaxonomy (id=-3) from the database
"""
TaxonomyOrg = apps.get_model("content_tagging", "TaxonomyOrg")
TaxonomyOrg.objects.create(id=-1, taxonomy_id=-1, org=None)
Taxonomy = apps.get_model("oel_tagging", "Taxonomy")
Taxonomy.objects.get(id=-3).delete()
def revert_system_defined_org_taxonomies(apps, _schema_editor):
"""
Deletes association of system defined taxonomy Language (id=-1) to all orgs and
creates the ContentOrganizationTaxonomy (id=-3) in the database
"""
TaxonomyOrg = apps.get_model("content_tagging", "TaxonomyOrg")
TaxonomyOrg.objects.get(id=-1).delete()
Taxonomy = apps.get_model("oel_tagging", "Taxonomy")
org_taxonomy = Taxonomy(
pk=-3,
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,
)
ContentOrganizationTaxonomy = apps.get_model("content_tagging", "ContentOrganizationTaxonomy")
org_taxonomy.taxonomy_class = ContentOrganizationTaxonomy
org_taxonomy.save()
class Migration(migrations.Migration):
dependencies = [
("content_tagging", "0003_system_defined_fixture"),
]
operations = [
migrations.RunPython(load_system_defined_org_taxonomies, revert_system_defined_org_taxonomies),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.20 on 2023-08-30 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('content_tagging', '0004_system_defined_org'),
]
operations = [
migrations.DeleteModel(
name='ContentOrganizationTaxonomy',
),
migrations.DeleteModel(
name='OrganizationModelObjectTag',
),
]

View File

@@ -9,5 +9,4 @@ from .base import (
from .system_defined import (
ContentLanguageTaxonomy,
ContentAuthorTaxonomy,
ContentOrganizationTaxonomy,
)

View File

@@ -1,7 +1,7 @@
"""
Content Tagging models
"""
from typing import List, Union
from __future__ import annotations
from django.db import models
from django.db.models import Exists, OuterRef, Q, QuerySet
@@ -49,7 +49,7 @@ class TaxonomyOrg(models.Model):
@classmethod
def get_relationships(
cls, taxonomy: Taxonomy, rel_type: RelType, org_short_name: Union[str, None] = None
cls, taxonomy: Taxonomy, rel_type: RelType, org_short_name: str | None = None
) -> QuerySet:
"""
Returns the relationships of the given rel_type and taxonomy where:
@@ -68,7 +68,7 @@ class TaxonomyOrg(models.Model):
@classmethod
def get_organizations(
cls, taxonomy: Taxonomy, rel_type: RelType
) -> List[Organization]:
) -> list[Organization]:
"""
Returns the list of Organizations which have the given relationship to the taxonomy.
"""
@@ -91,7 +91,7 @@ class ContentObjectTag(ObjectTag):
proxy = True
@property
def object_key(self) -> Union[BlockUsageLocator, LearningContextKey]:
def object_key(self) -> BlockUsageLocator | LearningContextKey:
"""
Returns the object ID parsed as a UsageKey or LearningContextKey.
Raises InvalidKeyError object_id cannot be parse into one of those key types.
@@ -115,7 +115,7 @@ class ContentTaxonomyMixin:
def taxonomies_for_org(
cls,
queryset: QuerySet,
org: Organization = None,
org: Organization | None = None,
) -> QuerySet:
"""
Filters the given QuerySet to those ContentTaxonomies which are available for the given organization.

View File

@@ -1,62 +1,14 @@
"""
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

View File

@@ -0,0 +1,169 @@
"""
Defines asynchronous celery task for auto-tagging content
"""
from __future__ import annotations
import logging
from celery import shared_task
from celery_utils.logged_task import LoggedTask
from django.conf import settings
from django.contrib.auth import get_user_model
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_tagging.core.tagging.models import Taxonomy
from xmodule.modulestore.django import modulestore
from . import api
LANGUAGE_TAXONOMY_ID = -1
log = logging.getLogger(__name__)
User = get_user_model()
def _has_taxonomy(taxonomy: Taxonomy, content_object: CourseKey | UsageKey) -> bool:
"""
Return True if this Taxonomy have some Tag set in the content_object
"""
_exausted = object()
content_tags = api.get_content_tags(object_id=str(content_object), taxonomy_id=taxonomy.id)
return next(content_tags, _exausted) is not _exausted
def _set_initial_language_tag(content_object: CourseKey | UsageKey, lang: str) -> None:
"""
Create a tag for the language taxonomy in the content_object if it doesn't exist.
If the language is not configured in the plataform or the language tag doesn't exist,
use the default language of the platform.
"""
lang_taxonomy = Taxonomy.objects.get(pk=LANGUAGE_TAXONOMY_ID)
if lang and not _has_taxonomy(lang_taxonomy, content_object):
tags = api.get_tags(lang_taxonomy)
is_language_configured = any(lang_code == lang for lang_code, _ in settings.LANGUAGES) is not None
if not is_language_configured:
logging.warning(
"Language not configured in the plataform: %s. Using default language: %s",
lang,
settings.LANGUAGE_CODE,
)
lang = settings.LANGUAGE_CODE
lang_tag = next((tag for tag in tags if tag.external_id == lang), None)
if lang_tag is None:
if not is_language_configured:
logging.error(
"Language tag not found for default language: %s. Skipping", lang
)
return
logging.warning(
"Language tag not found for language: %s. Using default language: %s", lang, settings.LANGUAGE_CODE
)
lang_tag = next(tag for tag in tags if tag.external_id == settings.LANGUAGE_CODE)
api.tag_content_object(lang_taxonomy, [lang_tag.id], content_object)
def _delete_tags(content_object: CourseKey | UsageKey) -> None:
api.delete_object_tags(str(content_object))
@shared_task(base=LoggedTask)
def update_course_tags(course_key_str: str) -> bool:
"""
Updates the automatically-managed tags for a course
(whenever a course is created or updated)
Params:
course_key_str (str): identifier of the Course
"""
try:
course_key = CourseKey.from_string(course_key_str)
log.info("Updating tags for Course with id: %s", course_key)
course = modulestore().get_course(course_key)
if course:
lang = course.language
_set_initial_language_tag(course_key, lang)
return True
except Exception as e: # pylint: disable=broad-except
log.error("Error updating tags for Course with id: %s. %s", course_key, e)
return False
@shared_task(base=LoggedTask)
def delete_course_tags(course_key_str: str) -> bool:
"""
Delete the tags for a Course (when the course itself has been deleted).
Params:
course_key_str (str): identifier of the Course
"""
try:
course_key = CourseKey.from_string(course_key_str)
log.info("Deleting tags for Course with id: %s", course_key)
_delete_tags(course_key)
return True
except Exception as e: # pylint: disable=broad-except
log.error("Error deleting tags for Course with id: %s. %s", course_key, e)
return False
@shared_task(base=LoggedTask)
def update_xblock_tags(usage_key_str: str) -> bool:
"""
Updates the automatically-managed tags for a XBlock
(whenever an XBlock is created/updated).
Params:
usage_key_str (str): identifier of the XBlock
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
log.info("Updating tags for XBlock with id: %s", usage_key)
if usage_key.course_key.is_course:
course = modulestore().get_course(usage_key.course_key)
if course is None:
return True
lang = course.language
else:
return True
_set_initial_language_tag(usage_key, lang)
return True
except Exception as e: # pylint: disable=broad-except
log.error("Error updating tags for XBlock with id: %s. %s", usage_key, e)
return False
@shared_task(base=LoggedTask)
def delete_xblock_tags(usage_key_str: str) -> bool:
"""
Delete the tags for a XBlock (when the XBlock itself is deleted).
Params:
usage_key_str (str): identifier of the XBlock
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
log.info("Deleting tags for XBlock with id: %s", usage_key)
_delete_tags(usage_key)
return True
except Exception as e: # pylint: disable=broad-except
log.error("Error deleting tags for XBlock with id: %s. %s", usage_key, e)
return False

View File

@@ -12,7 +12,6 @@ from openedx_tagging.core.tagging.api import create_taxonomy
from ..models import (
ContentLanguageTaxonomy,
ContentAuthorTaxonomy,
ContentOrganizationTaxonomy,
)
@@ -29,9 +28,6 @@ class TestSystemDefinedModels(TestCase):
(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(

View File

@@ -0,0 +1,240 @@
"""
Test for auto-tagging content
"""
from __future__ import annotations
from unittest.mock import patch
from django.core.management import call_command
from django.test import override_settings
from edx_toggles.toggles.testutils import override_waffle_flag
from openedx_tagging.core.tagging.models import ObjectTag, Tag, Taxonomy
from organizations.models import Organization
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangolib.testing.utils import skip_unless_cms
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, ModuleStoreTestCase
from .. import api
from ..models import ContentLanguageTaxonomy, TaxonomyOrg
from ..toggles import CONTENT_TAGGING_AUTO
LANGUAGE_TAXONOMY_ID = -1
@skip_unless_cms # Auto-tagging is only available in the CMS
@override_waffle_flag(CONTENT_TAGGING_AUTO, active=True)
class TestAutoTagging(ModuleStoreTestCase):
"""
Test if the Course and XBlock tags are automatically created
"""
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
def _check_tag(self, object_id: str, taxonomy_id: int, value: str | None):
"""
Check if the ObjectTag exists for the given object_id and taxonomy_id
If value is None, check if the ObjectTag does not exists
"""
object_tag = ObjectTag.objects.filter(object_id=object_id, taxonomy_id=taxonomy_id).first()
if value is None:
assert not object_tag, f"Expected no tag for taxonomy_id={taxonomy_id}, " \
f"but one found with value={object_tag.value}"
else:
assert object_tag, f"Tag for taxonomy_id={taxonomy_id} with value={value} with expected, but none found"
assert object_tag.value == value, f"Tag value mismatch {object_tag.value} != {value}"
return True
@classmethod
def setUpClass(cls):
# Run fixtures to create the system defined tags
call_command("loaddata", "--app=oel_tagging", "language_taxonomy.yaml")
# Configure language taxonomy
language_taxonomy = Taxonomy.objects.get(id=-1)
language_taxonomy.taxonomy_class = ContentLanguageTaxonomy
language_taxonomy.save()
# Enable Language taxonomy for all orgs
TaxonomyOrg.objects.create(id=-1, taxonomy=language_taxonomy, org=None)
super().setUpClass()
def setUp(self):
super().setUp()
# Create user
self.user = UserFactory.create()
self.user_id = self.user.id
self.orgA = Organization.objects.create(name="Organization A", short_name="orgA")
self.patcher = patch("openedx.features.content_tagging.tasks.modulestore", return_value=self.store)
self.addCleanup(self.patcher.stop)
self.patcher.start()
def test_create_course(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "pt"},
)
# Check if the tags are created in the Course
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, "Portuguese")
@override_settings(LANGUAGE_CODE='pt')
def test_create_course_invalid_language(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "11"},
)
# Check if the tags are created in the Course is the system default
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, "Portuguese")
@override_settings(LANGUAGES=[('pt', 'Portuguese')], LANGUAGE_CODE='pt')
def test_create_course_unsuported_language(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "en"},
)
# Check if the tags are created in the Course is the system default
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, "Portuguese")
@override_settings(LANGUAGE_CODE='pt')
def test_create_course_no_tag_language(self):
# Remove English tag
Tag.objects.filter(taxonomy_id=LANGUAGE_TAXONOMY_ID, value="English").delete()
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "en"},
)
# Check if the tags are created in the Course is the system default
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, "Portuguese")
@override_settings(LANGUAGE_CODE='pt')
def test_create_course_no_tag_default_language(self):
# Remove Portuguese tag
Tag.objects.filter(taxonomy_id=LANGUAGE_TAXONOMY_ID, value="Portuguese").delete()
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "11"},
)
# No tags created
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, None)
def test_update_course(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "pt"},
)
# Simulates user manually changing a tag
lang_taxonomy = Taxonomy.objects.get(pk=LANGUAGE_TAXONOMY_ID)
api.tag_content_object(lang_taxonomy, ["Spanish"], course.id)
# Update course language
course.language = "en"
self.store.update_item(course, self.user_id)
# Does not automatically update the tag
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, "Spanish")
def test_create_delete_xblock(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "pt"},
)
# Create XBlocks
sequential = self.store.create_child(self.user_id, course.location, "sequential", "test_sequential")
vertical = self.store.create_child(self.user_id, sequential.location, "vertical", "test_vertical")
usage_key_str = str(vertical.location)
# Check if the tags are created in the XBlock
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, "Portuguese")
# Delete the XBlock
self.store.delete_item(vertical.location, self.user_id)
# Check if the tags are deleted
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)
@override_waffle_flag(CONTENT_TAGGING_AUTO, active=False)
def test_waffle_disabled_create_update_course(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "pt"},
)
# No tags created
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, None)
# Update course language
course.language = "en"
self.store.update_item(course, self.user_id)
# No tags created
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, None)
@override_waffle_flag(CONTENT_TAGGING_AUTO, active=False)
def test_waffle_disabled_create_delete_xblock(self):
# Create course
course = self.store.create_course(
self.orgA.short_name,
"test_course",
"test_run",
self.user_id,
fields={"language": "pt"},
)
# Create XBlocks
sequential = self.store.create_child(self.user_id, course.location, "sequential", "test_sequential")
vertical = self.store.create_child(self.user_id, sequential.location, "vertical", "test_vertical")
usage_key_str = str(vertical.location)
# No tags created
assert self._check_tag(course.id, LANGUAGE_TAXONOMY_ID, None)
# Delete the XBlock
self.store.delete_item(vertical.location, self.user_id)
# Still no tags
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)

View File

@@ -0,0 +1,17 @@
"""
Toggles for content tagging
"""
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# .. toggle_name: content_tagging.auto
# .. toggle_implementation: WaffleSwitch
# .. toggle_default: False
# .. toggle_description: Setting this enables automatic tagging of content
# .. toggle_type: feature_flag
# .. toggle_category: admin
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2023-08-30
# .. toggle_tickets: https://github.com/openedx/modular-learning/issues/79
CONTENT_TAGGING_AUTO = CourseWaffleFlag('content_tagging.auto', __name__)

View File

@@ -538,13 +538,15 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
assert len(items_in_tree) == expected_items_in_tree
# draft:
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: get draft, get ancestors up to course (2-6), compute inheritance
# sends: update problem and then each ancestor up to course (edit info)
# split:
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: definitions (calculator field), structures
# sends: 2 sends to update index & structure (note, it would also be definition if a content field changed)
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 6, 5), (ModuleStoreEnum.Type.split, 3, 2, 2))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 6, 5), (ModuleStoreEnum.Type.split, 4, 2, 2))
@ddt.unpack
def test_update_item(self, default_ms, num_mysql, max_find, max_send):
"""
@@ -1069,15 +1071,17 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
assert self.store.has_changes(parent)
# Draft
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
# Find: find parents (definition.children query), get parent, get course (fill in run?),
# find parents of the parent (course), get inheritance items,
# get item (to delete subtree), get inheritance again.
# Sends: delete item, update parent
# Split
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
# Find: active_versions, 2 structures (published & draft), definition (unnecessary)
# Sends: updated draft and published structures and active_versions
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 6, 2), (ModuleStoreEnum.Type.split, 4, 2, 3))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 6, 2), (ModuleStoreEnum.Type.split, 5, 2, 3))
@ddt.unpack
def test_delete_item(self, default_ms, num_mysql, max_find, max_send):
"""
@@ -1099,14 +1103,16 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.store.get_item(self.writable_chapter_location, revision=ModuleStoreEnum.RevisionOption.published_only)
# Draft:
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: find parent (definition.children), count versions of item, get parent, count grandparents,
# inheritance items, draft item, draft child, inheritance
# sends: delete draft vertical and update parent
# Split:
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: draft and published structures, definition (unnecessary)
# sends: update published (why?), draft, and active_versions
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 8, 2), (ModuleStoreEnum.Type.split, 4, 3, 3))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 8, 2), (ModuleStoreEnum.Type.split, 5, 3, 3))
@ddt.unpack
def test_delete_private_vertical(self, default_ms, num_mysql, max_find, max_send):
"""
@@ -1154,13 +1160,15 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
assert vert_loc not in course.children
# Draft:
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: find parent (definition.children) 2x, find draft item, get inheritance items
# send: one delete query for specific item
# Split:
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
# find: structure (cached)
# send: update structure and active_versions
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 3, 1), (ModuleStoreEnum.Type.split, 4, 1, 2))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 3, 1), (ModuleStoreEnum.Type.split, 5, 1, 2))
@ddt.unpack
def test_delete_draft_vertical(self, default_ms, num_mysql, max_find, max_send):
"""