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

feat: paste tags when pasting xblocks with tag data [FC-0049] #34270

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b999e1
feat: paste tags when pasting xblocks with tag data
rpenido Feb 22, 2024
efbcccf
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
rpenido Feb 23, 2024
4b895de
fix: pylint
rpenido Feb 23, 2024
905f711
chore: temp requirements
rpenido Feb 23, 2024
77f95cb
fix: pylint
rpenido Feb 24, 2024
9ac16fa
test: fix objecttag str
rpenido Feb 24, 2024
f9c82a8
refactor: parse data from xml
rpenido Feb 26, 2024
6f76a82
fix: changes from review
rpenido Feb 27, 2024
f1c806f
feat: duplicate tags when duplicating a tagged XBlock. (#637)
pomegranited Feb 29, 2024
74d907a
chore: update openedx-learning version
rpenido Feb 29, 2024
2eb0230
fix: removing temp comment
rpenido Feb 29, 2024
dc2b973
test: fix tag_object param order
rpenido Feb 29, 2024
59e37db
chore: update ora2 version
rpenido Feb 29, 2024
d44efc0
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
rpenido Feb 29, 2024
15288dd
chore: fix inconsistent python dependencies
rpenido Feb 29, 2024
dacba7d
chore: temp update xblock-drag-and-drop-v2 version
rpenido Feb 29, 2024
24c4729
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
rpenido Feb 29, 2024
c1fed5e
Revert "chore: temp update xblock-drag-and-drop-v2 version"
rpenido Feb 29, 2024
2ec5afa
fix: store tags in TaggedBlockMixin.tags_v1 field
pomegranited Mar 1, 2024
b98e2f8
chore: update ora2 to reverted version
rpenido Mar 1, 2024
a3b181e
style: rename add_tags_from_xml() to add_tags_from_field()
rpenido Mar 1, 2024
e0fb8e9
chore: temp ora2 update
rpenido Mar 1, 2024
46556f9
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
rpenido Mar 4, 2024
3166daf
refactor: TaggedBlockMixin (#642)
pomegranited Mar 6, 2024
b4f195a
chore: update edx-ora2 package
rpenido Mar 7, 2024
0f2ac20
docs: improve docstring
rpenido Mar 7, 2024
672f76c
test: change xblock duplication tests to be implementation independent
rpenido Mar 8, 2024
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
8 changes: 8 additions & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@ def _import_xml_node_to_parent(
else:
for child_node in child_nodes:
_import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id)

# ToDo: Check the better place to call it
# I tried to call it in the xml_block.py in the parse_xml() function,
# but the usage_key is not persited yet there
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin
if isinstance(new_xblock, TaggedBlockMixin):
new_xblock.add_tags_from_xml()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rpenido I tested copy/pasting a tagged blocks of various types, but the pasted blocks didn't get tagged, even though I was pasting them into the same course. CC @yusuf-musleh

These are the types I've tried:

Copy link
Contributor Author

@rpenido rpenido Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pomegranited! I tested these block types with mixed results. Maybe for some cases, it's missing a refresh after paste? If that's not the case, could you help me identify the issue? Do you have any errors in the shell?

@yusuf-musleh If you have some time, could you also test to see if I have something different in my stack?

* Text/Html ❌
tag_text.webm
* Zooming Image Tool (listed under Text)  ❌
tag_zooming.webm
* Video ❌
tag_video.webm
* Checkbox and Numerical problems (so probably all CAPA problems are affected) ❌

The checkbox doesn't show the tags icon in the outline. The video was cut, but the copy/paste seems to work.

tag_checkbox.webm
* Drag and Drop v2 ❌

I'm unsure if it is the v2, but the Drag and Drop I tested didn't copy the tags around. I'm looking into it and will edit here if I find something.
edit: it seems to be the same case of OpenAssessments. I will implement the copy/paste the same way we did.

Is there some other XBlock that we should look for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DragAndDrop fixed here: openedx/xblock-drag-and-drop-v2#385

Copy link
Contributor

@pomegranited pomegranited Mar 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rpenido I don't know what was going on with my devstack yesterday, but I'm testing your branch again, and the issues I had yesterday aren't happening now. My apologies!

Is there some other XBlock that we should look for?

There's a lot of XBlocks that are variously maintained by Open edX or community members.. We can't possibly modify them all, and shouldn't have to either.. so I don't think we're headed down the correct path with openedx/xblock-drag-and-drop-v2#385 and openedx/edx-ora2#2181.

I've raised open-craft#640 to handle this issue better than we've been doing it. The issue is that TaggedBlockMixin is relying on the xml_attributes field from XmlMixin in order to do its thing, but not all XBlocks will be XmlMixins (since XmlMixin is not in XBLOCK_MIXINS), and so we can't rely on this field being present.

Instead, I've added a tags_v1 field to TaggedBlockMixin, and use that to store the serialized tag data. What do you think?

CC @yusuf-musleh @bradenmacdonald

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited @rpenido

Oh I see what's going on. So on master we have a method in TaggedBlockMixin that would handle storing the tags in the xml for all the "normal" blocks, since they all call the add_xml_to_node method, it overrides it and extends it. The HTML serialization was separate so we called the add_tags_to_node in there.

Seems like in this PR, we removed the add_xml_to_node method override from TaggedBlockMixin, hence we started seeing some inconsistent behavior, including blocks that do not have that method, because they would inherit it from TaggedBlockMixin.

But I like @pomegranited's idea of having a new field that is separate from all that, and is more explicit on what it is and does, so it doesn't get accidentally removed cause some side effects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was at first thinking of using an "on paste" event handler to just copy the tags over during paste. But I realize that doesn't support the "copy, delete, paste" workflow. So I think yeah, we'd add a new field to StagedContent. It could be a tags field but it's probably better to be a extra_data JSON field and then allow the content_tagging app to use that field to store/retrieve tags during copy/paste, so that the content_staging app doesn't need to be aware of tagging. Another option is to create a StagedContentTags model inside the content_staging app and use an "on copy" event handler to create the StagedContentTags model, and an "on paste" event handler to set the tags on paste.

Copy link
Contributor

@bradenmacdonald bradenmacdonald Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yet another idea requires no new models or DB fields at all. During a copy event, ObjectTag instances could be created that point to the StagedContent instance, rather than pointing to a UsageKey. I like this approach because the tags don't have to be serialized (they are represented in the DB the same way as other "normal" tags on content). The only downside is if the StagedContent contains child blocks, some arbitrary new ID format is required to indicate that tags apply to child blocks inside the StagedContent.

Edit: but if we moved StagedContent to use Components from LearningCore 😂, then we wouldn't have that problem, because each staged component would have its own DB model.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald are you ok with the approach implemented here (for now)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nudge @bradenmacdonald :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is basically done, I'm OK with merging it (because nobody is using this in prod yet), but I expect that next sprint we'll be following up with a PR to change the implementation so it doesn't modify the OLX (but keeping the tests).

return new_xblock


Expand Down
72 changes: 72 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import ddt
from opaque_keys.edx.keys import UsageKey
from rest_framework.test import APIClient
from openedx_tagging.core.tagging.models import Tag
from organizations.models import Organization
from xmodule.modulestore.django import contentstore, modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course
from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, ToyCourseFactory

from cms.djangoapps.contentstore.utils import reverse_usage_url
from openedx.core.djangoapps.content_libraries import api as library_api
from openedx.core.djangoapps.content_tagging import api as tagging_api

CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/"
XBLOCK_ENDPOINT = "/xblock/"
Expand Down Expand Up @@ -141,6 +143,76 @@ def test_copy_and_paste_component(self, block_args):
# The new block should store a reference to where it was copied from
assert dest_block.copied_from_block == str(source_block.location)

def test_copy_and_paste_unit_with_tags(self):
"""
Test copying a unit (vertical) with tags from one course into another
"""
course_key, client = self._setup_course()
dest_course = CourseFactory.create(display_name='Destination Course')
with self.store.bulk_operations(dest_course.id):
dest_chapter = BlockFactory.create(parent=dest_course, category='chapter', display_name='Section')
dest_sequential = BlockFactory.create(parent=dest_chapter, category='sequential', display_name='Subsection')

unit_key = course_key.make_usage_key("vertical", "vertical_test")
# Add tags to the unit
taxonomy_all_org = tagging_api.create_taxonomy("test_taxonomy", "Test Taxonomy")

taxonomy_all_org = tagging_api.create_taxonomy("test_taxonomy", "Test Taxonomy")
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
tagging_api.set_taxonomy_orgs(taxonomy_all_org, all_orgs=True)
Tag.objects.create(taxonomy=taxonomy_all_org, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_all_org, value="tag_2")
# Removing a tag is causing tag_object to fail
# tag_removed = Tag.objects.create(taxonomy=taxonomy, value="tag_removed")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some commented out code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed: 6f76a82

tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_all_org,
# tags=["tag_1", "tag_2", "tag_removed"],
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
tags=["tag_1", "tag_2"],
)

taxonomy_all_org_removed = tagging_api.create_taxonomy("test_taxonomy_removed", "Test Taxonomy Removed")
tagging_api.set_taxonomy_orgs(taxonomy_all_org_removed, all_orgs=True)
Tag.objects.create(taxonomy=taxonomy_all_org_removed, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_all_org_removed, value="tag_2")
tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_all_org_removed,
tags=["tag_1", "tag_2"],
)
tagging_api.get_object_tags(str(unit_key))

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")
tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_no_org,
tags=["tag_1", "tag_2"],
)

# Copy the unit
copy_response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(unit_key)}, format="json")
assert copy_response.status_code == 200

# tag_removed.delete()
taxonomy_all_org_removed.delete()

# Paste the unit
paste_response = client.post(XBLOCK_ENDPOINT, {
"parent_locator": str(dest_sequential.location),
"staged_content": "clipboard",
}, format="json")
assert paste_response.status_code == 200
dest_unit_key = UsageKey.from_string(paste_response.json()["locator"])

# Only tags from the taxonomy that is associated with the dest org should be copied
tags = list(tagging_api.get_object_tags(str(dest_unit_key)))
assert len(tags) == 2
assert str(tags[0]) == '<ObjectTag> ' \
'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_1'
assert str(tags[1]) == '<ObjectTag> ' \
'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2'

def test_paste_with_assets(self):
"""
When pasting into a different course, any required static assets should
Expand Down
24 changes: 23 additions & 1 deletion cms/lib/xblock/tagging/tagged_block_mixin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# lint-amnesty, pylint: disable=missing-module-docstring
from urllib.parse import quote
from urllib.parse import quote, unquote


class TaggedBlockMixin:
Expand Down Expand Up @@ -55,3 +55,25 @@ def add_xml_to_node(self, node):
"""
super().add_xml_to_node(node)
self.add_tags_to_node(node)

def add_tags_from_xml(self):
"""
Parse and add tag data from xml
"""
# This import is done here since we import and use TaggedBlockMixin in the cms settings, but the
# content_tagging app wouldn't have loaded yet, so importing it outside causes an error
from openedx.core.djangoapps.content_tagging.api import set_object_tags

tag_data = self.xml_attributes.get('tags-v1', None) if self.xml_attributes else None
if not tag_data:
return

serialized_tags = tag_data.split(';')
taxonomy_and_tags_dict = {}
for serialized_tag in serialized_tags:
taxonomy_export_id, tags = serialized_tag.split(':')
tags = tags.split(',')
tag_values = [unquote(tag) for tag in tags]
taxonomy_and_tags_dict[taxonomy_export_id] = tag_values

set_object_tags(self.usage_key, taxonomy_and_tags_dict)
31 changes: 29 additions & 2 deletions openedx/core/djangoapps/content_tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from organizations.models import Organization

from .models import TaxonomyOrg
from .types import ObjectTagByObjectIdDict, TaxonomyDict
from .types import ContentKey, ObjectTagByObjectIdDict, TagValuesByTaxonomyExportIdDict, TaxonomyDict
from .utils import check_taxonomy_context_key_org, get_context_key_from_key


def create_taxonomy(
Expand Down Expand Up @@ -156,16 +157,42 @@ def get_all_object_tags(

for object_id, block_tags in groupby(all_object_tags, lambda x: x.object_id):
grouped_object_tags[object_id] = {}
for taxonomy_id, taxonomy_tags in groupby(block_tags, lambda x: x.tag.taxonomy_id):
for taxonomy_id, taxonomy_tags in groupby(block_tags, lambda x: x.tag.taxonomy_id if x.tag else 0):
Copy link
Contributor

@pomegranited pomegranited Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjectTag.objects.filter above guarantees that tag is not null.. why do we need to check that again here (and assert below)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a type guard to make our typer checker happy. I usually use assert for type guards (without error-handling routines), as this is not expected to be thrown at all.

Is there a better way to handle this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mainly puzzled as to why we had to add these checks now, but they weren't needed before. But it's not a big deal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, we "opt-in" to type checking when we add a return type to the function.
Accessing x.tag.taxonomy_id will always throw a type error if the tag is nullable and we didn't check it before calling it.

object_tags_list = list(taxonomy_tags)
grouped_object_tags[object_id][taxonomy_id] = object_tags_list

if taxonomy_id not in taxonomies:
assert object_tags_list[0].tag
assert object_tags_list[0].tag.taxonomy
taxonomies[taxonomy_id] = object_tags_list[0].tag.taxonomy

return grouped_object_tags, taxonomies


def set_object_tags(
content_key: ContentKey,
object_tags: TagValuesByTaxonomyExportIdDict,
) -> None:
"""
Sets the tags for the given content object.
"""
context_key = get_context_key_from_key(content_key)

for taxonomy_export_id, tags_values in object_tags.items():
taxonomy = oel_tagging.get_taxonomy_by_export_id(taxonomy_export_id)
if not taxonomy:
continue

if not check_taxonomy_context_key_org(taxonomy, context_key):
continue

oel_tagging.tag_object(
object_id=str(content_key),
taxonomy=taxonomy,
tags=tags_values,
)


# Expose the oel_tagging APIs

get_taxonomy = oel_tagging.get_taxonomy
Expand Down
14 changes: 3 additions & 11 deletions openedx/core/djangoapps/content_tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
)

from .models import TaxonomyOrg
from .utils import get_context_key_from_key_string, TaggingRulesCache
from .utils import check_taxonomy_context_key_org, get_context_key_from_key_string, rules_cache


rules_cache = TaggingRulesCache()
UserType = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]


Expand Down Expand Up @@ -288,19 +287,12 @@ def can_change_object_tag(
"""
if oel_tagging.can_change_object_tag(user, perm_obj):
if perm_obj and perm_obj.taxonomy and perm_obj.object_id:
# can_change_object_tag_objectid already checked that object_id is valid and has an org,
# so these statements will not fail. But we need to assert to keep the type checker happy.
try:
context_key = get_context_key_from_key_string(perm_obj.object_id)
assert context_key.org
except (ValueError, AssertionError):
except ValueError:
return False # pragma: no cover

is_all_org, taxonomy_orgs = TaxonomyOrg.get_organizations(perm_obj.taxonomy)
if not is_all_org:
# Ensure the object_id's org is among the allowed taxonomy orgs
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and object_org[0] in taxonomy_orgs
return check_taxonomy_context_key_org(perm_obj.taxonomy, context_key)

return True
return False
Expand Down
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/content_tagging/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy

ContentKey = Union[LibraryLocatorV2, CourseKey, UsageKey]
ContextKey = Union[LibraryLocatorV2, CourseKey]

ObjectTagByTaxonomyIdDict = Dict[int, List[ObjectTag]]
ObjectTagByObjectIdDict = Dict[str, ObjectTagByTaxonomyIdDict]
TaxonomyDict = Dict[int, Taxonomy]
TagValuesByTaxonomyExportIdDict = Dict[str, List[str]]
39 changes: 34 additions & 5 deletions openedx/core/djangoapps/content_tagging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_tagging.core.tagging.models import Taxonomy
from organizations.models import Organization

from openedx.core.djangoapps.content_libraries.api import get_libraries_for_user

from .types import ContentKey
from .types import ContentKey, ContextKey
from .models import TaxonomyOrg


def get_content_key_from_string(key_str: str) -> ContentKey:
Expand All @@ -30,11 +32,10 @@ def get_content_key_from_string(key_str: str) -> ContentKey:
raise ValueError("object_id must be a CourseKey, LibraryLocatorV2 or a UsageKey") from usage_key_error


def get_context_key_from_key_string(key_str: str) -> CourseKey | LibraryLocatorV2:
def get_context_key_from_key(content_key: ContentKey) -> ContextKey:
"""
Get context key from an key string
Get context key from an key
rpenido marked this conversation as resolved.
Show resolved Hide resolved
"""
content_key = get_content_key_from_string(key_str)
# If the content key is a CourseKey or a LibraryLocatorV2, return it
if isinstance(content_key, (CourseKey, LibraryLocatorV2)):
return content_key
Expand All @@ -48,6 +49,31 @@ def get_context_key_from_key_string(key_str: str) -> CourseKey | LibraryLocatorV
raise ValueError("context must be a CourseKey or a LibraryLocatorV2")


def get_context_key_from_key_string(key_str: str) -> ContextKey:
"""
Get context key from an key string
"""
content_key = get_content_key_from_string(key_str)
return get_context_key_from_key(content_key)


def check_taxonomy_context_key_org(taxonomy: Taxonomy, context_key: ContextKey) -> bool:
"""
Returns True if the given taxonomy can tag a object with the given context_key.
"""
if not context_key.org:
return False

is_all_org, taxonomy_orgs = TaxonomyOrg.get_organizations(taxonomy)

if is_all_org:
return True

# Ensure the object_id's org is among the allowed taxonomy orgs
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and object_org[0] in taxonomy_orgs


class TaggingRulesCache:
"""
Caches data required for computing rules for the duration of the request.
Expand All @@ -57,7 +83,7 @@ def __init__(self):
"""
Initializes the request cache.
"""
self.request_cache = RequestCache('openedx.core.djangoapps.content_tagging.rules')
self.request_cache = RequestCache('openedx.core.djangoapps.content_tagging.utils')

def get_orgs(self, org_names: list[str] | None = None) -> list[Organization]:
"""
Expand Down Expand Up @@ -102,3 +128,6 @@ def get_library_orgs(self, user, org_names: list[str]) -> list[Organization]:
return [
library_orgs[org_name] for org_name in org_names if org_name in library_orgs
]


rules_cache = TaggingRulesCache()
2 changes: 1 addition & 1 deletion requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/kernel.in
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
Expand Down
2 changes: 1 addition & 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.6.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/kernel.in
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ openedx-calc # Library supporting mathematical calculatio
openedx-django-require
openedx-events # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
openedx-learning # Open edX Learning core (experimental)
# openedx-learning # Open edX Learning core (experimental)
openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function
openedx-mongodbproxy
openedx-django-wiki
openedx-blockstore
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
Expand Down
Loading