Mark components like libraryv2 and problem bank beta in API to be used by both legacy templates and new authoring mfe. Also updates order of components.
674 lines
28 KiB
Python
674 lines
28 KiB
Python
"""
|
|
Studio component views
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.http import Http404, HttpResponseBadRequest
|
|
from django.shortcuts import redirect
|
|
from django.utils.translation import gettext as _
|
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
|
from django.views.decorators.http import require_GET
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import UsageKey
|
|
from xblock.core import XBlock
|
|
from xblock.django.request import django_to_webob_request, webob_to_django_response
|
|
from xblock.exceptions import NoSuchHandlerError
|
|
from xblock.plugin import PluginMissingError
|
|
from xblock.runtime import Mixologist
|
|
|
|
from common.djangoapps.edxmako.shortcuts import render_to_response
|
|
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.helpers import is_unit
|
|
from cms.djangoapps.contentstore.toggles import (
|
|
libraries_v1_enabled,
|
|
libraries_v2_enabled,
|
|
use_new_problem_editor,
|
|
use_new_unit_page,
|
|
)
|
|
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
|
|
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
|
|
|
|
__all__ = [
|
|
'container_handler',
|
|
'component_handler',
|
|
'container_embed_handler',
|
|
]
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# NOTE: This list is disjoint from ADVANCED_COMPONENT_TYPES
|
|
COMPONENT_TYPES = [
|
|
'html',
|
|
'video',
|
|
'problem',
|
|
'itembank',
|
|
'library_v2', # Not an XBlock
|
|
'library',
|
|
'discussion',
|
|
'openassessment',
|
|
'drag-and-drop-v2',
|
|
]
|
|
|
|
BETA_COMPONENT_TYPES = ['library_v2', 'itembank']
|
|
|
|
ADVANCED_COMPONENT_TYPES = sorted({name for name, class_ in XBlock.load_classes()} - set(COMPONENT_TYPES))
|
|
|
|
ADVANCED_PROBLEM_TYPES = settings.ADVANCED_PROBLEM_TYPES
|
|
|
|
LIBRARY_BLOCK_TYPES = settings.LIBRARY_BLOCK_TYPES
|
|
|
|
CONTAINER_TEMPLATES = [
|
|
"basic-modal", "modal-button", "edit-xblock-modal",
|
|
"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", "tag-list",
|
|
"unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button",
|
|
"edit-title-button", "edit-upstream-alert",
|
|
]
|
|
|
|
|
|
def _advanced_component_types(show_unsupported):
|
|
"""
|
|
Return advanced component types which can be created.
|
|
|
|
Args:
|
|
show_unsupported: if True, unsupported XBlocks may be included in the return value
|
|
|
|
Returns:
|
|
A dict of authorable XBlock types and their support levels (see XBlockStudioConfiguration). For example:
|
|
{
|
|
"done": "us", # unsupported
|
|
"discussion: "fs" # fully supported
|
|
}
|
|
Note that the support level will be "True" for all XBlocks if XBlockStudioConfigurationFlag
|
|
is not enabled.
|
|
"""
|
|
enabled_block_types = _filter_disabled_blocks(ADVANCED_COMPONENT_TYPES)
|
|
if XBlockStudioConfigurationFlag.is_enabled():
|
|
authorable_blocks = authorable_xblocks(allow_unsupported=show_unsupported)
|
|
filtered_blocks = {}
|
|
for block in authorable_blocks:
|
|
if block.name in enabled_block_types:
|
|
filtered_blocks[block.name] = block.support_level
|
|
return filtered_blocks
|
|
else:
|
|
all_blocks = {}
|
|
for block_name in enabled_block_types:
|
|
all_blocks[block_name] = True
|
|
return all_blocks
|
|
|
|
|
|
def _load_mixed_class(category):
|
|
"""
|
|
Load an XBlock by category name, and apply all defined mixins
|
|
"""
|
|
# Libraries v2 content doesn't have an XBlock.
|
|
if category == 'library_v2':
|
|
return None
|
|
|
|
component_class = XBlock.load_class(category)
|
|
mixologist = Mixologist(settings.XBLOCK_MIXINS)
|
|
return mixologist.mix(component_class)
|
|
|
|
|
|
@require_GET
|
|
@login_required
|
|
def container_handler(request, usage_key_string): # pylint: disable=too-many-statements
|
|
"""
|
|
The restful handler for container xblock requests.
|
|
|
|
GET
|
|
html: returns the HTML page for editing a container
|
|
json: not currently supported
|
|
"""
|
|
|
|
from ..utils import get_container_handler_context, get_unit_url
|
|
|
|
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
|
|
|
try:
|
|
usage_key = UsageKey.from_string(usage_key_string)
|
|
except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string'
|
|
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
|
with modulestore().bulk_operations(usage_key.course_key):
|
|
try:
|
|
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
|
|
except ItemNotFoundError:
|
|
return HttpResponseBadRequest()
|
|
|
|
is_unit_page = is_unit(xblock)
|
|
unit = xblock if is_unit_page else None
|
|
|
|
if is_unit_page and use_new_unit_page(course.id):
|
|
return redirect(get_unit_url(course.id, unit.location))
|
|
|
|
container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
|
|
container_handler_context.update({
|
|
'draft_preview_link': preview_lms_link,
|
|
'published_preview_link': lms_link,
|
|
})
|
|
return render_to_response('container.html', container_handler_context)
|
|
else:
|
|
return HttpResponseBadRequest("Only supports HTML requests")
|
|
|
|
|
|
@require_GET
|
|
@login_required
|
|
@xframe_options_exempt
|
|
def container_embed_handler(request, usage_key_string): # pylint: disable=too-many-statements
|
|
"""
|
|
Returns an HttpResponse with HTML content for the container XBlock.
|
|
The returned HTML is a chromeless rendering of the XBlock.
|
|
|
|
GET
|
|
html: returns the HTML page for editing a container
|
|
json: not currently supported
|
|
"""
|
|
|
|
# Avoiding a circular dependency
|
|
from ..utils import get_container_handler_context
|
|
|
|
try:
|
|
usage_key = UsageKey.from_string(usage_key_string)
|
|
except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string'
|
|
return HttpResponseBadRequest()
|
|
with modulestore().bulk_operations(usage_key.course_key):
|
|
try:
|
|
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
|
|
except ItemNotFoundError:
|
|
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
|
|
|
container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
|
|
return render_to_response('container_chromeless.html', container_handler_context)
|
|
|
|
|
|
def get_component_templates(courselike, library=False): # lint-amnesty, pylint: disable=too-many-statements
|
|
"""
|
|
Returns the applicable component templates that can be used by the specified course or library.
|
|
"""
|
|
|
|
from ..helpers import xblock_type_display_name
|
|
|
|
def create_template_dict(name, category, support_level, boilerplate_name=None, tab="common", hinted=False):
|
|
"""
|
|
Creates a component template dict.
|
|
|
|
Parameters
|
|
display_name: the user-visible name of the component
|
|
category: the type of component (problem, html, etc.)
|
|
support_level: the support level of this component
|
|
boilerplate_name: name of boilerplate for filling in default values. May be None.
|
|
hinted: True if hinted problem else False
|
|
tab: common(default)/advanced, which tab it goes in
|
|
|
|
"""
|
|
return {
|
|
"display_name": name,
|
|
"category": category,
|
|
"boilerplate_name": boilerplate_name,
|
|
"hinted": hinted,
|
|
"tab": tab,
|
|
"support_level": support_level
|
|
}
|
|
|
|
def component_support_level(editable_types, name, template=None):
|
|
"""
|
|
Returns the support level for the given xblock name/template combination.
|
|
|
|
Args:
|
|
editable_types: a QuerySet of xblocks with their support levels
|
|
name: the name of the xblock
|
|
template: optional template for the xblock
|
|
|
|
Returns:
|
|
If XBlockStudioConfigurationFlag is enabled, returns the support level
|
|
(see XBlockStudioConfiguration) or False if this xblock name/template combination
|
|
has no Studio support at all. If XBlockStudioConfigurationFlag is disabled,
|
|
simply returns True.
|
|
"""
|
|
# If the Studio support feature is disabled, return True for all.
|
|
if not XBlockStudioConfigurationFlag.is_enabled():
|
|
return True
|
|
if template is None:
|
|
template = ""
|
|
extension_index = template.rfind(".yaml")
|
|
if extension_index >= 0:
|
|
template = template[0:extension_index]
|
|
for block in editable_types:
|
|
if block.name == name and block.template == template:
|
|
return block.support_level
|
|
|
|
return False
|
|
|
|
def create_support_legend_dict():
|
|
"""
|
|
Returns a dict of settings information for the display of the support level legend.
|
|
"""
|
|
return {
|
|
"show_legend": XBlockStudioConfigurationFlag.is_enabled(),
|
|
"allow_unsupported_xblocks": allow_unsupported,
|
|
"documentation_label": _("{platform_name} Support Levels:").format(platform_name=settings.PLATFORM_NAME)
|
|
}
|
|
|
|
component_display_names = {
|
|
'discussion': _("Discussion"),
|
|
'html': _("Text"),
|
|
'problem': _("Problem"),
|
|
'video': _("Video"),
|
|
'openassessment': _("Open Response"),
|
|
'library': _("Legacy Library"),
|
|
'library_v2': _("Library Content"),
|
|
'itembank': _("Problem Bank"),
|
|
'drag-and-drop-v2': _("Drag and Drop"),
|
|
}
|
|
|
|
component_templates = []
|
|
categories = set()
|
|
# The component_templates array is in the order of "advanced" (if present), followed
|
|
# by the components in the order listed in COMPONENT_TYPES.
|
|
component_types = COMPONENT_TYPES[:]
|
|
|
|
# Libraries do not support discussions, drag-and-drop, and openassessment and other libraries
|
|
component_not_supported_by_library = [
|
|
'discussion',
|
|
'library',
|
|
'openassessment',
|
|
'drag-and-drop-v2',
|
|
'library_v2',
|
|
'itembank',
|
|
]
|
|
if library:
|
|
component_types = [component for component in component_types
|
|
if component not in set(component_not_supported_by_library)]
|
|
|
|
component_types = _filter_disabled_blocks(component_types)
|
|
|
|
# Filter out discussion component from component_types if non-legacy discussion provider is configured for course
|
|
component_types = _filter_discussion_for_non_legacy_provider(component_types, courselike.location.course_key)
|
|
|
|
# Content Libraries currently don't allow opting in to unsupported xblocks/problem types.
|
|
allow_unsupported = getattr(courselike, "allow_unsupported_xblocks", False)
|
|
|
|
for category in component_types: # lint-amnesty, pylint: disable=too-many-nested-blocks
|
|
authorable_variations = authorable_xblocks(allow_unsupported=allow_unsupported, name=category)
|
|
support_level_without_template = component_support_level(authorable_variations, category)
|
|
templates_for_category = []
|
|
component_class = _load_mixed_class(category)
|
|
|
|
if support_level_without_template and category not in ['library']:
|
|
# add the default template with localized display name
|
|
# TODO: Once mixins are defined per-application, rather than per-runtime,
|
|
# this should use a cms mixed-in class. (cpennington)
|
|
template_id = None
|
|
display_name = xblock_type_display_name(category, _('Blank'))
|
|
# The ORA "blank" assessment should be Peer Assessment Only
|
|
if category == 'openassessment':
|
|
display_name = _("Peer Assessment Only")
|
|
template_id = "peer-assessment"
|
|
elif category == 'problem':
|
|
# Override generic "Problem" name to describe this blank template:
|
|
display_name = _("Blank Problem")
|
|
templates_for_category.append(
|
|
create_template_dict(display_name, category, support_level_without_template, template_id, 'advanced')
|
|
)
|
|
categories.add(category)
|
|
|
|
# add boilerplates
|
|
if hasattr(component_class, 'templates'):
|
|
for template in component_class.templates():
|
|
filter_templates = getattr(component_class, 'filter_templates', None)
|
|
if not filter_templates or filter_templates(template, courselike):
|
|
template_id = template.get('template_id')
|
|
support_level_with_template = component_support_level(
|
|
authorable_variations, category, template_id
|
|
)
|
|
if support_level_with_template:
|
|
# Tab can be 'common' 'advanced'
|
|
# Default setting is common/advanced depending on the presence of markdown
|
|
tab = 'common'
|
|
if template['metadata'].get('markdown') is None:
|
|
tab = 'advanced'
|
|
hinted = template.get('hinted', False)
|
|
|
|
templates_for_category.append(
|
|
create_template_dict(
|
|
_(template['metadata'].get('display_name')), # lint-amnesty, pylint: disable=translation-of-non-string
|
|
category,
|
|
support_level_with_template,
|
|
template_id,
|
|
tab,
|
|
hinted,
|
|
)
|
|
)
|
|
|
|
#If using new problem editor, we select problem type inside the editor
|
|
# because of this, we only show one problem.
|
|
if category == 'problem' and use_new_problem_editor():
|
|
templates_for_category = [
|
|
template for template in templates_for_category if template['boilerplate_name'] == 'blank_common.yaml'
|
|
]
|
|
|
|
# Add any advanced problem types. Note that these are different xblocks being stored as Advanced Problems,
|
|
# currently not supported in libraries .
|
|
if category == 'problem' and not library and not use_new_problem_editor():
|
|
disabled_block_names = [block.name for block in disabled_xblocks()]
|
|
advanced_problem_types = [advanced_problem_type for advanced_problem_type in ADVANCED_PROBLEM_TYPES
|
|
if advanced_problem_type['component'] not in disabled_block_names]
|
|
for advanced_problem_type in advanced_problem_types:
|
|
component = advanced_problem_type['component']
|
|
boilerplate_name = advanced_problem_type['boilerplate_name']
|
|
|
|
authorable_advanced_component_variations = authorable_xblocks(
|
|
allow_unsupported=allow_unsupported, name=component
|
|
)
|
|
advanced_component_support_level = component_support_level(
|
|
authorable_advanced_component_variations, component, boilerplate_name
|
|
)
|
|
if advanced_component_support_level:
|
|
try:
|
|
component_display_name = xblock_type_display_name(component)
|
|
except PluginMissingError:
|
|
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
|
|
else:
|
|
templates_for_category.append(
|
|
create_template_dict(
|
|
component_display_name,
|
|
component,
|
|
advanced_component_support_level,
|
|
boilerplate_name,
|
|
'advanced'
|
|
)
|
|
)
|
|
categories.add(component)
|
|
|
|
# Add library block types.
|
|
if category == 'library' and not library:
|
|
disabled_block_names = [block.name for block in disabled_xblocks()]
|
|
library_block_types = [problem_type for problem_type in LIBRARY_BLOCK_TYPES
|
|
if problem_type['component'] not in disabled_block_names]
|
|
for library_block_type in library_block_types:
|
|
component = library_block_type['component']
|
|
boilerplate_name = library_block_type['boilerplate_name']
|
|
authorable_variations = authorable_xblocks(allow_unsupported=allow_unsupported, name=component)
|
|
library_component_support_level = component_support_level(
|
|
authorable_variations, component, boilerplate_name
|
|
)
|
|
if library_component_support_level:
|
|
try:
|
|
component_display_name = xblock_type_display_name(component, default_display_name=component)
|
|
except PluginMissingError:
|
|
log.warning(
|
|
"Unable to load xblock type %s to read display_name",
|
|
component
|
|
)
|
|
else:
|
|
templates_for_category.append(
|
|
create_template_dict(
|
|
component_display_name,
|
|
component,
|
|
library_component_support_level,
|
|
boilerplate_name
|
|
)
|
|
)
|
|
categories.add(component)
|
|
|
|
component_templates.append({
|
|
"type": category,
|
|
"templates": templates_for_category,
|
|
"display_name": component_display_names[category],
|
|
"support_legend": create_support_legend_dict(),
|
|
"beta": category in BETA_COMPONENT_TYPES,
|
|
})
|
|
|
|
# Libraries do not support advanced components at this time.
|
|
if library:
|
|
return component_templates
|
|
|
|
# Check if there are any advanced modules specified in the course policy.
|
|
# These modules should be specified as a list of strings, where the strings
|
|
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
|
|
# enabled for the course.
|
|
course_advanced_keys = courselike.advanced_modules
|
|
advanced_component_templates = {
|
|
"type": "advanced",
|
|
"templates": [],
|
|
"display_name": _("Advanced"),
|
|
"support_legend": create_support_legend_dict()
|
|
}
|
|
advanced_component_types = _advanced_component_types(allow_unsupported)
|
|
# Set component types according to course policy file
|
|
if isinstance(course_advanced_keys, list):
|
|
for category in course_advanced_keys:
|
|
if category in advanced_component_types.keys() and category not in categories: # pylint: disable=consider-iterating-dictionary
|
|
# boilerplates not supported for advanced components
|
|
try:
|
|
component_display_name = xblock_type_display_name(category, default_display_name=category)
|
|
advanced_component_templates['templates'].append(
|
|
create_template_dict(
|
|
component_display_name,
|
|
category,
|
|
advanced_component_types[category]
|
|
)
|
|
)
|
|
categories.add(category)
|
|
except PluginMissingError:
|
|
# dhm: I got this once but it can happen any time the
|
|
# course author configures an advanced component which does
|
|
# not exist on the server. This code here merely
|
|
# prevents any authors from trying to instantiate the
|
|
# non-existent component type by not showing it in the menu
|
|
log.warning(
|
|
"Advanced component %s does not exist. It will not be added to the Studio new component menu.",
|
|
category
|
|
)
|
|
else:
|
|
log.error(
|
|
"Improper format for course advanced keys! %s",
|
|
course_advanced_keys
|
|
)
|
|
if advanced_component_templates['templates']:
|
|
component_templates.append(advanced_component_templates)
|
|
|
|
return component_templates
|
|
|
|
|
|
def _filter_discussion_for_non_legacy_provider(all_components, course_key):
|
|
"""
|
|
Filter out Discussion component if non-legacy discussion provider is configured for course key
|
|
"""
|
|
discussion_provider = DiscussionsConfiguration.get(context_key=course_key).provider_type
|
|
|
|
if discussion_provider != 'legacy':
|
|
filtered_components = [component for component in all_components if component != 'discussion']
|
|
else:
|
|
filtered_components = all_components
|
|
|
|
return filtered_components
|
|
|
|
|
|
def _filter_disabled_blocks(all_blocks):
|
|
"""
|
|
Filter out disabled xblocks from the provided list of xblock names.
|
|
"""
|
|
disabled_block_names = [block.name for block in disabled_xblocks()]
|
|
if not libraries_v1_enabled():
|
|
disabled_block_names.append('library')
|
|
if not libraries_v2_enabled():
|
|
disabled_block_names.append('library_v2')
|
|
disabled_block_names.append('itembank')
|
|
return [block_name for block_name in all_blocks if block_name not in disabled_block_names]
|
|
|
|
|
|
@login_required
|
|
def _get_item_in_course(request, usage_key):
|
|
"""
|
|
Helper method for getting the old location, containing course,
|
|
item, lms_link, and preview_lms_link for a given locator.
|
|
|
|
Verifies that the caller has permission to access this item.
|
|
"""
|
|
|
|
from ..utils import get_lms_link_for_item
|
|
|
|
# usage_key's course_key may have an empty run property
|
|
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
|
|
|
|
course_key = usage_key.course_key
|
|
|
|
if not has_course_author_access(request.user, course_key):
|
|
raise PermissionDenied()
|
|
|
|
course = modulestore().get_course(course_key)
|
|
item = modulestore().get_item(usage_key, depth=1)
|
|
lms_link = get_lms_link_for_item(item.location)
|
|
preview_lms_link = get_lms_link_for_item(item.location, preview=True)
|
|
|
|
return course, item, lms_link, preview_lms_link
|
|
|
|
|
|
@login_required
|
|
def component_handler(request, usage_key_string, handler, suffix=''):
|
|
"""
|
|
Dispatch an AJAX action to an xblock
|
|
|
|
Args:
|
|
usage_id: The usage-id of the block to dispatch to
|
|
handler (str): The handler to execute
|
|
suffix (str): The remainder of the url to be passed to the handler
|
|
|
|
Returns:
|
|
:class:`django.http.HttpResponse`: The response from the handler, converted to a
|
|
django response
|
|
"""
|
|
usage_key = UsageKey.from_string(usage_key_string)
|
|
|
|
# Let the block handle the AJAX
|
|
req = django_to_webob_request(request)
|
|
|
|
try:
|
|
if is_xblock_aside(usage_key):
|
|
# Get the block being wrapped by the aside (not the aside itself)
|
|
block = modulestore().get_item(usage_key.usage_key)
|
|
handler_block = get_aside_from_xblock(block, usage_key.aside_type)
|
|
asides = [handler_block]
|
|
else:
|
|
block = modulestore().get_item(usage_key)
|
|
handler_block = block
|
|
asides = []
|
|
load_services_for_studio(handler_block.runtime, request.user)
|
|
resp = handler_block.handle(handler, req, suffix)
|
|
except NoSuchHandlerError:
|
|
log.info("XBlock %s attempted to access missing handler %r", handler_block, handler, exc_info=True)
|
|
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
|
|
|
# unintentional update to handle any side effects of handle call
|
|
# could potentially be updating actual course data or simply caching its values
|
|
# Addendum:
|
|
# TNL 101-62 studio write permission is also checked for editing content.
|
|
|
|
if has_course_author_access(request.user, usage_key.course_key):
|
|
modulestore().update_item(block, request.user.id, asides=asides)
|
|
else:
|
|
#fail quietly if user is not course author.
|
|
log.warning(
|
|
"%s does not have have studio write permissions on course: %s. write operations not performed on %r",
|
|
request.user.id,
|
|
usage_key.course_key,
|
|
handler
|
|
)
|
|
|
|
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(str(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
|