Skip to content
Open
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
6 changes: 0 additions & 6 deletions .annotation_safe_list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,6 @@ openedx_content.ContainerType:
".. no_pii:": "No PII"
openedx_content.ContainerVersion:
".. no_pii:": "No PII"
openedx_content.Draft:
".. no_pii:": "No PII"
openedx_content.EntityList:
".. no_pii:": "No PII"
openedx_content.EntityListRow:
Expand All @@ -398,10 +396,6 @@ openedx_content.PublishLogRecord:
".. no_pii:": "No PII"
openedx_content.PublishableEntity:
".. no_pii:": "No PII"
openedx_content.PublishableEntityVersion:
".. no_pii:": "No PII"
openedx_content.PublishableEntityVersionDependency:
".. no_pii:": "No PII"
openedx_content.Published:
".. no_pii:": "No PII"
openedx_content.Section:
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ def _import_xml_node_to_parent(
content_tagging_api.set_all_object_tags(
content_key=new_xblock.location,
object_tags=object_tags,
ignore_invalid_orgs=True,
)

return new_xblock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,16 +256,19 @@ def _setup_tagged_content(self, course_key) -> dict:
tags=["tag_1", "tag_2"],
)

# Tag our blocks using a taxonomy that isn't enabled for any orgs -- these tags won't be pasted
# Tag our blocks using a taxonomy that isn't enabled for any orgs -- these tags won't be pasted.
taxonomy_no_org = tagging_api.create_taxonomy("test_taxonomy_no_org", "Test Taxonomy No Org")
Tag.objects.create(taxonomy=taxonomy_no_org, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_no_org, value="tag_2")
# We have to temporarily make it apply to "all orgs", then we can apply the tags then change back to "no orgs":
tagging_api.set_taxonomy_orgs(taxonomy_no_org, all_orgs=True)
for object_key in all_block_keys.values():
tagging_api.tag_object(
object_id=str(object_key),
taxonomy=taxonomy_no_org,
tags=["tag_1", "tag_2"],
)
tagging_api.set_taxonomy_orgs(taxonomy_no_org, all_orgs=False, orgs=[])

return all_block_keys

Expand Down
4 changes: 0 additions & 4 deletions openedx/core/djangoapps/content/search/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ def setUp(self):

self.orgA = OrganizationFactory.create(short_name="orgA")

self.patcher = patch("openedx.core.djangoapps.content_tagging.tasks.modulestore", return_value=self.store)
self.addCleanup(self.patcher.stop)
self.patcher.start()

api.clear_meilisearch_client() # Clear the Meilisearch client to avoid leaking state from other tests

def test_create_delete_xblock(self, meilisearch_client):
Expand Down
55 changes: 35 additions & 20 deletions openedx/core/djangoapps/content_tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Iterator # noqa: UP035

import openedx_tagging.api as oel_tagging
from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef, Q, QuerySet
from django.utils.timezone import now
from opaque_keys.edx.keys import CollectionKey, ContainerKey, CourseKey, UsageKey
Expand All @@ -25,6 +26,16 @@
from .utils import check_taxonomy_context_key_org, get_content_key_from_string, get_context_key_from_key


class InvalidOrgException(ValidationError):
"""
Exception when attempting to use a taxonomy that's not valid for the given
org's content.
"""

def __init__(self, message: str):
"""Initialize with the detailed error message"""
super().__init__(message)

def create_taxonomy(
name: str,
description: str | None = None,
Expand Down Expand Up @@ -71,9 +82,6 @@ def set_taxonomy_orgs(
If not `all_orgs`, the taxonomy is associated with each org in the `orgs` list. If that list is empty, the
taxonomy is not associated with any orgs.
"""
if taxonomy.system_defined:
raise ValueError("Cannot set orgs for a system-defined taxonomy")

TaxonomyOrg.objects.filter(
taxonomy=taxonomy,
rel_type=relationship,
Expand Down Expand Up @@ -103,7 +111,6 @@ def get_taxonomies_for_org(
Generates a list of the enabled Taxonomies available for the given org, sorted by name.

We return a QuerySet here for ease of use with Django Rest Framework and other query-based use cases.
So be sure to use `Taxonomy.cast()` to cast these instances to the appropriate subclass before use.

If no `org` is provided, then only Taxonomies which are available for _all_ Organizations are returned.

Expand Down Expand Up @@ -200,22 +207,27 @@ def get_all_object_tags(
def set_all_object_tags(
content_key: ContentKey,
object_tags: TagValuesByTaxonomyIdDict,
ignore_invalid_orgs=False,
) -> None:
"""
Sets the tags for the given content object.

If any of the taxonomies are not configured for use by the org that owns
the content, this will raise ``InvalidOrgException``, unless you also pass
``ignore_invalid_orgs=True``.
"""
for taxonomy_id, tags_values in object_tags.items():

taxonomy = oel_tagging.get_taxonomy(taxonomy_id)

if not taxonomy:
continue

tag_object(
object_id=str(content_key),
taxonomy=taxonomy,
tags=tags_values,
)
try:
tag_object(object_id=str(content_key), taxonomy=taxonomy, tags=tags_values)
except InvalidOrgException:
if ignore_invalid_orgs:
pass
else:
raise


def generate_csv_rows(object_id, buffer) -> Iterator[str]:
Expand Down Expand Up @@ -372,18 +384,20 @@ def copy_object_tags(

for taxonomy_id, taxonomy in taxonomies.items():
tags = source_object_tags.get(taxonomy_id, [])
tag_object(
object_id=str(dest_content_key),
taxonomy=taxonomy,
tags=tags,
)
try:
tag_object(
object_id=str(dest_content_key),
taxonomy=taxonomy,
tags=tags,
)
except InvalidOrgException:
pass


def tag_object(
object_id: str,
taxonomy: Taxonomy,
tags: list[str],
object_tag_class: type[ObjectTag] = ObjectTag,
) -> None:
"""
Replaces the existing ObjectTag entries for the given taxonomy + object_id
Expand All @@ -394,9 +408,6 @@ def tag_object(

tags: A list of the values of the tags from this taxonomy to apply.

object_tag_class: Optional. Use a proxy subclass of ObjectTag for additional
validation. (e.g. only allow tagging certain types of objects.)

Raised Tag.DoesNotExist if the proposed tags are invalid for this taxonomy.
Preserves existing (valid) tags, adds new (valid) tags, and removes omitted
(or invalid) tags.
Expand Down Expand Up @@ -427,6 +438,10 @@ def tag_object(
time=now(),
content_object=ContentObjectData(object_id=object_id)
)
else:
raise InvalidOrgException(
f'Taxonomy "{taxonomy.name}" is not configured for use with organization "{context_key.org}".'
)

# Expose the oel_tagging APIs

Expand Down
110 changes: 26 additions & 84 deletions openedx/core/djangoapps/content_tagging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,41 @@

import logging

import crum
from django.dispatch import receiver
from openedx_events.content_authoring.data import CourseData, DuplicatedXBlockData, LibraryBlockData, XBlockData
from openedx_events.content_authoring.data import DuplicatedXBlockData, LibraryBlockData, XBlockData
from openedx_events.content_authoring.signals import (
COURSE_CREATED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_UPDATED,
XBLOCK_CREATED,
LIBRARY_BLOCK_DELETED,
XBLOCK_DELETED,
XBLOCK_DUPLICATED,
XBLOCK_UPDATED,
)

from .api import copy_object_tags
from .tasks import (
delete_course_tags,
delete_xblock_tags,
update_course_tags,
update_library_block_tags,
update_xblock_tags,
)
from .toggles import CONTENT_TAGGING_AUTO
from . import api
from .types import ContentKey

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_info':
# We want to add tags only to the course id, not with its XBlock
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))
return

update_xblock_tags.delay(str(xblock_info.usage_key))
def _delete_tags(content_object: ContentKey) -> None:
"""Delete all tags associated with the given XBlock/course/etc."""
log.info("Deleting tags for %s", content_object)
# This is super fast; no need to do it from a celery task.
api.delete_object_tags(str(content_object))


@receiver(XBLOCK_DELETED)
def delete_tag_xblock(**kwargs):
"""
Automatically delete XBlock auto tags.
Delete an XBlock's tags when the block itself is deleted.
"""
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))

_delete_tags(xblock_info.usage_key.course_key)

@receiver(LIBRARY_BLOCK_CREATED)
@receiver(LIBRARY_BLOCK_UPDATED)
def auto_tag_library_block(**kwargs):
"""
Automatically tag Library Blocks based on metadata
"""
if not CONTENT_TAGGING_AUTO.is_enabled():
return

library_block_data = kwargs.get("library_block", None)
if not library_block_data or not isinstance(library_block_data, LibraryBlockData):
log.error("Received null or incorrect data for event")
return

current_request = crum.get_current_request()
update_library_block_tags.delay(
str(library_block_data.usage_key), current_request.LANGUAGE_CODE
)
_delete_tags(xblock_info.usage_key)


@receiver(XBLOCK_DUPLICATED)
Expand All @@ -122,7 +51,20 @@ def duplicate_tags(**kwargs):
log.error("Received null or incorrect data for event")
return

copy_object_tags(
api.copy_object_tags(
xblock_data.source_usage_key,
xblock_data.usage_key,
)


@receiver(LIBRARY_BLOCK_DELETED)
def library_block_deleted(**kwargs) -> None:
"""
Delete an XBlock's tags when the block itself is deleted.
"""
library_block_data = kwargs.get("library_block", None)
if not library_block_data or not isinstance(library_block_data, LibraryBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

_delete_tags(library_block_data.usage_key)
Loading
Loading