From c0960cd1c4825b84ac10a56588e0d25fbe211cab Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Wed, 19 Oct 2022 12:15:07 +0000 Subject: [PATCH 01/16] docs: Document the discussions app (#31045) This change documents some of the workings of the discussions app for reference. --- .../core/djangoapps/discussions/README.rst | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 openedx/core/djangoapps/discussions/README.rst diff --git a/openedx/core/djangoapps/discussions/README.rst b/openedx/core/djangoapps/discussions/README.rst new file mode 100644 index 000000000000..00bb8a2e78de --- /dev/null +++ b/openedx/core/djangoapps/discussions/README.rst @@ -0,0 +1,66 @@ +Discussions +=========== + +This Discussions app is responsible for providing support for configuring +discussion tools in the Open edX platform. This includes the in-built forum +tool that uses the `cs_comments_service`, but also other LTI-based tools. + + +Technical Overview +------------------ + +This app adds support for alternative Discussion providers, where a provider is +initially an LTI app that can be embedded in the discussion tab. + +A list of supported providers is included configured in this app, with the +features each app supports, its support level, and other details that are +returned by the API and drive the UI for selecting and comparing providers in +the course authoring MFE. + +Each context (course) has an associated `DiscussionConfiguration` object that +specifies the provider to use for that course, and includes other configuration +information for the course. For LTI-based providers, it's also linked to an +`LtiConfiguration` entry. + +The `plugin_configuration` field on this model is to store +provider/plugin-specific configuration that won't make sense for other plugins. + +Different parts of the course can be linked to different discussion topics. The +standard use case here to have a topic for each Unit in the course. The +`DiscussionTopicLink` model handles this association, and links a particular +usage key in a particular course to a topic. + +The new Discussion API is driven entirely by these `DiscussionTopicLinks` in +the database, so it's possible to have a third-party plugin that changes how +and where these topic links are created. + +When a course is published, the `listen_for_course_publish` signal handler is +called, and this signal in turn calls the +update_discussions_settings_from_course_task` in the background. + +That task goes through the module store and builds a +`CourseDiscussionConfigurationData` object that has all the relevant course +discussion configuration information such as the course key, the provider type, +whether in-context discussions are enabled, whether graded units are enabled, +when unit level visibility is enabled. Other plugin configuration and a list +of discussion contexts for which discussions are enabled. Each discussion +context has a usage key, a title (the units name) an external id +(the cs_comments_service id), it's ordering in the course, and additional +context. It then sends its own signal that has the discussion configuration +object attached. + +Finally, the handler for this discussion change signal, takes the information +from the discussion change signal and compares it to the topics in the +database, and does the following; + +- If it sees discussions contexts (units) without topics it creates new topics + link entries for the new units. This will happen if you create a new unit, + or enable discussions for a unit that previously had them disabled. + +- If it sees discussion topics without contexts it disables/archives them. + This could happen when a unit is deleted or if a unit that previously had + discussions enabled, now has discussions disabled. + +- If it sees any other change, i.e. unit name change etc. it applies that + change to the database as well. + From 0ceba001acf504347232477e2b409a34af2b1e40 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Tue, 1 Nov 2022 09:31:27 +0000 Subject: [PATCH 02/16] fix: Change approach to processing course topics (#31200) Course topics are now created by traversing the entire course structure from top to bottom instead of starting at the sequential level and then moving up or down as needed. This also introduces a lot of debug logs to pontetially find the reason why under some circumstances new units don't get processed and end up without a discussions topic. --- .../core/djangoapps/discussions/README.rst | 2 +- .../core/djangoapps/discussions/handlers.py | 7 ++- openedx/core/djangoapps/discussions/tasks.py | 63 +++++++++++-------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/openedx/core/djangoapps/discussions/README.rst b/openedx/core/djangoapps/discussions/README.rst index 00bb8a2e78de..5c1127d324cc 100644 --- a/openedx/core/djangoapps/discussions/README.rst +++ b/openedx/core/djangoapps/discussions/README.rst @@ -51,7 +51,7 @@ object attached. Finally, the handler for this discussion change signal, takes the information from the discussion change signal and compares it to the topics in the -database, and does the following; +database, and does the following: - If it sees discussions contexts (units) without topics it creates new topics link entries for the new units. This will happen if you create a new unit, diff --git a/openedx/core/djangoapps/discussions/handlers.py b/openedx/core/djangoapps/discussions/handlers.py index 8787da82cee2..c894a6a53b8c 100644 --- a/openedx/core/djangoapps/discussions/handlers.py +++ b/openedx/core/djangoapps/discussions/handlers.py @@ -61,14 +61,16 @@ def update_course_discussion_config(configuration: CourseDiscussionConfiguration lookup_key = topic_link.usage_key or topic_link.external_id topic_context = new_topic_map.pop(lookup_key, None) if topic_context is None: + log.info(f"[DEBUG INF-291] Unit was deleted or discussion disabled: {lookup_key}") topic_link.enabled_in_context = False try: # If the section/subsection/unit a topic is in is deleted, add that context to title. topic_link.title = "{section}|{subsection}|{unit}".format(**topic_link.context) except KeyError: - # It's possible the context is if the topic link was created before the context field was added. + # It's possible the context is empty if the link was created before the context field was added. pass else: + log.info(f"[DEBUG INF-291] Unit topic already exists, will be updated: {lookup_key}") topic_link.enabled_in_context = True topic_link.ordering = topic_context.ordering topic_link.title = topic_context.title @@ -78,6 +80,9 @@ def update_course_discussion_config(configuration: CourseDiscussionConfiguration topic_link.save() log.info(f"Creating new discussion topic links for {course_key}") + log.info(f"[DEBUG INF-291] Discovered new units with keys: {[str(key) for key in new_topic_map.keys()]}") + log.info(f"[DEBUG INF-291] New unit names: {[topic.title for topic in new_topic_map.values()]}") + DiscussionTopicLink.objects.bulk_create([ DiscussionTopicLink( context_key=course_key, diff --git a/openedx/core/djangoapps/discussions/tasks.py b/openedx/core/djangoapps/discussions/tasks.py index 9cbfac57d599..0113c63d831a 100644 --- a/openedx/core/djangoapps/discussions/tasks.py +++ b/openedx/core/djangoapps/discussions/tasks.py @@ -51,37 +51,48 @@ def update_discussions_settings_from_course(course_key: CourseKey) -> CourseDisc provider_type = discussions_config.provider_type def iter_discussable_units(): - subsections = store.get_items(course_key, qualifiers={"category": "sequential"}) # Start at 99 so that the initial increment starts it at 100. # This leaves the first 100 slots for the course wide topics, which is only a concern if there are more # than that many. idx = 99 - for subsection in subsections: - section = store.get_item(subsection.parent) - for unit in subsection.get_children(): - # Increment index even for skipped units so that the index is more stable and won't change - # if settings change, only if a unit is added or removed. - idx += 1 - # If unit-level visibility is enabled and the unit doesn't have discussion enabled, skip it. - if unit_level_visibility and not getattr(unit, "discussion_enabled", False): + log.info(f"[DEBUG INF-291] Unit-level visibility enabled: {unit_level_visibility}") + for section in course.get_children(): + if section.location.block_type != "chapter": + continue + for subsection in section.get_children(): + if subsection.location.block_type != "sequential": continue - # If the unit is in a graded section and graded sections aren't enabled skip it. - if subsection.graded and not enable_graded_units: - continue - # If the unit is an exam, skip it. - if subsection.is_practice_exam or subsection.is_proctored_enabled or subsection.is_time_limited: - 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, - }, - ) + for unit in subsection.get_children(): + if unit.location.block_type != 'vertical': + continue + log.info(f"[DEBUG INF-291] Processing unit: {unit.location}") + # Increment index even for skipped units so that the index is more stable and won't change + # if settings change, only if a unit is added or removed. + idx += 1 + # If unit-level visibility is enabled and the unit doesn't have discussion enabled, skip it. + if unit_level_visibility and not getattr(unit, "discussion_enabled", False): + log.info(f"[DEBUG INF-291] Skipping unit because discussion is disbled: {unit.location}") + continue + # If the unit is in a graded section and graded sections aren't enabled skip it. + if subsection.graded and not enable_graded_units: + log.info(f"[DEBUG INF-291] Skipping unit because it's in a graded subsection: {unit.location}") + continue + # If the unit is an exam, skip it. + if subsection.is_practice_exam or subsection.is_proctored_enabled or subsection.is_time_limited: + log.info(f"[DEBUG INF-291] Skipping unit because it's in an exam: {unit.location}") + continue + log.info(f"[DEBUG INF-291] Topic will be created for unit: {unit.location}") + 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, + }, + ) with store.branch_setting(ModuleStoreEnum.Branch.published_only, course_key): course = store.get_course(course_key) From a4fa75f1079124ce14000e6a4c4064d10802bb1d Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Fri, 18 Nov 2022 11:49:29 +0000 Subject: [PATCH 03/16] fix: delay creation of course topics after course publish (#31307) When running in a sharded MongoDB setup it's possible that querying the modulestore right after the course publish signal will not return the latest data. This commit adds a delay similar to the one used in other places in the codebase for a similar reason. --- cms/djangoapps/contentstore/signals/handlers.py | 5 ++++- openedx/core/djangoapps/discussions/handlers.py | 5 ----- openedx/core/djangoapps/discussions/tasks.py | 6 ------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 44bfdea32908..76711eaa4ddc 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -145,7 +145,10 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= if CoursewareSearchIndexer.indexing_is_enabled() and CourseAboutSearchIndexer.indexing_is_enabled(): update_search_index.delay(course_key_str, datetime.now(UTC).isoformat()) - update_discussions_settings_from_course_task.delay(course_key_str) + update_discussions_settings_from_course_task.apply_async( + args=[course_key_str], + countdown=settings.DISCUSSION_SETTINGS['COURSE_PUBLISH_TASK_DELAY'], + ) # Send to a signal for catalog info changes as well, but only once we know the transaction is committed. transaction.on_commit(lambda: emit_catalog_info_changed_signal(course_key)) diff --git a/openedx/core/djangoapps/discussions/handlers.py b/openedx/core/djangoapps/discussions/handlers.py index c894a6a53b8c..f43aaf4b09eb 100644 --- a/openedx/core/djangoapps/discussions/handlers.py +++ b/openedx/core/djangoapps/discussions/handlers.py @@ -61,7 +61,6 @@ def update_course_discussion_config(configuration: CourseDiscussionConfiguration lookup_key = topic_link.usage_key or topic_link.external_id topic_context = new_topic_map.pop(lookup_key, None) if topic_context is None: - log.info(f"[DEBUG INF-291] Unit was deleted or discussion disabled: {lookup_key}") topic_link.enabled_in_context = False try: # If the section/subsection/unit a topic is in is deleted, add that context to title. @@ -70,7 +69,6 @@ def update_course_discussion_config(configuration: CourseDiscussionConfiguration # It's possible the context is empty if the link was created before the context field was added. pass else: - log.info(f"[DEBUG INF-291] Unit topic already exists, will be updated: {lookup_key}") topic_link.enabled_in_context = True topic_link.ordering = topic_context.ordering topic_link.title = topic_context.title @@ -80,9 +78,6 @@ def update_course_discussion_config(configuration: CourseDiscussionConfiguration topic_link.save() log.info(f"Creating new discussion topic links for {course_key}") - log.info(f"[DEBUG INF-291] Discovered new units with keys: {[str(key) for key in new_topic_map.keys()]}") - log.info(f"[DEBUG INF-291] New unit names: {[topic.title for topic in new_topic_map.values()]}") - DiscussionTopicLink.objects.bulk_create([ DiscussionTopicLink( context_key=course_key, diff --git a/openedx/core/djangoapps/discussions/tasks.py b/openedx/core/djangoapps/discussions/tasks.py index 0113c63d831a..1985da1c9aab 100644 --- a/openedx/core/djangoapps/discussions/tasks.py +++ b/openedx/core/djangoapps/discussions/tasks.py @@ -55,7 +55,6 @@ def iter_discussable_units(): # This leaves the first 100 slots for the course wide topics, which is only a concern if there are more # than that many. idx = 99 - log.info(f"[DEBUG INF-291] Unit-level visibility enabled: {unit_level_visibility}") for section in course.get_children(): if section.location.block_type != "chapter": continue @@ -65,23 +64,18 @@ def iter_discussable_units(): for unit in subsection.get_children(): if unit.location.block_type != 'vertical': continue - log.info(f"[DEBUG INF-291] Processing unit: {unit.location}") # Increment index even for skipped units so that the index is more stable and won't change # if settings change, only if a unit is added or removed. idx += 1 # If unit-level visibility is enabled and the unit doesn't have discussion enabled, skip it. if unit_level_visibility and not getattr(unit, "discussion_enabled", False): - log.info(f"[DEBUG INF-291] Skipping unit because discussion is disbled: {unit.location}") continue # If the unit is in a graded section and graded sections aren't enabled skip it. if subsection.graded and not enable_graded_units: - log.info(f"[DEBUG INF-291] Skipping unit because it's in a graded subsection: {unit.location}") continue # If the unit is an exam, skip it. if subsection.is_practice_exam or subsection.is_proctored_enabled or subsection.is_time_limited: - log.info(f"[DEBUG INF-291] Skipping unit because it's in an exam: {unit.location}") continue - log.info(f"[DEBUG INF-291] Topic will be created for unit: {unit.location}") yield DiscussionTopicContext( usage_key=unit.location, title=unit.display_name, From 85ad6ed7448615e99904ee33ec07470cb33e613b Mon Sep 17 00:00:00 2001 From: Muhammad Adeel Tajamul <77053848+muhammadadeeltajamul@users.noreply.github.com> Date: Wed, 12 Oct 2022 12:00:32 +0500 Subject: [PATCH 04/16] fix: removed switch experience bar from legacy (#31138) Co-authored-by: adeel.tajamul --- .../discussion/discussion_board_fragment.html | 1 - .../_switch_experience_fragment.html | 74 ------------------- 2 files changed, 75 deletions(-) delete mode 100644 lms/templates/discussion/_switch_experience_fragment.html diff --git a/lms/djangoapps/discussion/templates/discussion/discussion_board_fragment.html b/lms/djangoapps/discussion/templates/discussion/discussion_board_fragment.html index a99739f07c48..be7bce1733e9 100644 --- a/lms/djangoapps/discussion/templates/discussion/discussion_board_fragment.html +++ b/lms/djangoapps/discussion/templates/discussion/discussion_board_fragment.html @@ -23,7 +23,6 @@ data-sort-preference="${sort_preference}" data-flag-moderator="${json.dumps(flag_moderator)}" data-user-group-id="${user_group_id}"> - <%include file="_switch_experience_fragment.html" />