-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: export tagged course as csv #34091
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 57 commits into
openedx:master
from
open-craft:rpenido/fal-3610-download-course-tag-spreadsheet
Feb 16, 2024
Merged
Changes from all commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
52bc665
feat: export tagged course as csv
rpenido f275908
docs: add comment
rpenido a870dfd
fix: add select_related to ObjectTag query
rpenido 4dd027a
fix: always use objecttag.value
rpenido 5fb03aa
docs: change comment position
rpenido 8f1238e
refactor: rename serializer to a more sane name
rpenido bdedf93
refactor: remove download param
rpenido 04ca072
refactor: create a new view to export objecttags
rpenido a8a6e7e
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido b01d6d4
refactor: change api and view structure
rpenido 0871537
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido 18a8425
docs: remove old comment
rpenido 38ae353
docs: revert view docstring
rpenido b65a6c8
fix: remove include_children query param
rpenido f5ac3a6
fix: removing .all() call from queryset
rpenido debf254
refactor: method rename
rpenido 9726d6d
fix: filter deleted tags
rpenido c8c12bb
fix: pylint
rpenido 1f11b17
fix: quote string in csv export
rpenido 484c042
test: add querycount
rpenido 6b6ba34
fix: pylint
rpenido 9096454
fix: pylint..
rpenido a688689
fix: pylint
rpenido 30e06d0
test: compare results to hardcoded strings
pomegranited 35a3d2b
test: Adds "deleted" object tags to ensure they are omitted from results
pomegranited 1e13f54
test: adds untagged blocks with children
pomegranited e9335c8
revert: undo removed property
rpenido ab1a69e
style: fix camelCase
rpenido db9116d
refactor: remove xblock from TaggedContent and include_children param
rpenido 9b3dee8
fix: remove UsageKey
rpenido 821e216
fix: removing unused import
rpenido 6207915
refactor: cleaning code
rpenido 35bc860
test: refactors tests so shared data can be re-used
pomegranited 7a28742
refactor: refactoring api, helper and view code
rpenido fabb729
docs: add comment about ObjectTag query
rpenido 548d57c
test: use CourseFactory and BlockFactory
pomegranited a1d41fd
test: fix variable name
rpenido f07b841
fix: delete unwanted file
rpenido 233135a
test: fix query count
rpenido ac98812
test: fix expected value with new tags
rpenido 309ce94
fix: use variables from outer function
rpenido 32cdf93
test: use UserFactory
rpenido 4ed7570
style: removed unused imports
rpenido ee5bc3d
chore: trigger CI
rpenido 4bc8ca7
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido 6d4c23a
fix: disable default staff user from module store mixin
rpenido da3fdf9
style: fix case
rpenido 726b7ef
Merge branch 'jill/rpenido/fal-3610-download-course-tag-spreadsheet' …
rpenido fd5a542
docs: adds removed docstring
rpenido 79a1786
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido dfe43be
fix: cleaning merged code
rpenido 5245264
style: run isort
rpenido c82e9cb
refactor: use taxonomy.export_id in header
rpenido 01b9b5f
refactor: change `object_id` to `context_id`
rpenido f87fc4c
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido 779cc98
chore: trigger CI
rpenido 4a3d092
Merge branch 'master' into rpenido/fal-3610-download-course-tag-sprea…
rpenido 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
88 changes: 88 additions & 0 deletions
88
openedx/core/djangoapps/content_tagging/rest_api/v1/objecttag_export_helpers.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,88 @@ | ||
| """ | ||
| This module contains helper functions to build a object tree with object tags. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Iterator | ||
|
|
||
| from attrs import define | ||
| from opaque_keys.edx.keys import CourseKey, LearningContextKey | ||
|
|
||
| from xmodule.modulestore.django import modulestore | ||
|
|
||
| from ...types import ObjectTagByObjectIdDict, ObjectTagByTaxonomyIdDict | ||
|
|
||
|
|
||
| @define | ||
| class TaggedContent: | ||
| """ | ||
| A tagged content, with its tags and children. | ||
| """ | ||
| display_name: str | ||
| block_id: str | ||
| category: str | ||
| object_tags: ObjectTagByTaxonomyIdDict | ||
| children: list[TaggedContent] | None | ||
|
|
||
|
|
||
| def iterate_with_level( | ||
| tagged_content: TaggedContent, level: int = 0 | ||
| ) -> Iterator[tuple[TaggedContent, int]]: | ||
| """ | ||
| Iterator that yields the tagged content and the level of the block | ||
| """ | ||
| yield tagged_content, level | ||
| if tagged_content.children: | ||
| for child in tagged_content.children: | ||
| yield from iterate_with_level(child, level + 1) | ||
|
|
||
|
|
||
| def build_object_tree_with_objecttags( | ||
| content_key: LearningContextKey, | ||
| object_tag_cache: ObjectTagByObjectIdDict, | ||
| ) -> TaggedContent: | ||
| """ | ||
| Returns the object with the tags associated with it. | ||
| """ | ||
| store = modulestore() | ||
|
|
||
| if isinstance(content_key, CourseKey): | ||
| course = store.get_course(content_key) | ||
| if course is None: | ||
| raise ValueError(f"Course not found: {content_key}") | ||
| else: | ||
| raise NotImplementedError(f"Invalid content_key: {type(content_key)} -> {content_key}") | ||
|
|
||
| display_name = course.display_name_with_default | ||
| course_id = str(course.id) | ||
|
|
||
| tagged_course = TaggedContent( | ||
| display_name=display_name, | ||
| block_id=course_id, | ||
| category=course.category, | ||
| object_tags=object_tag_cache.get(str(content_key), {}), | ||
| children=None, | ||
| ) | ||
|
|
||
| blocks = [(tagged_course, course)] | ||
|
|
||
| while blocks: | ||
| tagged_block, xblock = blocks.pop() | ||
| tagged_block.children = [] | ||
|
|
||
| if xblock.has_children: | ||
| for child_id in xblock.children: | ||
| child_block = store.get_item(child_id) | ||
| tagged_child = TaggedContent( | ||
| display_name=child_block.display_name_with_default, | ||
| block_id=str(child_id), | ||
| category=child_block.category, | ||
| object_tags=object_tag_cache.get(str(child_id), {}), | ||
| children=None, | ||
| ) | ||
| tagged_block.children.append(tagged_child) | ||
|
|
||
| blocks.append((tagged_child, child_block)) | ||
|
|
||
| return tagged_course |
177 changes: 177 additions & 0 deletions
177
openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_objecttag_export_helpers.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,177 @@ | ||
| """ | ||
| Test the objecttag_export_helpers module | ||
| """ | ||
| from unittest.mock import patch | ||
|
|
||
| from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase | ||
| from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory | ||
|
|
||
| from .... import api | ||
| from ....tests.test_api import TestGetAllObjectTagsMixin | ||
| from ..objecttag_export_helpers import TaggedContent, build_object_tree_with_objecttags, iterate_with_level | ||
|
|
||
|
|
||
| class TaggedCourseMixin(TestGetAllObjectTagsMixin, ModuleStoreTestCase): # type: ignore[misc] | ||
| """ | ||
| Mixin with a course structure and taxonomies | ||
| """ | ||
| MODULESTORE = TEST_DATA_SPLIT_MODULESTORE | ||
| CREATE_USER = False | ||
|
|
||
| def setUp(self): | ||
| super().setUp() | ||
|
|
||
| # Patch modulestore | ||
| self.patcher = patch("openedx.core.djangoapps.content_tagging.tasks.modulestore", return_value=self.store) | ||
| self.addCleanup(self.patcher.stop) | ||
| self.patcher.start() | ||
|
|
||
| # Create course | ||
| self.course = CourseFactory.create( | ||
| org=self.orgA.short_name, | ||
| number="test_course", | ||
| run="test_run", | ||
| display_name="Test Course", | ||
| ) | ||
| self.expected_tagged_xblock = TaggedContent( | ||
| display_name="Test Course", | ||
| block_id="course-v1:orgA+test_course+test_run", | ||
| category="course", | ||
| children=[], | ||
| object_tags={ | ||
| self.taxonomy_1.id: list(self.course_tags), | ||
| }, | ||
| ) | ||
|
|
||
| # Create XBlocks | ||
| self.sequential = BlockFactory.create( | ||
| parent=self.course, | ||
| category="sequential", | ||
| display_name="test sequential", | ||
| ) | ||
| # Tag blocks | ||
| tagged_sequential = TaggedContent( | ||
| display_name="test sequential", | ||
| block_id="block-v1:orgA+test_course+test_run+type@sequential+block@test_sequential", | ||
| category="sequential", | ||
| children=[], | ||
| object_tags={ | ||
| self.taxonomy_1.id: list(self.sequential_tags1), | ||
| self.taxonomy_2.id: list(self.sequential_tags2), | ||
| }, | ||
| ) | ||
|
|
||
| assert self.expected_tagged_xblock.children is not None # type guard | ||
| self.expected_tagged_xblock.children.append(tagged_sequential) | ||
|
|
||
| # Untagged blocks | ||
| sequential2 = BlockFactory.create( | ||
| parent=self.course, | ||
| category="sequential", | ||
| display_name="untagged sequential", | ||
| ) | ||
| untagged_sequential = TaggedContent( | ||
| display_name="untagged sequential", | ||
| block_id="block-v1:orgA+test_course+test_run+type@sequential+block@untagged_sequential", | ||
| category="sequential", | ||
| children=[], | ||
| object_tags={}, | ||
| ) | ||
| assert self.expected_tagged_xblock.children is not None # type guard | ||
| self.expected_tagged_xblock.children.append(untagged_sequential) | ||
| BlockFactory.create( | ||
| parent=sequential2, | ||
| category="vertical", | ||
| display_name="untagged vertical", | ||
| ) | ||
| untagged_vertical = TaggedContent( | ||
| display_name="untagged vertical", | ||
| block_id="block-v1:orgA+test_course+test_run+type@vertical+block@untagged_vertical", | ||
| category="vertical", | ||
| children=[], | ||
| object_tags={}, | ||
| ) | ||
| assert untagged_sequential.children is not None # type guard | ||
| untagged_sequential.children.append(untagged_vertical) | ||
| # /Untagged blocks | ||
|
|
||
| vertical = BlockFactory.create( | ||
| parent=self.sequential, | ||
| category="vertical", | ||
| display_name="test vertical1", | ||
| ) | ||
| tagged_vertical = TaggedContent( | ||
| display_name="test vertical1", | ||
| block_id="block-v1:orgA+test_course+test_run+type@vertical+block@test_vertical1", | ||
| category="vertical", | ||
| children=[], | ||
| object_tags={ | ||
| self.taxonomy_2.id: list(self.vertical1_tags), | ||
| }, | ||
| ) | ||
| assert tagged_sequential.children is not None # type guard | ||
| tagged_sequential.children.append(tagged_vertical) | ||
|
|
||
| vertical2 = BlockFactory.create( | ||
| parent=self.sequential, | ||
| category="vertical", | ||
| display_name="test vertical2", | ||
| ) | ||
| untagged_vertical2 = TaggedContent( | ||
| display_name="test vertical2", | ||
| block_id="block-v1:orgA+test_course+test_run+type@vertical+block@test_vertical2", | ||
| category="vertical", | ||
| children=[], | ||
| object_tags={}, | ||
| ) | ||
| assert tagged_sequential.children is not None # type guard | ||
| tagged_sequential.children.append(untagged_vertical2) | ||
|
|
||
| html = BlockFactory.create( | ||
| parent=vertical2, | ||
| category="html", | ||
| display_name="test html", | ||
| ) | ||
| tagged_text = TaggedContent( | ||
| display_name="test html", | ||
| block_id="block-v1:orgA+test_course+test_run+type@html+block@test_html", | ||
| category="html", | ||
| children=[], | ||
| object_tags={ | ||
| self.taxonomy_2.id: list(self.html_tags), | ||
| }, | ||
| ) | ||
| assert untagged_vertical2.children is not None # type guard | ||
| untagged_vertical2.children.append(tagged_text) | ||
|
|
||
| self.all_object_tags, _ = api.get_all_object_tags(self.course.id) | ||
| self.expected_tagged_content_list = [ | ||
| (self.expected_tagged_xblock, 0), | ||
| (tagged_sequential, 1), | ||
| (tagged_vertical, 2), | ||
| (untagged_vertical2, 2), | ||
| (tagged_text, 3), | ||
| (untagged_sequential, 1), | ||
| (untagged_vertical, 2), | ||
| ] | ||
rpenido marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| class TestContentTagChildrenExport(TaggedCourseMixin): # type: ignore[misc] | ||
| """ | ||
| Test helper functions for exporting tagged content | ||
| """ | ||
| def test_build_object_tree(self) -> None: | ||
| """ | ||
| Test if we can export a course | ||
| """ | ||
| with self.assertNumQueries(3): | ||
| tagged_xblock = build_object_tree_with_objecttags(self.course.id, self.all_object_tags) | ||
|
|
||
| assert tagged_xblock == self.expected_tagged_xblock | ||
|
|
||
| def test_iterate_with_level(self) -> None: | ||
| """ | ||
| Test if we can iterate over the tagged content in the correct order | ||
| """ | ||
| tagged_content_list = list(iterate_with_level(self.expected_tagged_xblock)) | ||
| assert tagged_content_list == self.expected_tagged_content_list | ||
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.
Uh oh!
There was an error while loading. Please reload this page.