diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index 5f4f48dcb6..06f7f5384b 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -540,6 +540,7 @@ class CourseAboutSearchIndexer(object): AboutInfo("enrollment_end", AboutInfo.PROPERTY, AboutInfo.FROM_COURSE_PROPERTY), AboutInfo("org", AboutInfo.PROPERTY, AboutInfo.FROM_COURSE_PROPERTY), AboutInfo("modes", AboutInfo.PROPERTY, AboutInfo.FROM_COURSE_MODE), + AboutInfo("language", AboutInfo.PROPERTY, AboutInfo.FROM_COURSE_PROPERTY), ] @classmethod diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b13c341070..d26d393b0a 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1022,6 +1022,14 @@ class ContentStoreTest(ContentStoreTestCase): """Test new course creation - happy path""" self.assert_created_course() + @override_settings(DEFAULT_COURSE_LANGUAGE='hr') + def test_create_course_default_language(self): + """Test new course creation and verify default language""" + test_course_data = self.assert_created_course() + course_id = _get_course_id(self.store, test_course_data) + course_module = self.store.get_course(course_id) + self.assertEquals(course_module.language, 'hr') + def test_create_course_with_dots(self): """Test new course creation with dots in the name""" self.course_data['org'] = 'org.foo.bar' diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index a102083735..a060f21eac 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -50,6 +50,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus)) self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) + self.assertIsNone(details.language, "language somehow initialized" + str(details.language)) def test_encoder(self): details = CourseDetails.fetch(self.course.id) @@ -62,6 +63,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized") self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized") + self.assertIsNone(jsondetails['language'], "language somehow initialized") def test_ooc_encoder(self): """ @@ -116,6 +118,11 @@ class CourseDetailsTestCase(CourseTestCase): CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name, jsondetails.course_image_name ) + jsondetails.language = "hr" + self.assertEqual( + CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language, + jsondetails.language + ) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): @@ -316,6 +323,7 @@ class CourseDetailsViewTest(CourseTestCase): self.alter_field(url, details, 'intro_video', "intro_video") self.alter_field(url, details, 'effort', "effort") self.alter_field(url, details, 'course_image_name', "course_image_name") + self.alter_field(url, details, 'language', "en") def compare_details_with_encoding(self, encoded, details, context): """ @@ -330,6 +338,7 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==") + self.assertEqual(details['language'], encoded['language'], context + " languages not ==") def compare_date_fields(self, details, encoded, context, field): """ diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 87dd6c6c99..4fe48d432c 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -693,6 +693,9 @@ def create_new_course_in_store(store, user, org, number, run, fields): definition_data = {'wiki_slug': wiki_slug} fields.update(definition_data) + # Set default language from settings + fields.update({'language': getattr(settings, 'DEFAULT_COURSE_LANGUAGE', 'en')}) + with modulestore().default_store(store): # Creating the course raises DuplicateCourseError if an existing course with this org/name is found new_course = modulestore().create_course( @@ -868,6 +871,7 @@ def settings_handler(request, course_key_string): 'short_description_editable': short_description_editable, 'upload_asset_url': upload_asset_url, 'course_handler_url': reverse_course_url('course_handler', course_key), + 'language_options': settings.ALL_LANGUAGES, } if prerequisite_course_enabled: courses, in_process_course_actions = get_courses_accessible_to_user(request) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index e2f8119841..0b81444ab5 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -33,6 +33,7 @@ class CourseDetails(object): self.org = org self.course_id = course_id self.run = run + self.language = None self.start_date = None # 'start' self.end_date = None # 'end' self.enrollment_start = None @@ -80,6 +81,7 @@ class CourseDetails(object): 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.language = descriptor.language # Default course license is "All Rights Reserved" course_details.license = getattr(descriptor, "license", "all-rights-reserved") @@ -180,6 +182,10 @@ class CourseDetails(object): descriptor.license = jsondict['license'] dirty = True + if 'language' in jsondict and jsondict['language'] != descriptor.language: + descriptor.language = jsondict['language'] + dirty = True + if dirty: module_store.update_item(descriptor, user.id) diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 2f22ba400f..e5f049e150 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -43,6 +43,7 @@ class CourseMetadata(object): 'entrance_exam_id', 'is_entrance_exam', 'in_entrance_exam', + 'language', ] @classmethod diff --git a/cms/envs/common.py b/cms/envs/common.py index 39a3008c30..87450f3f75 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -874,6 +874,10 @@ MAX_ASSET_UPLOAD_FILE_SIZE_URL = "" ### Default value for entrance exam minimum score ENTRANCE_EXAM_MIN_SCORE_PCT = 50 +### Default language for a new course +DEFAULT_COURSE_LANGUAGE = "en" + + ################ ADVANCED_COMPONENT_TYPES ############### ADVANCED_COMPONENT_TYPES = [ diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 6f8487697e..a9f70eac7e 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -6,6 +6,7 @@ var CourseDetails = Backbone.Model.extend({ org : '', course_id: '', run: '', + language: '', start_date: null, // maps to 'start' end_date: null, // maps to 'end' enrollment_start: null, diff --git a/cms/static/js/spec/views/settings/main_spec.js b/cms/static/js/spec/views/settings/main_spec.js index f489732eab..e691c12061 100644 --- a/cms/static/js/spec/views/settings/main_spec.js +++ b/cms/static/js/spec/views/settings/main_spec.js @@ -30,7 +30,8 @@ define([ pre_requisite_courses : [], entrance_exam_enabled : '', entrance_exam_minimum_score_pct: '50', - license: null + license: null, + language: '' }, mockSettingsPage = readFixtures('mock/mock-settings-page.underscore'); @@ -154,5 +155,28 @@ define([ ); AjaxHelpers.respondWithJson(requests, expectedJson); }); + + it('should save language as part of course details', function(){ + var requests = AjaxHelpers.requests(this); + var expectedJson = $.extend(true, {}, modelData, { + language: 'en', + }); + $('#course-language').val('en').trigger('change'); + expect(this.model.get('language')).toEqual('en'); + this.view.saveView(); + AjaxHelpers.expectJsonRequest( + requests, 'POST', urlRoot, expectedJson + ); + }); + + it('should not error if about_page_editable is False', function(){ + var requests = AjaxHelpers.requests(this); + // if about_page_editable is false, there is no section.course_details + $('.course_details').remove(); + expect(this.model.get('language')).toEqual(''); + this.view.saveView(); + AjaxHelpers.expectJsonRequest(requests, 'POST', urlRoot, modelData); + }); + }); }); diff --git a/cms/static/js/views/settings/main.js b/cms/static/js/views/settings/main.js index 2a66802c94..78b5dbc491 100644 --- a/cms/static/js/views/settings/main.js +++ b/cms/static/js/views/settings/main.js @@ -24,6 +24,7 @@ var DetailsView = ValidatingView.extend({ initialize : function() { this.fileAnchorTemplate = _.template(' <%= filename %>'); // fill in fields + this.$el.find("#course-language").val(this.model.get('language')); this.$el.find("#course-organization").val(this.model.get('org')); this.$el.find("#course-number").val(this.model.get('course_id')); this.$el.find("#course-name").val(this.model.get('run')); @@ -93,6 +94,7 @@ var DetailsView = ValidatingView.extend({ return this; }, fieldToSelectorMap : { + 'language' : 'course-language', 'start_date' : "course-start", 'end_date' : 'course-end', 'enrollment_start' : 'enrollment-start', @@ -166,6 +168,9 @@ var DetailsView = ValidatingView.extend({ updateModel: function(event) { switch (event.currentTarget.id) { + case 'course-language': + this.setField(event); + break; case 'course-image-url': this.setField(event); var url = $(event.currentTarget).val(); diff --git a/cms/templates/js/mock/mock-settings-page.underscore b/cms/templates/js/mock/mock-settings-page.underscore index 41a00463b0..1422279fb9 100644 --- a/cms/templates/js/mock/mock-settings-page.underscore +++ b/cms/templates/js/mock/mock-settings-page.underscore @@ -90,4 +90,21 @@ + +
+
+

Course Details

+ Provide useful information about your course +
+
    +
  1. + + + Identify the course language here. This is used to assist users find courses that are taught in a specific language. +
  2. +
+
diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 21f41289b6..937e174bb1 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -199,6 +199,28 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; % endif + + % if about_page_editable: +
+
+

${_('Course Details')}

+ ${_('Provide useful information about your course')} +
+
    +
  1. + + + ${_("Identify the course language here. This is used to assist users find courses that are taught in a specific language.")} +
  2. +
+
+ % endif +
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 89eba5d50e..8a86989541 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -23,6 +23,7 @@ from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float from .fields import Date from django.utils.timezone import UTC + log = logging.getLogger(__name__) # Make '_' a no-op so we can scrape strings @@ -851,6 +852,12 @@ class CourseFields(object): default=None, scope=Scope.settings, ) + language = String( + display_name=_("Course Language"), + help=_("Specify the language of your course."), + default=None, + scope=Scope.settings + ) teams_configuration = Dict( display_name=_("Teams Configuration"),