diff --git a/AUTHORS b/AUTHORS index e4bc6c3777cd13f224ab581d96dc5371520b28e5..b5ac7c421ed194e330ade04fdf75c2461235ff26 100644 --- a/AUTHORS +++ b/AUTHORS @@ -129,3 +129,4 @@ Alison Hodges <ahodges@edx.org> Jane Manning <jmanning@gmail.com> Toddi Norum <toddi@edx.org> Xavier Antoviaque <xavier@antoviaque.org> +Ali Reza Sharafat <ali.sharafat@gmail.com> diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f541a43e8d54499f6bb9502eb8c5d2b03f044d5f..f63b3b5b55415520c75fd1b5a4610c8e980f6521 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ Blades: Add view for field type Dict in Studio. BLD-658. Blades: Refactor stub implementation of LTI Provider. BLD-601. +Studio: Added ability to edit course short descriptions that appear on the course catalog page. + LMS: In left accordion and progress page, due dates are now displayed in time zone specified by settings.TIME_ZONE, instead of UTC always diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 9b33654b70f850334239f2d28d1693e20dbb6245..b3ef6bd80a41d28bf6849dc7f099eeb024fcbd0e 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -76,6 +76,11 @@ class CourseDetailsTestCase(CourseTestCase): CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).syllabus, jsondetails.syllabus, "After set syllabus" ) + jsondetails.short_description = "Short Description" + self.assertEqual( + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).short_description, + jsondetails.short_description, "After set short_description" + ) jsondetails.overview = "Overview" self.assertEqual( CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__, self.user).overview, @@ -120,10 +125,19 @@ class CourseDetailsTestCase(CourseTestCase): self.assertContains(response, "Introducing Your Course") self.assertContains(response, "Course Image") + self.assertContains(response, "Course Short Description") self.assertNotContains(response, "Course Overview") self.assertNotContains(response, "Course Introduction Video") self.assertNotContains(response, "Requirements") + def test_editable_short_description_fetch(self): + settings_details_url = self.course_locator.url_reverse('settings/details/') + + with mock.patch.dict('django.conf.settings.FEATURES', {'EDITABLE_SHORT_DESCRIPTION': False}): + response = self.client.get_html(settings_details_url) + self.assertNotContains(response, "Course Short Description") + + def test_regular_site_fetch(self): settings_details_url = self.course_locator.url_reverse('settings/details/') @@ -141,6 +155,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertContains(response, "Introducing Your Course") self.assertContains(response, "Course Image") + self.assertContains(response, "Course Short Description") self.assertContains(response, "Course Overview") self.assertContains(response, "Course Introduction Video") self.assertContains(response, "Requirements") @@ -186,6 +201,7 @@ class CourseDetailsViewTest(CourseTestCase): self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc)) 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, 'overview', "Overview") self.alter_field(url, details, 'intro_video', "intro_video") self.alter_field(url, details, 'effort', "effort") @@ -199,6 +215,7 @@ class CourseDetailsViewTest(CourseTestCase): self.compare_date_fields(details, encoded, context, 'end_date') self.compare_date_fields(details, encoded, context, 'enrollment_start') self.compare_date_fields(details, encoded, context, 'enrollment_end') + self.assertEqual(details['short_description'], encoded['short_description'], context + " short_description 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 ==") diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 782ea03c3b0fb021946f71cfee39dceb9c23c400..5816982e578be967994b956c8756b8b9a4557f1d 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -459,6 +459,8 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu settings.FEATURES.get('ENABLE_MKTG_SITE', False) ) + short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) + return render_to_response('settings.html', { 'context_course': course_module, 'course_locator': locator, @@ -466,6 +468,7 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu 'course_image_url': utils.course_image_url(course_module), 'details_url': locator.url_reverse('/settings/details/'), 'about_page_editable': about_page_editable, + 'short_description_editable': short_description_editable, 'upload_asset_url': upload_asset_url }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index eeb82c54a513daf389b4be688cd30620ac2267e0..41d5ba98fe88b4adb2af2234ad3b91d29caa1eae 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -23,6 +23,7 @@ class CourseDetails(object): self.enrollment_start = None self.enrollment_end = None self.syllabus = None # a pdf file asset + self.short_description = "" self.overview = "" # html to render as the overview self.intro_video = None # a video pointer self.effort = None # int hours/week @@ -51,6 +52,12 @@ class CourseDetails(object): except ItemNotFoundError: pass + temploc = course_old_location.replace(category='about', name='short_description') + try: + course.short_description = get_modulestore(temploc).get_item(temploc).data + except ItemNotFoundError: + pass + temploc = temploc.replace(name='overview') try: course.overview = get_modulestore(temploc).get_item(temploc).data @@ -150,7 +157,7 @@ class CourseDetails(object): # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. - for about_type in ['syllabus', 'overview', 'effort']: + for about_type in ['syllabus', 'overview', 'effort', 'short_description']: cls.update_about_item(course_old_location, about_type, jsondict[about_type], descriptor, user) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) diff --git a/cms/envs/common.py b/cms/envs/common.py index ea8003161119f13fe42c869d6cbedcd742e61725..2c98ceda178e1bcba044b014c0aa0c1b35cfaf5b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -73,6 +73,9 @@ FEATURES = { # Turn off account locking if failed login attempts exceeds a limit 'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': False, + + # Allow editing of short description in course settings in cms + 'EDITABLE_SHORT_DESCRIPTION': True, } ENABLE_JASMINE = False diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 058cacadd70eeb401bdaf42d8706b8283198a0d2..4cf28519039e0adf3944df890c4689a45d9ffb41 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -10,6 +10,7 @@ var CourseDetails = Backbone.Model.extend({ enrollment_start: null, enrollment_end: null, syllabus: null, + short_description: "", overview: "", intro_video: null, effort: null, // an int or null, diff --git a/cms/static/js/views/settings/main.js b/cms/static/js/views/settings/main.js index 63776829c3bd1bcc3d273fe6412ecc7a7fdc667e..08aeabb83c49ad5e11ef537358a66ba42de5be80 100644 --- a/cms/static/js/views/settings/main.js +++ b/cms/static/js/views/settings/main.js @@ -50,6 +50,8 @@ var DetailsView = ValidatingView.extend({ this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview')); this.codeMirrorize(null, $('#course-overview')[0]); + this.$el.find('#' + this.fieldToSelectorMap['short_description']).val(this.model.get('short_description')); + 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') || ''); if (this.model.has('intro_video')) { @@ -71,6 +73,7 @@ var DetailsView = ValidatingView.extend({ 'enrollment_start' : 'enrollment-start', 'enrollment_end' : 'enrollment-end', 'overview' : 'course-overview', + 'short_description' : 'course-short-description', 'intro_video' : 'course-introduction-video', 'effort' : "course-effort", 'course_image_asset_path': 'course-image-url' @@ -148,6 +151,9 @@ var DetailsView = ValidatingView.extend({ case 'course-effort': this.setField(event); break; + case 'course-short-description': + this.setField(event); + break; // Don't make the user reload the page to check the Youtube ID. // Wait for a second to load the video, avoiding egregious AJAX calls. case 'course-introduction-video': diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 2c27c2562d433a2d01ae0cd7525097f01ecb78c6..e78ed541513677b19dbcd45def61c6e87172d4fc 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -67,19 +67,19 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s <ol class="list-input"> <li class="field text is-not-editable" id="field-course-organization"> <label for="course-organization">${_("Organization")}</label> - <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" readonly /> </li> <li class="field text is-not-editable" id="field-course-number"> <label for="course-number">${_("Course Number")}</label> - <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" readonly> </li> <li class="field text is-not-editable" id="field-course-name"> <label for="course-name">${_("Course Name")}</label> - <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" readonly /> </li> </ol> @@ -93,7 +93,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s <ul class="list-actions"> <li class="action-item"> - <a title="${_('Send a note to students via email')}" + <a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${lms_link_for_about_page}%20to%20enroll." class="action action-primary"> <i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a> </li> @@ -198,6 +198,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s <span class="tip">${_("Information for prospective students")}</span> </header> <ol class="list-input"> + % if short_description_editable: + <li class="field text" id="field-course-short-description"> + <label for="course-overview">${_("Course Short Description")}</label> + <textarea class="text" id="course-short-description"></textarea> + <span class="tip tip-stacked">${_("Appears on the course catalog page when students roll over the course name. Limit to ~150 characters")}</span> + </li> + % endif + % if about_page_editable: <li class="field text" id="field-course-overview"> <label for="course-overview">${_("Course Overview")}</label>