-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: adds Content Tagging #32661
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
Merged
bradenmacdonald
merged 2 commits into
openedx:master
from
open-craft:jill/add-content-tagging-smart-objecttags
Jul 26, 2023
Merged
feat: adds Content Tagging #32661
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This can come in a separate PR)
It would be good to add
openedx.features.content_taggingtomypy.iniso you get static type checking, and tosetup.cfgisolated_appsso you can make sure all other parts of the code are only importing fromcontent_tagging.api. I think you can addopenedx_tagging.core.taggingtoisolated_appsas well to make sure we're only importing fromapi.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oo that's very cool.. will remember this for a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cf openedx/modular-learning#83