Files
edx-platform/openedx/core/djangoapps/discussions/tasks.py
Ahtisham Shahid 33dc8e1f21 feat: added user messages and backed now uses discussion_enabled flag (#31716)
* refactor: simplified tasks.py for discussions

* fix: do not create a topic for the unpublished unit

* feat: added user messages and backed now uses discussion_enabled flag

* fix: update default for discussion_enabled flag

* feat: removed redundant tests and fixes
2023-02-22 12:41:02 +05:00

230 lines
8.7 KiB
Python

"""
Tasks for discussions
"""
import logging
from celery import shared_task
from edx_django_utils.monitoring import set_code_owner_attribute
from opaque_keys.edx.keys import CourseKey
from openedx_events.learning.data import CourseDiscussionConfigurationData, DiscussionTopicContext
from openedx_events.learning.signals import COURSE_DISCUSSIONS_CHANGED
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from .config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
from .models import DiscussionsConfiguration, Provider
from .utils import get_accessible_discussion_xblocks_by_course_id
log = logging.getLogger(__name__)
@shared_task
@set_code_owner_attribute
def update_discussions_settings_from_course_task(course_key_str: str):
"""
Celery task that creates or updates discussions settings for a course.
Args:
course_key_str (str): course key string
"""
course_key = CourseKey.from_string(course_key_str)
config_data = update_discussions_settings_from_course(course_key)
COURSE_DISCUSSIONS_CHANGED.send_event(configuration=config_data)
def update_discussions_settings_from_course(course_key: CourseKey) -> CourseDiscussionConfigurationData:
"""
When there are changes to a course, construct a new data structure containing all the context needed to update the
course's discussion settings in the database.
Args:
course_key (CourseKey): The course that was recently updated.
Returns:
(CourseDiscussionConfigurationData): structured discussion configuration data.
"""
log.info(f"Updating discussion settings for course: {course_key}")
store = modulestore()
discussions_config = DiscussionsConfiguration.get(course_key)
supports_in_context = discussions_config.supports_in_context_discussions()
provider_type = discussions_config.provider_type
with store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key):
course = store.get_course(course_key)
enable_in_context = discussions_config.enable_in_context
provider_config = discussions_config.plugin_configuration
unit_level_visibility = discussions_config.unit_level_visibility
enable_graded_units = discussions_config.enable_graded_units
contexts = []
if supports_in_context:
sorted_topics = sorted(
course.discussion_topics.items(),
key=lambda item: item[1].get("sort_key", item[0]),
)
contexts = []
for idx, (topic_name, topic_config) in enumerate(sorted_topics):
if topic_config.get('id', None):
context = DiscussionTopicContext(
title=topic_name,
external_id=topic_config.get('id', None),
ordering=idx
)
contexts.append(context)
if enable_in_context:
contexts.extend(list(get_discussable_units(course, enable_graded_units)))
config_data = CourseDiscussionConfigurationData(
course_key=course_key,
enable_in_context=enable_in_context,
enable_graded_units=enable_graded_units,
unit_level_visibility=unit_level_visibility,
provider_type=provider_type,
plugin_configuration=provider_config,
contexts=contexts,
)
return config_data
def get_discussable_units(course, enable_graded_units):
"""
Get all the units in the course that are discussable.
"""
idx = 99
store = modulestore()
for section in get_sections(course):
for subsection in get_subsections(section):
with store.bulk_operations(course.id, emit_signals=False):
for unit in get_units(subsection):
idx += 1
if not is_discussable_unit(unit, store, enable_graded_units, subsection):
unit.discussion_enabled = False
store.update_item(unit, unit.published_by, emit_signals=False)
continue
yield DiscussionTopicContext(
usage_key=unit.location,
title=unit.display_name,
group_id=None,
ordering=idx,
context={
"section": section.display_name,
"subsection": subsection.display_name,
"unit": unit.display_name,
},
)
def get_sections(course):
"""
Get sections for given course
"""
for section in course.get_children():
if section.location.block_type == "chapter":
yield section
def get_subsections(section):
"""
Get subsections for given section
"""
for subsection in section.get_children():
if subsection.location.block_type == "sequential":
yield subsection
def get_units(subsection):
"""
Get units for given subsection
"""
for unit in subsection.get_children():
if unit.location.block_type == 'vertical':
yield unit
def is_discussable_unit(unit, store, enable_graded_units, subsection):
"""
Check if unit should have discussion's topic
"""
if not store.has_published_version(unit):
return False
if not getattr(unit, "discussion_enabled", False):
return False
if subsection.graded and not enable_graded_units:
return False
if subsection.is_practice_exam or subsection.is_proctored_enabled or subsection.is_time_limited:
return False
return True
def update_unit_discussion_state_from_discussion_blocks(course_key: CourseKey, user_id: int, force=False) -> None:
"""
Migrate existing courses to the new mechanism for linking discussion to units.
This will iterate over an existing course's discussion xblocks and mark the units
they are in as discussable.
Args:
course_key (CourseKey): CourseKey for course.
user_id (int): User id for the user performing this operation.
force (bool): Force migration of data even if not using legacy provider
"""
store = modulestore()
course = store.get_course(course_key)
provider = course.discussions_settings.get('provider', None)
# Only migrate to the new discussion provider if the current provider is the legacy provider.
if provider is not None and provider != Provider.LEGACY and not force:
return
log.info(f"Migrating legacy discussion config for {course_key}")
with store.bulk_operations(course_key):
discussion_blocks = get_accessible_discussion_xblocks_by_course_id(course_key, include_all=True)
discussible_units = {
discussion_block.parent
for discussion_block in discussion_blocks
if discussion_block.parent.block_type == 'vertical'
}
log.info(f"Found {len(discussible_units)} discussible unit(s) in {course_key}")
verticals = store.get_items(course_key, qualifiers={'block_type': 'vertical'})
graded_subsections = {
block.location
for block in store.get_items(
course_key,
qualifies={'block_type': 'sequential'},
settings={'graded': True}
)
}
subsections_with_discussions = set()
for vertical in verticals:
if vertical.location in discussible_units:
vertical.discussion_enabled = True
subsections_with_discussions.add(vertical.parent)
else:
vertical.discussion_enabled = False
store.update_item(vertical, user_id)
# If there are any graded subsections that have discussion units,
# then enable discussions for graded subsections for the course
enable_graded_subsections = bool(graded_subsections & subsections_with_discussions)
# If the new discussions experience is enabled globally,
# then also set up the new provider for the course.
if ENABLE_NEW_STRUCTURE_DISCUSSIONS.is_enabled():
log.info(f"New structure is enabled, also updating {course_key} to use new provider")
course = store.get_course(course_key)
provider = Provider.OPEN_EDX
course.discussions_settings['provider'] = provider
course.discussions_settings['enable_graded_units'] = enable_graded_subsections
course.discussions_settings['unit_level_visibility'] = True
store.update_item(course, user_id)
discussion_config = DiscussionsConfiguration.get(course_key)
discussion_config.provider_type = provider
discussion_config.enable_graded_units = enable_graded_subsections
discussion_config.unit_level_visibility = True
discussion_config.save()