diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index 0aeb95dbd9..ca8b3ec4cb 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -79,7 +79,6 @@ Feature: CMS.Course Settings | Course Start Time | 11:00 | | Course Introduction Video | 4r7wHMg5Yjg | | Course Effort | 200:00 | - | Course Image URL | image.jpg | # Special case because we have to type in code mirror Scenario: Changes in Course Overview show a confirmation @@ -94,11 +93,3 @@ Feature: CMS.Course Settings When I select Schedule and Details And I change the "Course Start Date" field to "" Then the save notification button is disabled - - Scenario: User can upload course image - Given I have opened a new course in Studio - When I select Schedule and Details - And I click the "Upload Course Image" button - And I upload a new course image - Then I should see the new course image - And the image URL should be present in the field diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py index 4e11b393d1..350eb0392e 100644 --- a/cms/djangoapps/contentstore/features/course-settings.py +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -132,37 +132,6 @@ def test_change_course_overview(_step): type_in_codemirror(0, "

Overview

") -@step('I click the "Upload Course Image" button') -def click_upload_button(_step): - button_css = '.action-upload-image' - world.css_click(button_css) - - -@step('I upload a new course image$') -def upload_new_course_image(_step): - upload_file('image.jpg', sub_path="uploads") - - -@step('I should see the new course image$') -def i_see_new_course_image(_step): - img_css = '#course-image' - images = world.css_find(img_css) - assert len(images) == 1 - img = images[0] - expected_src = 'image.jpg' - - # Don't worry about the domain in the URL - success_func = lambda _: img['src'].endswith(expected_src) - world.wait_for(success_func) - - -@step('the image URL should be present in the field') -def image_url_present(_step): - field_css = '#course-image-url' - expected_value = 'image.jpg' - assert world.css_value(field_css).endswith(expected_value) - - ############### HELPER METHODS #################### def set_date_or_time(css, date_or_time): """ diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 5d5c6a5d7c..5171d59059 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -255,7 +255,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): self.assertContains(response, "not the dates shown on your course summary page") self.assertContains(response, "Introducing Your Course") - self.assertContains(response, "Course Image") + self.assertContains(response, "Course Card Image") self.assertContains(response, "Course Short Description") self.assertNotContains(response, "Course Title") self.assertNotContains(response, "Course Subtitle") @@ -264,6 +264,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): self.assertNotContains(response, "Course Overview") self.assertNotContains(response, "Course Introduction Video") self.assertNotContains(response, "Requirements") + self.assertNotContains(response, "Course Banner Image") + self.assertNotContains(response, "Course Video Thumbnail Image") @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True) def test_entrance_exam_created_updated_and_deleted_successfully(self): @@ -385,7 +387,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): self.assertNotContains(response, "not the dates shown on your course summary page") self.assertContains(response, "Introducing Your Course") - self.assertContains(response, "Course Image") + self.assertContains(response, "Course Card Image") self.assertContains(response, "Course Title") self.assertContains(response, "Course Subtitle") self.assertContains(response, "Course Duration") @@ -394,6 +396,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): self.assertContains(response, "Course Overview") self.assertContains(response, "Course Introduction Video") self.assertContains(response, "Requirements") + self.assertContains(response, "Course Banner Image") + self.assertContains(response, "Course Video Thumbnail Image") @ddt.ddt diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 0fcf6714c5..4450031b62 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -991,7 +991,9 @@ def settings_handler(request, course_key_string): 'context_course': course_module, 'course_locator': course_key, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key), - 'course_image_url': course_image_url(course_module), + 'course_image_url': course_image_url(course_module, 'course_image'), + 'banner_image_url': course_image_url(course_module, 'banner_image'), + 'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'), 'details_url': reverse_course_url('settings_handler', course_key), 'about_page_editable': about_page_editable, 'short_description_editable': short_description_editable, diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json index 14115e3612..2e9c6086b6 100644 --- a/cms/envs/bok_choy.env.json +++ b/cms/envs/bok_choy.env.json @@ -76,7 +76,8 @@ "ALLOW_ALL_ADVANCED_COMPONENTS": true, "ENABLE_CONTENT_LIBRARIES": true, "ENABLE_SPECIAL_EXAMS": true, - "SHOW_LANGUAGE_SELECTOR": true + "SHOW_LANGUAGE_SELECTOR": true, + "ENABLE_EXTENDED_COURSE_DETAILS": true }, "FEEDBACK_SUBMISSION_EMAIL": "", "GITHUB_REPO_ROOT": "** OVERRIDDEN **", diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 8742a8fe67..2e192e066b 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -23,6 +23,10 @@ var CourseDetails = Backbone.Model.extend({ license: null, course_image_name: '', // the filename course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename) + banner_image_name: '', + banner_image_asset_path: '', + video_thumbnail_image_name: '', + video_thumbnail_image_asset_path: '', pre_requisite_courses: [], entrance_exam_enabled : '', entrance_exam_minimum_score_pct: '50' diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js index 202ff4cf0d..5c686f4ac9 100644 --- a/cms/static/js/spec/views/settings/main_spec.js +++ b/cms/static/js/spec/views/settings/main_spec.js @@ -31,6 +31,10 @@ define([ effort : null, course_image_name : '', course_image_asset_path : '', + banner_image_name : '', + banner_image_asset_path : '', + video_thumbnail_image_name : '', + video_thumbnail_image_asset_path : '', pre_requisite_courses : [], entrance_exam_enabled : '', entrance_exam_minimum_score_pct: '50', @@ -182,5 +186,69 @@ define([ AjaxHelpers.expectJsonRequest(requests, 'POST', urlRoot, modelData); }); + it('should save title', function(){ + var requests = AjaxHelpers.requests(this), + expectedJson = $.extend(true, {}, modelData, { + title: 'course title' + }); + + // Input some value. + this.view.$("#course-title").val('course title'); + this.view.$("#course-title").trigger('change'); + this.view.saveView(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', urlRoot, expectedJson + ); + AjaxHelpers.respondWithJson(requests, expectedJson); + }); + + it('should save subtitle', function(){ + var requests = AjaxHelpers.requests(this), + expectedJson = $.extend(true, {}, modelData, { + subtitle: 'course subtitle' + }); + + // Input some value. + this.view.$("#course-subtitle").val('course subtitle'); + this.view.$("#course-subtitle").trigger('change'); + this.view.saveView(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', urlRoot, expectedJson + ); + AjaxHelpers.respondWithJson(requests, expectedJson); + }); + + it('should save duration', function(){ + var requests = AjaxHelpers.requests(this), + expectedJson = $.extend(true, {}, modelData, { + duration: '8 weeks' + }); + + // Input some value. + this.view.$("#course-duration").val('8 weeks'); + this.view.$("#course-duration").trigger('change'); + this.view.saveView(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', urlRoot, expectedJson + ); + AjaxHelpers.respondWithJson(requests, expectedJson); + }); + + it('should save description', function(){ + var requests = AjaxHelpers.requests(this), + expectedJson = $.extend(true, {}, modelData, { + description: 'course description' + }); + + // Input some value. + this.view.$("#course-description").val('course description'); + this.view.$("#course-description").trigger('change'); + this.view.saveView(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', urlRoot, expectedJson + ); + AjaxHelpers.respondWithJson(requests, expectedJson); + }); + }); }); diff --git a/cms/static/js/views/settings/main.js b/cms/static/js/views/settings/main.js index 81221969d6..063e5ec47f 100644 --- a/cms/static/js/views/settings/main.js +++ b/cms/static/js/views/settings/main.js @@ -34,10 +34,10 @@ var DetailsView = ValidatingView.extend({ this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); // Avoid showing broken image on mistyped/nonexistent image - this.$el.find('img.course-image').error(function() { + this.$el.find('img').error(function() { $(this).hide(); }); - this.$el.find('img.course-image').load(function() { + this.$el.find('img').load(function() { $(this).show(); }); @@ -92,9 +92,17 @@ var DetailsView = ValidatingView.extend({ this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort')); - var imageURL = this.model.get('course_image_asset_path'); - this.$el.find('#course-image-url').val(imageURL); - this.$el.find('#course-image').attr('src', imageURL); + var courseImageURL = this.model.get('course_image_asset_path'); + this.$el.find('#course-image-url').val(courseImageURL); + this.$el.find('#course-image').attr('src', courseImageURL); + + var bannerImageURL = this.model.get('banner_image_asset_path'); + this.$el.find('#banner-image-url').val(bannerImageURL); + this.$el.find('#banner-image').attr('src', bannerImageURL); + + var videoThumbnailImageURL = this.model.get('video_thumbnail_image_asset_path'); + this.$el.find('#video-thumbnail-image-url').val(videoThumbnailImageURL); + this.$el.find('#video-thumbnail-image').attr('src', videoThumbnailImageURL); var pre_requisite_courses = this.model.get('pre_requisite_courses'); pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : ''; @@ -144,6 +152,8 @@ var DetailsView = ValidatingView.extend({ 'intro_video' : 'course-introduction-video', 'effort' : "course-effort", 'course_image_asset_path': 'course-image-url', + 'banner_image_asset_path': 'banner-image-url', + 'video_thumbnail_image_asset_path': 'video-thumbnail-image-url', 'pre_requisite_courses': 'pre-requisite-course', 'entrance_exam_enabled': 'entrance-exam-enabled', 'entrance_exam_minimum_score_pct': 'entrance-exam-minimum-score-pct' @@ -166,15 +176,13 @@ var DetailsView = ValidatingView.extend({ updateModel: function(event) { switch (event.currentTarget.id) { case 'course-image-url': - this.setField(event); - var url = $(event.currentTarget).val(); - var image_name = _.last(url.split('/')); - this.model.set('course_image_name', image_name); - // Wait to set the image src until the user stops typing - clearTimeout(this.imageTimer); - this.imageTimer = setTimeout(function() { - $('#course-image').attr('src', $(event.currentTarget).val()); - }, 1000); + this.updateImageField(event, 'course_image_name', '#course-image'); + break; + case 'banner-image-url': + this.updateImageField(event, 'banner_image_name', '#banner-image'); + break; + case 'video-thumbnail-image-url': + this.updateImageField(event, 'video_thumbnail_image_name', '#video-thumbnail-image'); break; case 'entrance-exam-enabled': if($(event.currentTarget).is(":checked")){ @@ -232,7 +240,17 @@ var DetailsView = ValidatingView.extend({ break; } }, - + updateImageField: function(event, image_field, selector) { + this.setField(event); + var url = $(event.currentTarget).val(); + var image_name = _.last(url.split('/')); + this.model.set(image_field, image_name); + // Wait to set the image src until the user stops typing + clearTimeout(this.imageTimer); + this.imageTimer = setTimeout(function() { + $(selector).attr('src', $(event.currentTarget).val()); + }, 1000); + }, removeVideo: function(event) { event.preventDefault(); if (this.model.has('intro_video')) { @@ -316,8 +334,30 @@ var DetailsView = ValidatingView.extend({ uploadImage: function(event) { event.preventDefault(); + var title = "", selector = "", image_key = "", image_path_key = ""; + switch (event.currentTarget.id) { + case 'upload-course-image': + title = gettext("Upload your course image."); + selector = "#course-image"; + image_key = 'course_image_name'; + image_path_key = 'course_image_asset_path'; + break; + case 'upload-banner-image': + title = gettext("Upload your banner image."); + selector = "#banner-image"; + image_key = 'banner_image_name'; + image_path_key = 'banner_image_asset_path'; + break; + case 'upload-video-thumbnail-image': + title = gettext("Upload your video thumbnail image."); + selector = "#video-thumbnail-image"; + image_key = 'video_thumbnail_image_name'; + image_path_key = 'video_thumbnail_image_asset_path'; + break; + } + var upload = new FileUploadModel({ - title: gettext("Upload your course image."), + title: title, message: gettext("Files must be in JPEG or PNG format."), mimeTypes: ['image/jpeg', 'image/png'] }); @@ -325,13 +365,12 @@ var DetailsView = ValidatingView.extend({ var modal = new FileUploadDialog({ model: upload, onSuccess: function(response) { - var options = { - 'course_image_name': response.asset.display_name, - 'course_image_asset_path': response.asset.url - }; + var options = {}; + options[image_key] = response.asset.display_name; + options[image_path_key] = response.asset.url; self.model.set(options); self.render(); - $('#course-image').attr('src', self.model.get('course_image_asset_path')); + $(selector).attr('src', self.model.get(image_path_key)); } }); modal.show(); diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index f753a42d20..0a56fec268 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -516,7 +516,7 @@ } // specific fields - course image - #field-course-image { + #field-course-image, #field-banner-image, #field-video-thumbnail-image { .current-course-image { margin-bottom: ($baseline/2); diff --git a/cms/templates/js/mock/mock-settings-page.underscore b/cms/templates/js/mock/mock-settings-page.underscore index 1422279fb9..cc92b82f9e 100644 --- a/cms/templates/js/mock/mock-settings-page.underscore +++ b/cms/templates/js/mock/mock-settings-page.underscore @@ -105,6 +105,96 @@ Identify the course language here. This is used to assist users find courses that are taught in a specific language. + + +
+
+

Introducing Your Course

+ Information for prospective students +
+
    + +
  1. + + + Displayed as title on the course details page. Limit to 50 characters. +
  2. +
  3. + + + Displayed as subtitle on the course details page. Limit to 150 characters. +
  4. +
  5. + + + Displayed on the course details page. Limit to 50 characters. +
  6. +
  7. + + + Displayed on the course details page. Limit to 1000 characters. +
  8. + +
  9. + +
    + + Course Image + + Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall) +
    + +
    +
    + ## Translators: This is the placeholder text for a field that requests the URL for a course image + + Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported) +
    + +
    +
  10. + +
  11. + +
    + + + + Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 1440px wide by 400px tall) +
    + +
    +
    + ## Translators: This is the placeholder text for a field that requests the URL for a course banner image + + Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported) +
    + +
    +
  12. + +
  13. + +
    + + Video Thumbnail Image + + Your course currently does not have a video thumbnail image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall) +
    + +
    +
    + ## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image + + Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported) +
    + +
    +
  14. + +
+
+ diff --git a/cms/templates/settings.html b/cms/templates/settings.html index f5df1caddb..428870f17b 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -297,17 +297,17 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'
  • - ${_("Displayed as hero image overlay on the course details page. Limit to 50 characters.")} + ${_("Displayed as title on the course details page. Limit to 50 characters.")}
  • - ${_("Displayed as hero image overlay on the course details page below the Course Title in a smaller font. Limit to 150 characters.")} + ${_("Displayed as subtitle on the course details page. Limit to 150 characters.")}
  • - ${_("Displayed on the course details page below the hero image. Limit to 50 characters.")} + ${_("Displayed on the course details page. Limit to 50 characters.")}
  • @@ -339,11 +339,11 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' % endif
  • - +
    % if context_course.course_image: - ${_('Course Image')} + ${_('Course Card Image')} @@ -352,7 +352,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' % else: - ${_('Course Image')} + ${_('Course Card Image')} ${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")} % endif @@ -364,10 +364,72 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}' ${_("Please provide a valid path and name to your course image (Note: only JPEG or PNG format supported)")}
    - +
  • + % if enable_extended_course_details: +
  • + +
    + % if context_course.banner_image: + + + + + + ${_("You can manage this image along with all of your other files & uploads").format(upload_asset_url)} + + + % else: + + + + ${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 1440px wide by 400px tall)")} + % endif +
    + +
    +
    + ## Translators: This is the placeholder text for a field that requests the URL for a course banner image + + ${_("Please provide a valid path and name to your banner image (Note: only JPEG or PNG format supported)")} +
    + +
    +
  • + +
  • + +
    + % if context_course.video_thumbnail_image: + + ${_('Video Thumbnail Image')} + + + + ${_("You can manage this image along with all of your other files & uploads").format(upload_asset_url)} + + + % else: + + ${_('Video Thumbnail Image')} + + ${_("Your course currently does not have a video thumbnail image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")} + % endif +
    + +
    +
    + ## Translators: This is the placeholder text for a field that requests the URL for a course video thumbnail image + + ${_("Please provide a valid path and name to your video thumbnail image (Note: only JPEG or PNG format supported)")} +
    + +
    +
  • + % endif + % if about_page_editable:
  • diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 9f15a46de9..6ed3385032 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -475,6 +475,26 @@ class CourseFields(object): # Ensure that courses imported from XML keep their image default="images_course_image.jpg" ) + banner_image = String( + display_name=_("Course Banner Image"), + help=_( + "Edit the name of the banner image file. " + "You can set the banner image on the Settings & Details page." + ), + scope=Scope.settings, + # Ensure that courses imported from XML keep their image + default="images_course_image.jpg" + ) + video_thumbnail_image = String( + display_name=_("Course Video Thumbnail Image"), + help=_( + "Edit the name of the video thumbnail image file. " + "You can set the video thumbnail image on the Settings & Details page." + ), + scope=Scope.settings, + # Ensure that courses imported from XML keep their image + default="images_course_image.jpg" + ) issue_badges = Boolean( display_name=_("Issue Open Badges"), help=_( diff --git a/common/test/acceptance/pages/studio/settings.py b/common/test/acceptance/pages/studio/settings.py index 53e2102597..adb012f95d 100644 --- a/common/test/acceptance/pages/studio/settings.py +++ b/common/test/acceptance/pages/studio/settings.py @@ -3,6 +3,7 @@ Course Schedule and Details Settings page. """ from __future__ import unicode_literals +import os from bok_choy.promise import EmptyPromise from bok_choy.javascript import requirejs @@ -17,6 +18,9 @@ class SettingsPage(CoursePage): """ url_path = "settings/details" + upload_image_browse_button_selector = 'form.upload-dialog input[type=file]' + upload_image_upload_button_selector = '.modal-actions li:nth-child(1) a' + upload_image_popup_window_selector = '.assetupload-modal' ################ # Helpers @@ -234,3 +238,50 @@ class SettingsPage(CoursePage): ).fulfill() self.wait_for_require_js() self.wait_for_ajax() + + @staticmethod + def get_asset_path(file_name): + """ + Returns the full path of the file to upload. + These files have been placed in edx-platform/common/test/data/uploads/ + """ + + # Separate the list of folders in the path reaching to the current file, + # e.g. '... common/test/acceptance/pages/lms/instructor_dashboard.py' will result in + # [..., 'common', 'test', 'acceptance', 'pages', 'lms', 'instructor_dashboard.py'] + folders_list_in_path = __file__.split(os.sep) + + # Get rid of the last 4 elements: 'acceptance', 'pages', 'lms', and 'instructor_dashboard.py' + # to point to the 'test' folder, a shared point in the path's tree. + folders_list_in_path = folders_list_in_path[:-4] + + # Append the folders in the asset's path + folders_list_in_path.extend(['data', 'uploads', file_name]) + + # Return the joined path of the required asset. + return os.sep.join(folders_list_in_path) + + def upload_image(self, image_selector, file_to_upload): + """ + Upload image specified by image_selector and file_to_upload + """ + + self.q(css=image_selector).results[0].click() + + # wait for popup + self.wait_for_element_presence(self.upload_image_popup_window_selector, 'upload dialog is present') + + # upload image + filepath = SettingsPage.get_asset_path(file_to_upload) + self.q(css=self.upload_image_browse_button_selector).results[0].send_keys(filepath) + self.q(css=self.upload_image_upload_button_selector).results[0].click() + + # wait for popup closed + self.wait_for_element_absence(self.upload_image_popup_window_selector, 'upload dialog is hidden') + + def get_uploaded_image_path(self, image_selector): + """ + Returns the uploaded image path + """ + + return self.q(css=image_selector).attrs('src')[0] diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index e209207189..7f189a4cd9 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -172,6 +172,8 @@ class AdvancedSettingsPage(CoursePage): 'cert_name_short', 'certificates_display_behavior', 'course_image', + 'banner_image', + 'video_thumbnail_image', 'cosmetic_display_price', 'advertised_start', 'announcement', diff --git a/common/test/acceptance/tests/studio/test_studio_settings.py b/common/test/acceptance/tests/studio/test_studio_settings.py index 3e63ee7167..9b67b5dcd4 100644 --- a/common/test/acceptance/tests/studio/test_studio_settings.py +++ b/common/test/acceptance/tests/studio/test_studio_settings.py @@ -571,3 +571,42 @@ class StudioSubsectionSettingsA11yTest(StudioCourseTest): include=['section.edit-settings-timed-examination'] ) self.course_outline.a11y_audit.check_for_accessibility_errors() + + +class StudioSettingsImageUploadTest(StudioCourseTest): + + """ + Class to test course settings image uploads. + """ + + def setUp(self): # pylint: disable=arguments-differ + super(StudioSettingsImageUploadTest, self).setUp() + self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'], + self.course_info['run']) + + def test_upload_course_card_image(self): + self.settings_page.visit() + self.settings_page.wait_for_page() + + # upload image + file_to_upload = 'image.jpg' + self.settings_page.upload_image('#upload-course-image', file_to_upload) + self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#course-image')) + + def test_upload_course_banner_image(self): + self.settings_page.visit() + self.settings_page.wait_for_page() + + # upload image + file_to_upload = 'image.jpg' + self.settings_page.upload_image('#upload-banner-image', file_to_upload) + self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#banner-image')) + + def test_upload_course_video_thumbnail_image(self): + self.settings_page.visit() + self.settings_page.wait_for_page() + + # upload image + file_to_upload = 'image.jpg' + self.settings_page.upload_image('#upload-video-thumbnail-image', file_to_upload) + self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#video-thumbnail-image')) diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py index 980484d57a..c818642def 100644 --- a/openedx/core/djangoapps/models/course_details.py +++ b/openedx/core/djangoapps/models/course_details.py @@ -58,6 +58,10 @@ class CourseDetails(object): self.license = "all-rights-reserved" # default course license is all rights reserved self.course_image_name = "" self.course_image_asset_path = "" # URL of the course image + self.banner_image_name = "" + self.banner_image_asset_path = "" + self.video_thumbnail_image_name = "" + self.video_thumbnail_image_asset_path = "" self.pre_requisite_courses = [] # pre-requisite courses self.entrance_exam_enabled = "" # is entrance exam enabled self.entrance_exam_id = "" # the content location for the entrance exam @@ -97,7 +101,11 @@ class CourseDetails(object): course_details.enrollment_end = descriptor.enrollment_end course_details.pre_requisite_courses = descriptor.pre_requisite_courses course_details.course_image_name = descriptor.course_image - course_details.course_image_asset_path = course_image_url(descriptor) + course_details.course_image_asset_path = course_image_url(descriptor, 'course_image') + course_details.banner_image_name = descriptor.banner_image + course_details.banner_image_asset_path = course_image_url(descriptor, 'banner_image') + course_details.video_thumbnail_image_name = descriptor.video_thumbnail_image + course_details.video_thumbnail_image_asset_path = course_image_url(descriptor, 'video_thumbnail_image') course_details.language = descriptor.language course_details.self_paced = descriptor.self_paced @@ -217,6 +225,15 @@ class CourseDetails(object): descriptor.course_image = jsondict['course_image_name'] dirty = True + if 'banner_image_name' in jsondict and jsondict['banner_image_name'] != descriptor.banner_image: + descriptor.banner_image = jsondict['banner_image_name'] + dirty = True + + if 'video_thumbnail_image_name' in jsondict \ + and jsondict['video_thumbnail_image_name'] != descriptor.video_thumbnail_image: + descriptor.video_thumbnail_image = jsondict['video_thumbnail_image_name'] + dirty = True + if 'pre_requisite_courses' in jsondict \ and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses): descriptor.pre_requisite_courses = jsondict['pre_requisite_courses'] diff --git a/openedx/core/djangoapps/models/tests/test_course_details.py b/openedx/core/djangoapps/models/tests/test_course_details.py index 46f39982cf..fd778ee10a 100644 --- a/openedx/core/djangoapps/models/tests/test_course_details.py +++ b/openedx/core/djangoapps/models/tests/test_course_details.py @@ -95,6 +95,16 @@ class CourseDetailsTestCase(ModuleStoreTestCase): CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name, jsondetails.course_image_name ) + jsondetails.banner_image_name = "an_image.jpg" + self.assertEqual( + CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).banner_image_name, + jsondetails.banner_image_name + ) + jsondetails.video_thumbnail_image_name = "an_image.jpg" + self.assertEqual( + CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).video_thumbnail_image_name, + jsondetails.video_thumbnail_image_name + ) jsondetails.language = "hr" self.assertEqual( CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language, diff --git a/openedx/core/lib/courses.py b/openedx/core/lib/courses.py index ca5d3f13d8..77f3920b10 100644 --- a/openedx/core/lib/courses.py +++ b/openedx/core/lib/courses.py @@ -10,24 +10,25 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum -def course_image_url(course): +def course_image_url(course, image_key='course_image'): """Try to look up the image url for the course. If it's not found, - log an error and return the dead link""" + log an error and return the dead link. + image_key can be one of the three: 'course_image', 'hero_image', 'thumbnail_image' """ if course.static_asset_path: - # If we are a static course with the course_image attribute + # If we are a static course with the image_key attribute # set different than the default, return that path so that # courses can use custom course image paths, otherwise just # return the default static path. url = '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) - if hasattr(course, 'course_image') and course.course_image != course.fields['course_image'].default: - url += '/' + course.course_image + if hasattr(course, image_key) and getattr(course, image_key) != course.fields[image_key].default: + url += '/' + getattr(course, image_key) else: - url += '/images/course_image.jpg' - elif not course.course_image: - # if course_image is empty, use the default image url from settings + url += '/images/' + image_key + '.jpg' + elif not getattr(course, image_key): + # if image_key is empty, use the default image url from settings url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL else: - loc = StaticContent.compute_location(course.id, course.course_image) + loc = StaticContent.compute_location(course.id, getattr(course, image_key)) url = StaticContent.serialize_asset_key_with_slash(loc) return url diff --git a/openedx/core/lib/tests/test_courses.py b/openedx/core/lib/tests/test_courses.py index 696f9e503b..45a8b47cdb 100644 --- a/openedx/core/lib/tests/test_courses.py +++ b/openedx/core/lib/tests/test_courses.py @@ -65,3 +65,21 @@ class CourseImageTestCase(ModuleStoreTestCase): 'static/test.png', course_image_url(course), ) + + def test_get_banner_image_url(self): + """Test banner image URL formatting.""" + banner_image = u'banner_image.jpg' + course = CourseFactory.create(banner_image=banner_image) + self.verify_url( + unicode(course.id.make_asset_key('asset', banner_image)), + course_image_url(course, 'banner_image') + ) + + def test_get_video_thumbnail_image_url(self): + """Test video thumbnail image URL formatting.""" + thumbnail_image = u'thumbnail_image.jpg' + course = CourseFactory.create(video_thumbnail_image=thumbnail_image) + self.verify_url( + unicode(course.id.make_asset_key('asset', thumbnail_image)), + course_image_url(course, 'video_thumbnail_image') + )