Merge pull request #17362 from caesar2164/allow-about-sidebar-content-from-instructor

Add custom HTML to Course About page sidebar
This commit is contained in:
Zia Fazal
2018-06-04 09:41:52 +05:00
committed by GitHub
13 changed files with 125 additions and 4 deletions

View File

@@ -130,6 +130,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=UTC))
self.alter_field(url, details, 'short_description', "Short Description")
self.alter_field(url, details, 'about_sidebar_html', "About Sidebar HTML")
self.alter_field(url, details, 'overview', "Overview")
self.alter_field(url, details, 'intro_video', "intro_video")
self.alter_field(url, details, 'effort', "effort")
@@ -148,6 +149,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertEqual(
details['short_description'], encoded['short_description'], context + " short_description not =="
)
self.assertEqual(
details['about_sidebar_html'], encoded['about_sidebar_html'], context + " about_sidebar_html not =="
)
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
@@ -270,6 +274,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "Introducing Your Course")
self.assertContains(response, "Course Card Image")
self.assertContains(response, "Course Short Description")
self.assertNotContains(response, "Course About Sidebar HTML")
self.assertNotContains(response, "Course Title")
self.assertNotContains(response, "Course Subtitle")
self.assertNotContains(response, "Course Duration")
@@ -425,6 +430,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertContains(response, "Course Duration")
self.assertContains(response, "Course Description")
self.assertContains(response, "Course Short Description")
self.assertNotContains(response, "Course About Sidebar HTML")
self.assertContains(response, "Course Overview")
self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements")

View File

@@ -25,6 +25,8 @@ from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import BlockUsageLocator
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from openedx.features.course_experience.waffle import waffle as course_experience_waffle
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from six import text_type
from contentstore.course_group_config import (
@@ -1050,6 +1052,7 @@ def settings_handler(request, course_key_string):
'EDITABLE_SHORT_DESCRIPTION',
settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
)
sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML)
# self_paced_enabled = SelfPacedConfiguration.current().enabled
settings_context = {
@@ -1062,6 +1065,7 @@ def settings_handler(request, course_key_string):
'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable,
'sidebar_html_enabled': sidebar_html_enabled,
'upload_asset_url': upload_asset_url,
'course_handler_url': reverse_course_url('course_handler', course_key),
'language_options': settings.ALL_LANGUAGES,

View File

@@ -16,6 +16,7 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
'change select': 'updateModel',
'click .remove-course-introduction-video': 'removeVideo',
'focus #course-overview': 'codeMirrorize',
'focus #course-about-sidebar-html': 'codeMirrorize',
'mouseover .timezone': 'updateTime',
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input': 'inputFocus',
@@ -97,6 +98,10 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
this.$el.find('#' + this.fieldToSelectorMap.description).val(this.model.get('description'));
this.$el.find('#' + this.fieldToSelectorMap.short_description).val(this.model.get('short_description'));
this.$el.find('#' + this.fieldToSelectorMap.about_sidebar_html).val(
this.model.get('about_sidebar_html')
);
this.codeMirrorize(null, $('#course-about-sidebar-html')[0]);
this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
this.$el.find('#' + this.fieldToSelectorMap.intro_video).val(this.model.get('intro_video') || '');
@@ -163,6 +168,7 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
subtitle: 'course-subtitle',
duration: 'course-duration',
description: 'course-description',
about_sidebar_html: 'course-about-sidebar-html',
short_description: 'course-short-description',
intro_video: 'course-introduction-video',
effort: 'course-effort',
@@ -363,7 +369,7 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
}
});
cmTextArea = this.codeMirrors[thisTarget.id].getInputField();
cmTextArea.setAttribute('id', 'course-overview-cm-textarea');
cmTextArea.setAttribute('id', thisTarget.id + '-cm-textarea');
}
},

View File

@@ -507,8 +507,10 @@
}
// specific fields - overview
#field-course-overview {
#course-overview {
#field-course-overview,
#field-course-about-sidebar-html {
#course-overview,
#course-about-sidebar-html {
height: ($baseline*20);
}

View File

@@ -366,6 +366,18 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
a_link_end=HTML("</a>")
)}</span>
</li>
% if sidebar_html_enabled:
<li class="field text" id="field-course-about-sidebar-html">
<label for="course-about-sidebar-html">${_("Course About Sidebar HTML")}
<textarea class="tinymce text-editor" id="course-about-sidebar-html" aria-describedby="tip-course-about-sidebar-html"></textarea>
</label>
<span class="tip tip-stacked" id="tip-course-about-sidebar-html">${
Text(_("Custom sidebar content for {a_link_start}your course summary page{a_link_end} (formatted in HTML)")).format(
a_link_start=HTML("<a class='link-courseURL' rel='external' href='{lms_link_for_about_page}'>").format(lms_link_for_about_page=lms_link_for_about_page),
a_link_end=HTML("</a>")
)}</span>
</li>
% endif
% endif
<li class="field image" id="field-course-image">

View File

@@ -224,6 +224,7 @@ def get_course_about_section(request, course, section_key):
Valid keys:
- overview
- about_sidebar_html
- short_description
- description
- key_dates (includes start, end, exams, etc)
@@ -259,6 +260,7 @@ def get_course_about_section(request, course, section_key):
'effort',
'end_date',
'prerequisites',
'about_sidebar_html',
'ocw_links'
}

View File

@@ -2,7 +2,7 @@
Test the about xblock
"""
import datetime
import ddt
import pytz
from ccx_keys.locator import CCXLocator
from django.conf import settings
@@ -12,9 +12,12 @@ from milestones.tests.utils import MilestonesTestCaseMixin
from mock import patch
from nose.plugins.attrib import attr
from six import text_type
from waffle.testutils import override_switch
from course_modes.models import CourseMode
from lms.djangoapps.ccx.tests.factories import CcxFactory
from openedx.features.course_experience.waffle import WAFFLE_NAMESPACE as COURSE_EXPERIENCE_WAFFLE_NAMESPACE
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from shoppingcart.models import Order, PaidCourseRegistration
from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory
@@ -417,6 +420,50 @@ class AboutWithClosedEnrollment(ModuleStoreTestCase):
self.assertNotIn('<span class="important-dates-item-text">$10</span>', resp.content)
@attr(shard=1)
@ddt.ddt
class AboutSidebarHTMLTestCase(SharedModuleStoreTestCase):
"""
This test case will check the About page for the content in the HTML sidebar.
"""
def setUp(self):
super(AboutSidebarHTMLTestCase, self).setUp()
self.course = CourseFactory.create()
@ddt.data(
("", "", False),
("about_sidebar_html", "About Sidebar HTML Heading", False),
("about_sidebar_html", "", False),
("", "", True),
("about_sidebar_html", "About Sidebar HTML Heading", True),
("about_sidebar_html", "", True),
)
@ddt.unpack
def test_html_sidebar_enabled(self, itemfactory_display_name, itemfactory_data, waffle_switch_value):
with override_switch(
'{}.{}'.format(
COURSE_EXPERIENCE_WAFFLE_NAMESPACE,
ENABLE_COURSE_ABOUT_SIDEBAR_HTML
),
active=waffle_switch_value
):
if itemfactory_display_name:
ItemFactory.create(
category="about",
parent_location=self.course.location,
display_name=itemfactory_display_name,
data=itemfactory_data,
)
url = reverse('about_course', args=[text_type(self.course.id)])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
if waffle_switch_value and itemfactory_display_name and itemfactory_data:
self.assertIn('<section class="about-sidebar-html">', resp.content)
self.assertIn(itemfactory_data, resp.content)
else:
self.assertNotIn('<section class="about-sidebar-html">', resp.content)
@attr(shard=1)
@patch.dict(settings.FEATURES, {'ENABLE_SHOPPING_CART': True})
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})

View File

@@ -90,6 +90,8 @@ from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
from openedx.features.course_experience.waffle import waffle as course_experience_waffle
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from openedx.features.enterprise_support.api import data_sharing_consent_required
from shoppingcart.utils import is_shopping_cart_enabled
from student.models import CourseEnrollment, UserTestGroup
@@ -835,6 +837,8 @@ def course_about(request, course_id):
# Overview
overview = CourseOverview.get_from_id(course.id)
sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML)
# This local import is due to the circularity of lms and openedx references.
# This may be resolved by using stevedore to allow web fragments to be used
# as plugins, and to avoid the direct import.
@@ -872,6 +876,7 @@ def course_about(request, course_id):
'pre_requisite_courses': pre_requisite_courses,
'course_image_urls': overview.image_urls,
'reviews_fragment_view': reviews_fragment_view,
'sidebar_html_enabled': sidebar_html_enabled,
}
return render_to_response('courseware/course_about.html', context)

View File

@@ -439,6 +439,12 @@
background: url('#{$static-path}/images/link-icon.png') left center no-repeat;
}
}
&.about-sidebar-html {
padding: 0 10px;
box-shadow: none;
border: none;
}
}
header {

View File

@@ -328,6 +328,13 @@ from six import string_types
</div>
%endif
% if sidebar_html_enabled:
% if get_course_about_section(request, course, "about_sidebar_html"):
<section class="about-sidebar-html">
${get_course_about_section(request, course, "about_sidebar_html")}
</section>
% endif
%endif
</div>
</div>

View File

@@ -27,6 +27,7 @@ ABOUT_ATTRIBUTES = [
'entrance_exam_enabled',
'entrance_exam_id',
'entrance_exam_minimum_score_pct',
'about_sidebar_html',
]
@@ -52,6 +53,7 @@ class CourseDetails(object):
self.description = ""
self.short_description = ""
self.overview = "" # html to render as the overview
self.about_sidebar_html = ""
self.intro_video = None # a video pointer
self.effort = None # hours/week
self.license = "all-rights-reserved" # default course license is all rights reserved

View File

@@ -72,6 +72,11 @@ class CourseDetailsTestCase(ModuleStoreTestCase):
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video,
jsondetails.intro_video, "After set intro_video"
)
jsondetails.about_sidebar_html = "About Sidebar HTML"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).about_sidebar_html,
jsondetails.about_sidebar_html, "After set about_sidebar_html"
)
jsondetails.effort = "effort"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort,

View File

@@ -0,0 +1,17 @@
"""
Miscellaneous waffle switches that both LMS and Studio need to access
"""
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
# Namespace
WAFFLE_NAMESPACE = u'course_experience'
# Switches
ENABLE_COURSE_ABOUT_SIDEBAR_HTML = u'enable_about_sidebar_html'
def waffle():
"""
Returns the namespaced, cached, audited shared Waffle Switch class.
"""
return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Course Experience: ')