Repository: openedx/edx-platform
Pull request #36532: Allow Default Advanced Modules Configuration
Author: login: andrii-hantkovskyi
Labels: name: open-source-contribution
description: PR author is not from Axim or 2U
Created: 2025-04-16T16:58:51Z | Merged: 2025-04-23T13:16:51Z

This PR introduces functionality to configure default advanced modules that are automatically added to course-specific modules. The implementation merges course-specific advanced modules with system defaults defined in settings, ensuring no duplicates.


Diff:
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 50aaaaded7a6..ded3b9d2f462 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -82,6 +82,16 @@
     "edit-title-button", "edit-upstream-alert",
 ]
 
+DEFAULT_ADVANCED_MODULES = [
+    'google-calendar',
+    'google-document',
+    'lti_consumer',
+    'poll',
+    'split_test',
+    'survey',
+    'word_cloud',
+]
+
 
 def _advanced_component_types(show_unsupported):
     """
@@ -445,7 +455,7 @@ def create_support_legend_dict():
     # 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
+    course_advanced_keys = list(dict.fromkeys(courselike.advanced_modules + DEFAULT_ADVANCED_MODULES))
     advanced_component_templates = {
         "type": "advanced",
         "templates": [],
diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py
index 93e382fb7fba..d41856c5cd72 100644
--- a/cms/djangoapps/contentstore/views/tests/test_block.py
+++ b/cms/djangoapps/contentstore/views/tests/test_block.py
@@ -75,7 +75,7 @@
 from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
 from openedx.core.djangoapps.content_tagging import api as tagging_api
 
-from ..component import component_handler, get_component_templates
+from ..component import component_handler, DEFAULT_ADVANCED_MODULES, get_component_templates
 from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
     ALWAYS,
     VisibilityState,
@@ -2903,6 +2903,16 @@ def setUp(self):
 
         self.templates = get_component_templates(self.course)
 
+        self.default_advanced_modules_titles = [
+            "Google Calendar",
+            "Google Document",
+            "LTI Consumer",
+            "Poll",
+            "Content Experiment",
+            "Survey",
+            "Word cloud",
+        ]
+
     def get_templates_of_type(self, template_type):
         """
         Returns the templates for the specified type, or None if none is found.
@@ -2956,7 +2966,11 @@ def test_basic_components(self):
         self.assertGreater(len(self.get_templates_of_type("library")), 0)
         self.assertGreater(len(self.get_templates_of_type("html")), 0)
         self.assertGreater(len(self.get_templates_of_type("problem")), 0)
-        self.assertIsNone(self.get_templates_of_type("advanced"))
+
+        # Check for default advanced modules
+        advanced_templates = self.get_templates_of_type("advanced")
+        advanced_module_keys = [t['category'] for t in advanced_templates]
+        self.assertCountEqual(advanced_module_keys, DEFAULT_ADVANCED_MODULES)
 
         # Now fully disable video through XBlockConfiguration
         XBlockConfiguration.objects.create(name="video", enabled=False)
@@ -3004,29 +3018,38 @@ def test_advanced_components(self):
         """
         Test the handling of advanced component templates.
         """
-        self.course.advanced_modules.append("word_cloud")
+        self.course.advanced_modules.append("done")
+        EXPECTED_ADVANCED_MODULES_LENGTH = len(DEFAULT_ADVANCED_MODULES) + 1
         self.templates = get_component_templates(self.course)
         advanced_templates = self.get_templates_of_type("advanced")
-        self.assertEqual(len(advanced_templates), 1)
-        world_cloud_template = advanced_templates[0]
-        self.assertEqual(world_cloud_template.get("category"), "word_cloud")
-        self.assertEqual(world_cloud_template.get("display_name"), "Word cloud")
-        self.assertIsNone(world_cloud_template.get("boilerplate_name", None))
+        self.assertEqual(len(advanced_templates), EXPECTED_ADVANCED_MODULES_LENGTH)
+        done_template = advanced_templates[0]
+        self.assertEqual(done_template.get("category"), "done")
+        self.assertEqual(done_template.get("display_name"), "Completion")
+        self.assertIsNone(done_template.get("boilerplate_name", None))
 
-        # Verify that non-advanced components are not added twice
+        # Verify that components are not added twice
         self.course.advanced_modules.append("video")
         self.course.advanced_modules.append("drag-and-drop-v2")
+        # Already defined advanced modules
+        self.course.advanced_modules.append("poll")
+        self.course.advanced_modules.append("google-document")
+        self.course.advanced_modules.append("survey")
+
         self.templates = get_component_templates(self.course)
         advanced_templates = self.get_templates_of_type("advanced")
-        self.assertEqual(len(advanced_templates), 1)
+        self.assertEqual(len(advanced_templates), EXPECTED_ADVANCED_MODULES_LENGTH)
         only_template = advanced_templates[0]
         self.assertNotEqual(only_template.get("category"), "video")
         self.assertNotEqual(only_template.get("category"), "drag-and-drop-v2")
+        self.assertNotEqual(only_template.get("category"), "poll")
+        self.assertNotEqual(only_template.get("category"), "google-document")
+        self.assertNotEqual(only_template.get("category"), "survey")
 
-        # Now fully disable word_cloud through XBlockConfiguration
-        XBlockConfiguration.objects.create(name="word_cloud", enabled=False)
+        # Now fully disable done through XBlockConfiguration
+        XBlockConfiguration.objects.create(name="done", enabled=False)
         self.templates = get_component_templates(self.course)
-        self.assertIsNone(self.get_templates_of_type("advanced"))
+        self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced"))))
 
     def test_advanced_problems(self):
         """
@@ -3087,8 +3110,9 @@ def test_create_support_level_flag_off(self):
         XBlockConfiguration) if XBlockStudioConfigurationFlag is False.
         """
         XBlockStudioConfigurationFlag.objects.create(enabled=False)
-        self.course.advanced_modules.extend(["annotatable", "survey"])
-        self._verify_advanced_xblocks(["Annotation", "Survey"], [True, True])
+        self.course.advanced_modules.extend(["annotatable", "done"])
+        expected_xblocks = ["Annotation", "Completion"] + self.default_advanced_modules_titles
+        self._verify_advanced_xblocks(expected_xblocks, [True] * len(expected_xblocks))
 
     def test_xblock_masquerading_as_problem(self):
         """


Reviews:
user: login: feanil
body: One small concern about the brittleness of the tests but nothing blocking.  I'd love to see it improved but this can merge without that if need be.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T18:05:34Z
user: login: andrii-hantkovskyi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T08:59:29Z
user: login: andrii-hantkovskyi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T08:59:31Z
user: login: e0d
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:24:05Z
user: login: kdmccormick
body: I'm concerned about adding another layer to xblock configuration. It's already complicated.

What's the product requirement for Teak? Is it that these 7 specific block types are enabled by default? Or is it that DEFAULT_ADVANCED_MODULES is configurable?

If it's just the former, then we do not need a new Django setting. We can just hard-code this list. That way, we're not setting an expectation for operators that this new configuration method is stable.

If we really must have DEFAULT_ADVANCED_MODULES for teak, then could we add a warning to the setting definition which clarifies that this setting is unstable and might change by Ulmo?
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-22T17:11:01Z
user: login: feanil
body: Ok, I think we've decided that this should not be a setting exposed to operators.  Essentially this will be a hardcoded list for now and we can expose something configurable to operators in the future.

Note: We should make this clear in the release notes as well @jmakowski1123 that everyone will see these new blocks as available.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:57:49Z
user: login: andrii-hantkovskyi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T11:34:16Z
user: login: feanil
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:14:27Z

Review comments:
diff_hunk: @@ -2903,6 +2903,16 @@ def setUp(self):
 
         self.templates = get_component_templates(self.course)
 
+        self.default_advanced_modules_titles = [
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: feanil
body: Hardcoding this means the test is not resilient to the list of advanced modules changing.
created_at: 2025-04-18T18:01:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2956,7 +2966,12 @@ def test_basic_components(self):
         self.assertGreater(len(self.get_templates_of_type("library")), 0)
         self.assertGreater(len(self.get_templates_of_type("html")), 0)
         self.assertGreater(len(self.get_templates_of_type("problem")), 0)
-        self.assertIsNone(self.get_templates_of_type("advanced"))
+
+        # Check for default advanced modules
+        advanced_templates = self.get_templates_of_type("advanced")
+        self.assertEqual(len(advanced_templates), len(settings.DEFAULT_ADVANCED_MODULES))
+        advanced_module_titles = [t['display_name'] for t in advanced_templates]
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: feanil
body: Rather than capturing the display name is it possible to get the keys for the advanced modules and then we could compare those keys to the list in settings?
created_at: 2025-04-18T18:02:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2956,7 +2966,12 @@ def test_basic_components(self):
         self.assertGreater(len(self.get_templates_of_type("library")), 0)
         self.assertGreater(len(self.get_templates_of_type("html")), 0)
         self.assertGreater(len(self.get_templates_of_type("problem")), 0)
-        self.assertIsNone(self.get_templates_of_type("advanced"))
+
+        # Check for default advanced modules
+        advanced_templates = self.get_templates_of_type("advanced")
+        self.assertEqual(len(advanced_templates), len(settings.DEFAULT_ADVANCED_MODULES))
+        advanced_module_titles = [t['display_name'] for t in advanced_templates]
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: andrii-hantkovskyi
body: yes, updated
created_at: 2025-04-22T08:59:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2903,6 +2903,16 @@ def setUp(self):
 
         self.templates = get_component_templates(self.course)
 
+        self.default_advanced_modules_titles = [
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: andrii-hantkovskyi
body: I added `DEFAULT_ADVANCED_MODULES` to `cms/envs/test.py`, so changing them in other settings shouldn't break tests
created_at: 2025-04-22T08:59:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2956,7 +2966,12 @@ def test_basic_components(self):
         self.assertGreater(len(self.get_templates_of_type("library")), 0)
         self.assertGreater(len(self.get_templates_of_type("html")), 0)
         self.assertGreater(len(self.get_templates_of_type("problem")), 0)
-        self.assertIsNone(self.get_templates_of_type("advanced"))
+
+        # Check for default advanced modules
+        advanced_templates = self.get_templates_of_type("advanced")
+        self.assertEqual(len(advanced_templates), len(settings.DEFAULT_ADVANCED_MODULES))
+        advanced_module_titles = [t['display_name'] for t in advanced_templates]
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: e0d
body: @feanil @andrii-hantkovskyi cannot merge directly could you do that?
created_at: 2025-04-22T15:24:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2938,3 +2938,16 @@ def _should_send_learning_badge_events(settings):
     'survey',
     'word_cloud',
 ]
+
+# .. setting_name: DEFAULT_ADVANCED_MODULES
+# .. setting_default: ['google-calendar', 'google-document', 'lti_consumer', 'poll', 'split_test', 'survey', 'word_cloud']
+# .. setting_description: List of advanced modules that are enabled by default
+DEFAULT_ADVANCED_MODULES = [
path: cms/envs/common.py
user: login: feanil
body: After speaking w/Kyle and Ed, we've come to the conclusion that this should not be a setting in `common.py`  That implies that it can be overridden by the end-user which creates a new interface we don't want.

Instead of having this list here, move it to `cms/djangoapps/contentstore/views/component.py` 

In the future we can figure out what configurable modifiable version of settings we want to expose to operators but for now just make it so this is visible on all deployments by adding it into the software instead in the settings file here.
created_at: 2025-04-22T17:56:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2938,3 +2938,16 @@ def _should_send_learning_badge_events(settings):
     'survey',
     'word_cloud',
 ]
+
+# .. setting_name: DEFAULT_ADVANCED_MODULES
+# .. setting_default: ['google-calendar', 'google-document', 'lti_consumer', 'poll', 'split_test', 'survey', 'word_cloud']
+# .. setting_description: List of advanced modules that are enabled by default
+DEFAULT_ADVANCED_MODULES = [
path: cms/envs/common.py
user: login: andrii-hantkovskyi
body: updated
created_at: 2025-04-23T11:34:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2903,6 +2903,16 @@ def setUp(self):
 
         self.templates = get_component_templates(self.course)
 
+        self.default_advanced_modules_titles = [
path: cms/djangoapps/contentstore/views/tests/test_block.py
user: login: feanil
body: I think that's fine for now, if the tests become brittle we can improve them in the future.  This doesn't have to block merging this change.
created_at: 2025-04-23T13:14:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: openedx-webhooks
created_at: 2025-04-16T16:58:56Z
body: Thanks for the pull request, @andrii-hantkovskyi!


This repository is currently maintained by `@openedx/wg-maintenance-edx-platform`.


Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

<details><summary>:radio_button: Get product approval</summary>

If you haven't already, [check this list](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review#Does-my-contribution-require-Product-Review%3F) to see if your contribution needs to go through the product review process.

- If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the [Product Working Group](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3449028609/Product+Working+Group).
    - This process (including the steps you'll need to take) is documented [here](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review#Product-Review-Process).
- If it doesn't, simply proceed with the next step.
</details>

<details><summary>:radio_button: Provide context</summary>

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

- Dependencies
  > This PR must be merged before / after / at the same time as ...
- Blockers
  > This PR is waiting for OEP-1234 to be accepted.
- Timeline information
  > This PR must be merged by XX date **because** ...
- Partner information
  > This is for a course on edx.org.
- Supporting documentation
- Relevant [Open edX discussion forum](https://discuss.openedx.org/) threads
</details>





<details><summary>:radio_button: Get a green build</summary>

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.
</details><details>


---

<details><summary>Where can I find more information?</summary>

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

- [Overview of Review Process for Community Contributions](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/FAQ-about-pull-requests.html)
- [Pull Request Status Guide](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/pull-request-statuses.html)
- [Making changes to your pull request](https://docs.openedx.org/en/latest/documentors/how-tos/make_changes_to_your_pull_request.html)
</details>

<details><summary>When can I expect my changes to be merged?</summary>

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

- The size and impact of the changes that it introduces
- The need for product review
- Maintenance status of the parent repository

:bulb: *As a result it may take up to several weeks or months to complete a review and merge your PR.*
</details>
<!-- comment:external_pr -->
<!-- data: eyJkcmFmdCI6IGZhbHNlfQ== -->

author_association: NONE
user: login: e0d
created_at: 2025-04-18T14:37:26Z
body: @andrii-hantkovskyi I've added you to the openedx-triage team as you are covered under an entity CLA.  This will mean that tests will run automatically when you submit a pull request in the future.  Please look for an invitation in your email and accept it. 
author_association: CONTRIBUTOR
user: login: marcotuts
created_at: 2025-04-22T15:45:43Z
body: Hello! 

We discussed this a bit today in the project check-in, so I wanted to share some thinking on the future direction for this configuration. 

In the short term, it may make sense to keep the current hard coded list of modules reflected in this PR, though @feanil  did call out above the concern with this approach. In fact that comment is what made me realize it might be helpful to share some other platform configuration tools that might be a better eventual / future direction for this Xblock configuration. 

In Studio's django configuration there is an xblocks table, this table has the xblock name, metadata for whether it is provisionally supported, fully supported, or unsupported. It also has a flag to determine whether the xblock is active or not. 

If we were to extend / improve this area of the platform configuration it would keep the "advanced " vs "common" configuration out of the code and in django where it is more easily modifiable. 

To power initiative 1 / the earliest version of the unit page showing advanced blocks, we would need this table to know whether the xblock is considered: Common or Advanced, and Default on / Default Off.  (Generally all common blocks would be default on, some advanced xblocks would be default on and some default off.) 

This would also mean that any instance that has enabled custom xblocks would have a single place to do this configuration for determining the common / advanced / default on / default off status and thus more easily enable these xblocks to appear on unit pages. This approach also supports future Initiatives outlined in the product proposal: https://openedx.atlassian.net/wiki/spaces/COMM/pages/4325900293/Visually+Configure+Course+Components

While not all of these initiatives are in development or funded yet, there are other metadata fields we'd love to eventually also include in a centralized xblock configuration to aid in expanded use of the full list of community xblocks. For reference, here are some of those metadata fields. These don't need to be considered in the short term scope but are still worth highlighting as they help augment the case for why we benefit from this centralized django xblock configuration as we move toward a 'directory' model in the future.

XBlock Title - educator facing title of the xblock (eg: Google Calendar), separate from its configuration slug (ex: google_calendar)
XBlock Short Description - shorter description for use on xblock cards in UI
XBlock Long Description - longer description for use in xblock details sidebar in UI 
XBlock Documentation references - a list of N number of links (their url destination and educator facing text summary) to allow xblocks to link to relevant documentation or external configuration  details. 

The visual below from the confluence page shows how this metadata would be used. 

![image](https://github.com/user-attachments/assets/4a110fd0-1c46-4602-aed6-4446cfe3b7d4)

Finally, in the future we would love to be able to have organization or site level filtering / overrides to this configuration as we have done for the LTI configurations in the platform (a separate django table). For example, a given organization might not want to show a specific discussion LTI integration for any of their courses, even if other organizations in a multi-org instance are ok showing that provider. We build that kind of filtering into the LTI configuration but the xblock configuration doesn't have this today even if it would be valuable to eventually incorporate into this feature area. 

I hope this helps summarize short term vs medium term needs for this xblock configuration. thanks! 
author_association: CONTRIBUTOR
user: login: e0d
created_at: 2025-04-22T15:51:34Z
body: We were discussing this in the check in and @marcotuts remembered that there were a couple efforts to do similar things that were manageable via Django admin.  This may be the way we want to take the management of configuration ultimately.

There current fields for managing XBlocks are pretty minimal, but this could be extended:
```
mysql> desc xblock_config_courseeditltifieldsenabledflag;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | int          | NO   | PRI | NULL    | auto_increment |
| change_date   | datetime(6)  | NO   |     | NULL    |                |
| enabled       | tinyint(1)   | NO   |     | NULL    |                |
| course_id     | varchar(255) | NO   | MUL | NULL    |                |
| changed_by_id | int          | YES  | MUL | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
```
![image](https://github.com/user-attachments/assets/786fb12c-645a-4678-8d10-254446eaaf3a)

![image](https://github.com/user-attachments/assets/8cf7e550-2cda-4ed8-a148-853990cd01f3)
author_association: CONTRIBUTOR
user: login: kdmccormick
created_at: 2025-04-22T17:23:42Z
body: I realize that @feanil and I have made opposite requests. We have a meeting in moment; we'll talk and come up with consistent guidance for this PR.
author_association: MEMBER
user: login: jmakowski1123
created_at: 2025-04-22T18:06:49Z
body: > Ok, I think we've decided that this should not be a setting exposed to operators. Essentially this will be a hardcoded list for now and we can expose something configurable to operators in the future.
> 
> Note: We should make this clear in the release notes as well @jmakowski1123 that everyone will see these new blocks as available.

Yes, that is one of the intended outcomes, that everyone sees the blocks available in the Advanced tile. 
author_association: NONE
user: login: edx-pipeline-bot
created_at: 2025-04-23T14:30:55Z
body: **2U Release Notice**:  This PR has been deployed to the edX staging environment in preparation for a release to production.
author_association: CONTRIBUTOR
user: login: edx-pipeline-bot
created_at: 2025-04-23T14:48:35Z
body: **2U Release Notice**:  This PR has been deployed to the edX production environment. 
author_association: CONTRIBUTOR