diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index 3e652c899b5..79fc0ac7931 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -40,6 +40,7 @@ from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG from cms.djangoapps.contentstore.toggles import ENABLE_COPY_PASTE_UNITS from cms.djangoapps.models.settings.course_grading import CourseGradingModel +from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig from common.djangoapps.edxmako.services import MakoService from common.djangoapps.static_replace import replace_static_urls from common.djangoapps.student.auth import ( @@ -280,6 +281,7 @@ def modify_xblock(usage_key, request): prereq_min_completion=request_data.get("prereqMinCompletion"), publish=request_data.get("publish"), fields=request_data.get("fields"), + summary_configuration_enabled=request_data.get("summary_configuration_enabled"), ) @@ -354,6 +356,7 @@ def _save_xblock( # lint-amnesty, pylint: disable=too-many-statements prereq_min_completion=None, publish=None, fields=None, + summary_configuration_enabled=None, ): """ Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata. @@ -530,6 +533,12 @@ def _save_xblock( # lint-amnesty, pylint: disable=too-many-statements if publish == "make_public": modulestore().publish(xblock.location, user.id) + # If summary_configuration_enabled is not None, use AIAsideSummary to update it. + if xblock.category == "vertical" and summary_configuration_enabled is not None: + AiAsideSummaryConfig(course.id).set_summary_settings(xblock.location, { + 'enabled': summary_configuration_enabled + }) + # Note that children aren't being returned until we have a use case. return JsonResponse(result, encoder=EdxJSONEncoder) @@ -1049,6 +1058,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements user=None, course=None, is_concise=False, + summary_configuration=None, ): """ Creates the information needed for client-side XBlockInfo. @@ -1098,6 +1108,10 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements should_visit_children = include_child_info and ( course_outline and not is_xblock_unit or not course_outline ) + + if summary_configuration is None: + summary_configuration = AiAsideSummaryConfig(xblock.location.course_key) + if should_visit_children and xblock.has_children: child_info = _create_xblock_child_info( xblock, @@ -1107,6 +1121,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements user=user, course=course, is_concise=is_concise, + summary_configuration=summary_configuration, ) else: child_info = None @@ -1357,6 +1372,9 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements xblock, course=course ) + if is_xblock_unit and summary_configuration.is_enabled(): + xblock_info["summary_configuration_enabled"] = summary_configuration.is_summary_enabled(xblock_info['id']) + return xblock_info @@ -1550,6 +1568,7 @@ def _create_xblock_child_info( user=None, course=None, is_concise=False, + summary_configuration=None, ): """ Returns information about the children of an xblock, as well as about the primary category @@ -1576,6 +1595,7 @@ def _create_xblock_child_info( user=user, course=course, is_concise=is_concise, + summary_configuration=summary_configuration, ) for child in xblock.get_children() ] diff --git a/cms/lib/ai_aside_summary_config.py b/cms/lib/ai_aside_summary_config.py new file mode 100644 index 00000000000..06b9f682244 --- /dev/null +++ b/cms/lib/ai_aside_summary_config.py @@ -0,0 +1,56 @@ +""" +This file contains AiAsideSummaryConfig class that take a `course_key` and return if: + * the waffle flag is enabled in ai_aside + * is the summary is enabled for a given unit_key + * change the settings for a given unit_key +""" + + +class AiAsideSummaryConfig: + """ + Configuration for the AI Aside summary configuration. + """ + + def __init__(self, course_key): + self.course_key = course_key + + def __str__(self): + """ + Return user-friendly string. + """ + return f"AIAside summary configuration for {self.course_key} course" + + def is_enabled(self): + """ + Define if the waffle flag is enabled for the current course_key + """ + try: + from ai_aside.config_api.api import is_summary_config_enabled + return is_summary_config_enabled(self.course_key) + except (ModuleNotFoundError, ImportError): + return False + + def is_summary_enabled(self, unit_key=None): + """ + Define if the summary configuration is enabled in ai_aside + """ + try: + from ai_aside.config_api.api import is_course_settings_present, is_summary_enabled + if not is_course_settings_present(self.course_key): + return None + return is_summary_enabled(self.course_key, unit_key) + except (ModuleNotFoundError, ImportError): + return None + + def set_summary_settings(self, unit_key, settings=None): + """ + Define the settings for a given unit_key in ai_aside + """ + if settings is None: + return None + + try: + from ai_aside.config_api.api import set_unit_settings + return set_unit_settings(self.course_key, unit_key, settings) + except (ModuleNotFoundError, ImportError): + return None diff --git a/cms/lib/test/__init__.py b/cms/lib/test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cms/lib/test/test_ai_aside_summary_config.py b/cms/lib/test/test_ai_aside_summary_config.py new file mode 100644 index 00000000000..b10db89b789 --- /dev/null +++ b/cms/lib/test/test_ai_aside_summary_config.py @@ -0,0 +1,62 @@ +""" +Tests for AiAsideSummaryConfig class. +""" + + +import sys +from unittest import TestCase +from unittest.mock import Mock + +from opaque_keys.edx.keys import CourseKey, UsageKey + +from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig + +ai_aside = Mock() +sys.modules['ai_aside.config_api.api'] = ai_aside + + +class AiAsideSummaryConfigTestCase(TestCase): + """ Tests for AiAsideSummaryConfig. """ + COURSE_KEY = CourseKey.from_string("course-v1:test+Test+AiAsideSummaryConfigTestCase") + UNIT_KEY = UsageKey.from_string("block-v1:test+Test+AiAsideSummaryConfigTestCase+type@vertical+block@vertical_test") + + def test_is_enabled(self): + """ + Check if summary configuration is enabled using the ai_aside lib. + """ + ai_aside_summary_config = AiAsideSummaryConfig(self.COURSE_KEY) + ai_aside.is_summary_config_enabled.return_value = True + self.assertTrue(ai_aside_summary_config.is_enabled()) + + ai_aside.is_summary_config_enabled.return_value = False + self.assertFalse(ai_aside_summary_config.is_enabled()) + + def test_is_summary_enabled(self): + """ + Check the summary configuration value for a particular course and an optional unit using the ai_aside lib. + """ + ai_aside_summary_config = AiAsideSummaryConfig(self.COURSE_KEY) + ai_aside.is_course_settings_present.return_value = True + ai_aside.is_summary_enabled.return_value = True + self.assertTrue(ai_aside_summary_config.is_summary_enabled()) + + ai_aside.is_course_settings_present.return_value = True + ai_aside.is_summary_enabled.return_value = False + self.assertFalse(ai_aside_summary_config.is_summary_enabled(self.UNIT_KEY)) + + ai_aside.is_course_settings_present.return_value = False + ai_aside.is_summary_enabled.return_value = True + self.assertIsNone(ai_aside_summary_config.is_summary_enabled()) + + ai_aside.is_course_settings_present.return_value = False + ai_aside.is_summary_enabled.return_value = False + self.assertIsNone(ai_aside_summary_config.is_summary_enabled(self.UNIT_KEY)) + + def test_set_summary_settings(self): + """ + Set the summary configuration settings for a particular unit using the ai_aside lib. + """ + ai_aside_summary_config = AiAsideSummaryConfig(self.COURSE_KEY) + ai_aside.set_unit_settings.return_value = True + self.assertTrue(ai_aside_summary_config.set_summary_settings(self.UNIT_KEY, {})) + self.assertIsNone(ai_aside_summary_config.set_summary_settings(self.UNIT_KEY)) diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index ea64061e9de..d7d9ef278b0 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -168,7 +168,11 @@ define( highlights_enabled: false, highlights_enabled_for_messaging: false, highlights_preview_only: true, - highlights_doc_url: '' + highlights_doc_url: '', + /** + * True if summary configuration is enabled. + */ + summary_configuration_enabled: null, }, initialize: function() { diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index ed1124d4658..a230d17797c 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -313,7 +313,7 @@ describe('CourseOutlinePage', function() { 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', - 'course-highlights-enable', 'course-video-sharing-enable' + 'course-highlights-enable', 'course-video-sharing-enable', 'summary-configuration-editor' ]); appendSetFixtures(mockOutlinePage); mockCourseJSON = createMockCourseJSON({}, [ @@ -2491,6 +2491,41 @@ describe('CourseOutlinePage', function() { }); }); + describe('summary configuration', function() { + it('hides summary configuration settings if summary_configuration_enabled is not a boolean', function() { + getUnitStatus({summary_configuration_enabled: null}); + outlinePage.$('.outline-unit .configure-button').click(); + expect($('.modal-section .summary-configuration')).not.toExist(); + }); + + it('shows summary configuration settings if summary_configuration_enabled is true', function() { + getUnitStatus({summary_configuration_enabled: true}); + outlinePage.$('.outline-unit .configure-button').click(); + expect($('.modal-section .summary-configuration')).toExist(); + }); + + it('shows summary configuration settings if summary_configuration_enabled is false', function() { + getUnitStatus({summary_configuration_enabled: false}); + outlinePage.$('.outline-unit .configure-button').click(); + expect($('.modal-section .summary-configuration')).toExist(); + }); + + it('can be updated', function() { + getUnitStatus({summary_configuration_enabled: false}); + outlinePage.$('.outline-unit .configure-button').click(); + expect($('#summary_configuration_enabled').is(':checked')).toBe(false); + $('#summary_configuration_enabled').prop('checked', true).trigger('change'); + $('.wrapper-modal-window .action-save').click(); + AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-unit', { + summary_configuration_enabled: true, + publish: 'republish', + metadata: { + visible_to_staff_only: null, + } + }); + }) + }); + verifyTypePublishable('unit', function(options) { return createMockCourseJSON({}, [ createMockSectionJSON({}, [ diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js index d00af0a537f..1e90b8455f4 100644 --- a/cms/static/js/views/modals/course_outline_modals.js +++ b/cms/static/js/views/modals/course_outline_modals.js @@ -19,7 +19,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ReleaseDateEditor, DueDateEditor, SelfPacedDueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor, StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor, AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor, - DiscussionEditor; + DiscussionEditor, SummaryConfigurationEditor; CourseOutlineXBlockModal = BaseModal.extend({ events: _.extend({}, BaseModal.prototype.events, { @@ -1203,6 +1203,42 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', } }); + SummaryConfigurationEditor = AbstractEditor.extend({ + templateName: 'summary-configuration-editor', + className: 'summary-configuration', + + afterRender: function() { + AbstractEditor.prototype.afterRender.call(this); + this.setEnabled(this.isModelEnabled()); + }, + + isModelEnabled: function() { + return this.model.get('summary_configuration_enabled'); + }, + + setEnabled: function(value) { + this.$('#summary_configuration_enabled').prop('checked', value); + }, + + isEnabled: function() { + return this.$('#summary_configuration_enabled').is(':checked'); + }, + + hasChanges: function() { + return this.isModelEnabled() !== this.isEnabled(); + }, + + getRequestData: function() { + if (this.hasChanges()) { + return { + summary_configuration_enabled: this.isEnabled() + }; + } else { + return {}; + } + } + }); + return { getModal: function(type, xblockInfo, options) { if (type === 'edit') { @@ -1228,6 +1264,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', }; if (xblockInfo.isVertical()) { editors = [StaffLockEditor, UnitAccessEditor, DiscussionEditor]; + if (typeof xblockInfo.get('summary_configuration_enabled') === 'boolean') { + editors.push(SummaryConfigurationEditor); + } } else { tabs = [ { diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss index b6508ef49a0..defcf49efe6 100644 --- a/cms/static/sass/elements/_modal-window.scss +++ b/cms/static/sass/elements/_modal-window.scss @@ -747,6 +747,7 @@ .edit-discussion, .edit-staff-lock, + .summary-configuration, .edit-content-visibility, .edit-unit-access { margin-bottom: $baseline; @@ -760,6 +761,7 @@ // UI: staff lock and discussion .edit-discussion, .edit-staff-lock, + .summary-configuration, .edit-settings-timed-examination, .edit-unit-access { .checkbox-cosmetic .input-checkbox { @@ -832,7 +834,8 @@ .edit-discussion, .edit-unit-access, - .edit-staff-lock { + .edit-staff-lock, + .summary-configuration { .modal-section-content { @include font-size(16); @@ -874,7 +877,8 @@ .edit-discussion, .edit-unit-access, - .edit-staff-lock { + .edit-staff-lock, + .summary-configuration { .modal-section-content { @include font-size(16); diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index dfbfd76c499..180c032d722 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -29,7 +29,7 @@ <%block name="header_extras"> -% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-video-sharing-enable']: +% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-video-sharing-enable', 'summary-configuration-editor']: diff --git a/cms/templates/js/summary-configuration-editor.underscore b/cms/templates/js/summary-configuration-editor.underscore new file mode 100644 index 00000000000..2ce517bb067 --- /dev/null +++ b/cms/templates/js/summary-configuration-editor.underscore @@ -0,0 +1,11 @@ +
+ + +