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: Display tags on the Unit page in Studio (feature-flagged) - Tak… #25

Merged
merged 1 commit into from
May 16, 2024
Merged
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
8 changes: 5 additions & 3 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Common utility functions useful throughout the contentstore
"""

from __future__ import annotations
import logging
from contextlib import contextmanager
from datetime import datetime
Expand Down Expand Up @@ -395,7 +395,7 @@ def get_taxonomy_list_url():
return taxonomy_list_url


def get_taxonomy_tags_widget_url(course_locator) -> str:
def get_taxonomy_tags_widget_url(course_locator=None) -> str | None:
"""
Gets course authoring microfrontend URL for taxonomy tags drawer widget view.

Expand All @@ -404,7 +404,9 @@ def get_taxonomy_tags_widget_url(course_locator) -> str:
taxonomy_tags_widget_url = None
# Uses the same waffle flag as taxonomy list page
if use_tagging_taxonomy_list_page():
mfe_base_url = get_course_authoring_url(course_locator)
mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL
if course_locator:
mfe_base_url = get_course_authoring_url(course_locator)
if mfe_base_url:
taxonomy_tags_widget_url = f'{mfe_base_url}/tagging/components/widget/'
return taxonomy_tags_widget_url
Expand Down
9 changes: 7 additions & 2 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
get_visibility_partition_info,
has_children_visible_to_specific_partition_groups,
is_currently_visible_to_students,
is_self_paced
is_self_paced,
get_taxonomy_tags_widget_url,
)
from .helpers import (
create_xblock,
Expand Down Expand Up @@ -1141,7 +1142,7 @@ def _get_gating_info(course, xblock):
@pluggable_override('OVERRIDE_CREATE_XBLOCK_INFO')
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, # lint-amnesty, pylint: disable=too-many-statements
course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None,
user=None, course=None, is_concise=False):
user=None, course=None, is_concise=False, tags=None):
"""
Creates the information needed for client-side XBlockInfo.

Expand Down Expand Up @@ -1362,6 +1363,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_has_staff_lock'] = ancestor_has_staff_lock(xblock, parent_xblock)
else:
xblock_info['ancestor_has_staff_lock'] = False
if tags is not None:
xblock_info["tags"] = tags
if use_tagging_taxonomy_list_page():
xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url()

if course_outline:
if xblock_info['has_explicit_staff_lock']:
Expand Down
101 changes: 96 additions & 5 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
from cms.djangoapps.contentstore.toggles import use_new_problem_editor
from cms.djangoapps.contentstore.toggles import (
use_new_problem_editor,
use_tagging_taxonomy_list_page,
)
from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
from openedx.core.djangoapps.content_tagging.api import get_object_tags
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order

Expand Down Expand Up @@ -55,7 +59,7 @@
"editor-mode-button", "upload-dialog",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem",
"xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history",
"xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", "tag-list",
"unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button",
"edit-title-button",
]
Expand Down Expand Up @@ -103,7 +107,7 @@ def _load_mixed_class(category):

@require_GET
@login_required
def container_handler(request, usage_key_string):
def container_handler(request, usage_key_string): # pylint: disable=too-many-statements
"""
The restful handler for container xblock requests.

Expand Down Expand Up @@ -170,9 +174,14 @@ def container_handler(request, usage_key_string):
prev_url = quote_plus(prev_url) if prev_url else None
next_url = quote_plus(next_url) if next_url else None

show_unit_tags = use_tagging_taxonomy_list_page()
unit_tags = None
if show_unit_tags and is_unit_page:
unit_tags = get_unit_tags(usage_key)

# Fetch the XBlock info for use by the container page. Note that it includes information
# about the block's ancestors and siblings for use by the Unit Outline.
xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page)
xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags)

if is_unit_page:
add_container_page_publishing_info(xblock, xblock_info)
Expand Down Expand Up @@ -205,7 +214,8 @@ def container_handler(request, usage_key_string):
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'templates': CONTAINER_TEMPLATES
'templates': CONTAINER_TEMPLATES,
'show_unit_tags': show_unit_tags,
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
Expand Down Expand Up @@ -583,3 +593,84 @@ def component_handler(request, usage_key_string, handler, suffix=''):
)

return webob_to_django_response(resp)


def get_unit_tags(usage_key):
"""
Get the tags of a Unit and build a json to be read by the UI

Note: When migrating the `TagList` subview from `container_subview.js` to the course-authoring MFE,
this function can be simplified to use the REST API of openedx-learning,
which already provides this grouping + sorting logic.
"""
# Get content tags from content tagging API
content_tags = get_object_tags(usage_key)

# Group content tags by taxonomy
taxonomy_dict = {}
for content_tag in content_tags:
taxonomy_id = content_tag.taxonomy_id
# When a taxonomy is deleted, the id here is None.
# In that case the tag is not shown in the UI.
if taxonomy_id:
if taxonomy_id not in taxonomy_dict:
taxonomy_dict[taxonomy_id] = []
taxonomy_dict[taxonomy_id].append(content_tag)

taxonomy_list = []
total_count = 0

def handle_tag(tags, root_ids, tag, child_tag_id=None):
"""
Group each tag by parent to build a tree.
"""
tag_processed_before = tag.id in tags
if not tag_processed_before:
tags[tag.id] = {
'id': tag.id,
'value': tag.value,
'children': [],
}
if child_tag_id:
# Add a child into the children list
tags[tag.id].get('children').append(tags[child_tag_id])
if tag.parent_id is None:
if tag.id not in root_ids:
root_ids.append(tag.id)
elif not tag_processed_before:
# Group all the lineage of this tag.
#
# Skip this if the tag has been processed before,
# we don't need to process lineage again to avoid duplicates.
handle_tag(tags, root_ids, tag.parent, tag.id)

# Build a tag tree for each taxonomy
for content_tag_list in taxonomy_dict.values():
tags = {}
root_ids = []

for content_tag in content_tag_list:
# When a tag is deleted from the taxonomy, the `tag` here is None.
# In that case the tag is not shown in the UI.
if content_tag.tag:
handle_tag(tags, root_ids, content_tag.tag)

taxonomy = content_tag_list[0].taxonomy

if tags:
count = len(tags)
# Add the tree to the taxonomy list
taxonomy_list.append({
'id': taxonomy.id,
'value': taxonomy.name,
'tags': [tags[tag_id] for tag_id in root_ids],
'count': count,
})
total_count += count

unit_tags = {
'count': total_count,
'taxonomies': taxonomy_list,
}

return unit_tags
6 changes: 5 additions & 1 deletion cms/static/js/models/xblock_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ define(
highlights_enabled: false,
highlights_enabled_for_messaging: false,
highlights_preview_only: true,
highlights_doc_url: ''
highlights_doc_url: '',
/**
* List of tags of the unit. This list is managed by the content_tagging module.
*/
tags: null,
},

initialize: function() {
Expand Down
38 changes: 5 additions & 33 deletions cms/static/js/views/course_outline.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* - adding units will automatically redirect to the unit page rather than showing them inline
*/
define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils',
'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop'],
'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop',
'js/views/utils/tagging_drawer_utils',],
function(
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, TaggingDrawerUtils
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
Expand Down Expand Up @@ -215,41 +216,12 @@ function(
}
},

closeManageTagsDrawer(drawer, drawerCover) {
$(drawerCover).css('display', 'none');
$(drawer).empty();
$(drawer).css('display', 'none');
$('body').removeClass('drawer-open');
},

openManageTagsDrawer(event) {
const drawer = document.querySelector("#manage-tags-drawer");
const drawerCover = document.querySelector(".drawer-cover")
openManageTagsDrawer() {
const article = document.querySelector('[data-taxonomy-tags-widget-url]');
const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url');
const contentId = this.model.get('id');

// Add handler to close drawer when dark background is clicked
$(drawerCover).click(function() {
this.closeManageTagsDrawer(drawer, drawerCover);
}.bind(this));

// Add event listen to close drawer when close button is clicked from within the Iframe
window.addEventListener("message", function (event) {
if (event.data === 'closeManageTagsDrawer') {
this.closeManageTagsDrawer(drawer, drawerCover)
}
}.bind(this));

$(drawerCover).css('display', 'block');
// xss-lint: disable=javascript-jquery-html
$(drawer).html(
`<iframe src="${taxonomyTagsWidgetUrl}${contentId}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"></iframe>`
);
$(drawer).css('display', 'block');

// Prevent background from being scrollable when drawer is open
$('body').addClass('drawer-open');
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
},

addButtonActions: function(element) {
Expand Down
23 changes: 20 additions & 3 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils'],
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
'js/views/utils/tagging_drawer_utils'],
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
ContainerSubviews, UnitOutlineView, XBlockUtils) {
ContainerSubviews, UnitOutlineView, XBlockUtils, TaggingDrawerUtils) {
'use strict';
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
Expand All @@ -22,7 +23,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
'click .move-button': 'showMoveXBlockModal',
'click .delete-button': 'deleteXBlock',
'click .show-actions-menu-button': 'showXBlockActionsMenu',
'click .new-component-button': 'scrollToNewComponentButtons'
'click .new-component-button': 'scrollToNewComponentButtons',
'click .tags-button': 'openManageTags',
},

options: {
Expand Down Expand Up @@ -92,6 +94,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
});
this.viewLiveActions.render();

this.tagListView = new ContainerSubviews.TagList({
el: this.$('.unit-tags'),
model: this.model
});
this.tagListView.render();

this.unitOutlineView = new UnitOutlineView({
el: this.$('.wrapper-unit-overview'),
model: this.model
Expand Down Expand Up @@ -119,6 +127,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
xblockView = this.xblockView,
loadingElement = this.$('.ui-loading'),
unitLocationTree = this.$('.unit-location'),
unitTags = this.$('.unit-tags'),
hiddenCss = 'is-hidden';

loadingElement.removeClass(hiddenCss);
Expand All @@ -144,6 +153,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
// Refresh the views now that the xblock is visible
self.onXBlockRefresh(xblockView);
unitLocationTree.removeClass(hiddenCss);
unitTags.removeClass(hiddenCss);

// Re-enable Backbone events for any updated DOM elements
self.delegateEvents();
Expand Down Expand Up @@ -247,6 +257,13 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
this.duplicateComponent(this.findXBlockElement(event.target));
},

openManageTags: function(event) {
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
const contentId = this.findXBlockElement(event.target).data('locator');

TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
},

showMoveXBlockModal: function(event) {
var xblockElement = this.findXBlockElement(event.target),
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
Expand Down
Loading
Loading