-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* refactor: moves is_content_creator from cms.djangoapps.contentstore.helpers to common.djangoapps.student.auth * feat: adds content tagging app Adds models and APIs to support tagging content objects (e.g. XBlocks, content libraries) by content authors. Content tags can be thought of as "name:value" fields, though underneath they are a bit more complicated. * adds dependency on openedx-learning<=0.1.0 * adds tagging app to LMS and CMS * adds content tagging models, api, rules, admin, and tests. * content taxonomies and tags can be maintained per organization by content creators for that organization.
- Loading branch information
1 parent
9d4163d
commit 8098169
Showing
22 changed files
with
1,305 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
""" Tagging app admin """ | ||
from django.contrib import admin | ||
|
||
from .models import TaxonomyOrg | ||
|
||
admin.site.register(TaxonomyOrg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
""" | ||
Content Tagging APIs | ||
""" | ||
from typing import Iterator, List, Type, Union | ||
|
||
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 openedx_tagging.core.tagging.models import Taxonomy | ||
from organizations.models import Organization | ||
|
||
from .models import ContentObjectTag, ContentTaxonomy, TaxonomyOrg | ||
|
||
|
||
def create_taxonomy( | ||
name: str, | ||
description: str = None, | ||
enabled=True, | ||
required=False, | ||
allow_multiple=False, | ||
allow_free_text=False, | ||
taxonomy_class: Type = ContentTaxonomy, | ||
) -> Taxonomy: | ||
""" | ||
Creates, saves, and returns a new Taxonomy with the given attributes. | ||
If `taxonomy_class` not provided, then uses ContentTaxonomy. | ||
""" | ||
return oel_tagging.create_taxonomy( | ||
name=name, | ||
description=description, | ||
enabled=enabled, | ||
required=required, | ||
allow_multiple=allow_multiple, | ||
allow_free_text=allow_free_text, | ||
taxonomy_class=taxonomy_class, | ||
) | ||
|
||
|
||
def set_taxonomy_orgs( | ||
taxonomy: Taxonomy, | ||
all_orgs=False, | ||
orgs: List[Organization] = None, | ||
relationship: TaxonomyOrg.RelType = TaxonomyOrg.RelType.OWNER, | ||
): | ||
""" | ||
Updates the list of orgs associated with the given taxonomy. | ||
Currently, we only have an "owner" relationship, but there may be other types added in future. | ||
When an org has an "owner" relationship with a taxonomy, that taxonomy is available for use by content in that org, | ||
mies | ||
If `all_orgs`, then the taxonomy is associated with all organizations, and the `orgs` parameter is ignored. | ||
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. | ||
""" | ||
TaxonomyOrg.objects.filter( | ||
taxonomy=taxonomy, | ||
rel_type=relationship, | ||
).delete() | ||
|
||
# org=None means the relationship is with "all orgs" | ||
if all_orgs: | ||
orgs = [None] | ||
if orgs: | ||
TaxonomyOrg.objects.bulk_create( | ||
[ | ||
TaxonomyOrg( | ||
taxonomy=taxonomy, | ||
org=org, | ||
rel_type=relationship, | ||
) | ||
for org in orgs | ||
] | ||
) | ||
|
||
|
||
def get_taxonomies_for_org( | ||
enabled=True, | ||
org_owner: Organization = None, | ||
) -> QuerySet: | ||
""" | ||
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. | ||
If you want the disabled Taxonomies, pass enabled=False. | ||
If you want all Taxonomies (both enabled and disabled), pass enabled=None. | ||
""" | ||
taxonomies = oel_tagging.get_taxonomies(enabled=enabled) | ||
return ContentTaxonomy.taxonomies_for_org( | ||
org=org_owner, | ||
queryset=taxonomies, | ||
) | ||
|
||
|
||
def get_content_tags( | ||
object_id: str, taxonomy: Taxonomy = None, valid_only=True | ||
) -> Iterator[ContentObjectTag]: | ||
""" | ||
Generates a list of content tags for a given object. | ||
Pass taxonomy to limit the returned object_tags to a specific taxonomy. | ||
Pass valid_only=False when displaying tags to content authors, so they can see invalid tags too. | ||
Invalid tags will (probably) be hidden from learners. | ||
""" | ||
for object_tag in oel_tagging.get_object_tags( | ||
object_id=object_id, | ||
taxonomy=taxonomy, | ||
valid_only=valid_only, | ||
): | ||
yield ContentObjectTag.cast(object_tag) | ||
|
||
|
||
def tag_content_object( | ||
taxonomy: Taxonomy, | ||
tags: List, | ||
object_id: Union[BlockUsageLocator, LearningContextKey], | ||
) -> 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). | ||
It works one "Taxonomy" at a time, i.e. one field at a time, so you can set call it with taxonomy=Keywords, | ||
tags=["gravity", "newton"] to replace any "Keywords" [Taxonomy] tags on the given content object with "gravity" and | ||
"newton". Doing so to change the "Keywords" Taxonomy won't affect other Taxonomy's tags (other fields) on the | ||
object, such as "Language: [en]" or "Difficulty: [hard]". | ||
If it's a free-text taxonomy, then the list should be a list of tag values. | ||
Otherwise, it should be a list of existing Tag IDs. | ||
Raises ValueError if the proposed tags are invalid for this taxonomy. | ||
Preserves existing (valid) tags, adds new (valid) tags, and removes omitted (or invalid) tags. | ||
""" | ||
content_tags = [] | ||
for object_tag in oel_tagging.tag_object( | ||
taxonomy=taxonomy, | ||
tags=tags, | ||
object_id=str(object_id), | ||
): | ||
content_tags.append(ContentObjectTag.cast(object_tag)) | ||
return content_tags | ||
|
||
|
||
# Expose the oel_tagging APIs | ||
|
||
get_taxonomy = oel_tagging.get_taxonomy | ||
get_taxonomies = oel_tagging.get_taxonomies | ||
get_tags = oel_tagging.get_tags | ||
resync_object_tags = oel_tagging.resync_object_tags |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
""" | ||
Define the content tagging Django App. | ||
""" | ||
|
||
from django.apps import AppConfig | ||
|
||
|
||
class ContentTaggingConfig(AppConfig): | ||
"""App config for the content tagging feature""" | ||
|
||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "openedx.features.content_tagging" |
86 changes: 86 additions & 0 deletions
86
openedx/features/content_tagging/migrations/0001_initial.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# Generated by Django 3.2.20 on 2023-07-25 06:17 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
initial = True | ||
|
||
dependencies = [ | ||
("oel_tagging", "0002_auto_20230718_2026"), | ||
("organizations", "0003_historicalorganizationcourse"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="ContentObjectTag", | ||
fields=[], | ||
options={ | ||
"proxy": True, | ||
"indexes": [], | ||
"constraints": [], | ||
}, | ||
bases=("oel_tagging.objecttag",), | ||
), | ||
migrations.CreateModel( | ||
name="ContentTaxonomy", | ||
fields=[], | ||
options={ | ||
"proxy": True, | ||
"indexes": [], | ||
"constraints": [], | ||
}, | ||
bases=("oel_tagging.taxonomy",), | ||
), | ||
migrations.CreateModel( | ||
name="TaxonomyOrg", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"rel_type", | ||
models.CharField( | ||
choices=[("OWN", "owner")], default="OWN", max_length=3 | ||
), | ||
), | ||
( | ||
"org", | ||
models.ForeignKey( | ||
default=None, | ||
help_text="Organization that is related to this taxonomy.If None, then this taxonomy is related to all organizations.", | ||
null=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
to="organizations.organization", | ||
), | ||
), | ||
( | ||
"taxonomy", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
to="oel_tagging.taxonomy", | ||
), | ||
), | ||
], | ||
), | ||
migrations.AddIndex( | ||
model_name="taxonomyorg", | ||
index=models.Index( | ||
fields=["taxonomy", "rel_type"], name="content_tag_taxonom_b04dd1_idx" | ||
), | ||
), | ||
migrations.AddIndex( | ||
model_name="taxonomyorg", | ||
index=models.Index( | ||
fields=["taxonomy", "rel_type", "org"], | ||
name="content_tag_taxonom_70d60b_idx", | ||
), | ||
), | ||
] |
Empty file.
Oops, something went wrong.