diff --git a/cms/envs/common.py b/cms/envs/common.py index bfecbffcaadf..95d2036f8107 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2444,6 +2444,7 @@ ############### Settings for django file storage ################## DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +QUALTRICS_SCORE_ID = None ###################### Grade Downloads ###################### # These keys are used for all of our asynchronous downloadable files, including # the ones that contain information other than grades. diff --git a/cms/envs/production.py b/cms/envs/production.py index 8a20a6ba12fd..ff08a3d3ee81 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -11,6 +11,7 @@ import copy import os import warnings +#from os.path import abspath, dirname, join import yaml from corsheaders.defaults import default_headers as corsheaders_default_headers diff --git a/common/djangoapps/student/migrations/0044_auto_20220808_2144.py b/common/djangoapps/student/migrations/0044_auto_20220808_2144.py new file mode 100644 index 000000000000..28147a106c35 --- /dev/null +++ b/common/djangoapps/student/migrations/0044_auto_20220808_2144.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2022-08-08 21:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('student', '0043_remove_userprofile_allow_certificate'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='gender', + field=models.CharField(blank=True, choices=[('m', 'Male'), ('f', 'Female'), ('nbtg', 'Non-binary / third gender'), ('prefer-not-to-say', 'Prefer not to say'), ('o', 'Other')], db_index=True, max_length=25, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='level_of_education', + field=models.CharField(blank=True, choices=[('some-hs', 'Some high school'), ('hs', 'High School or GED'), ('some-college', 'Some college'), ('a', '2-year degree'), ('b', '4-year degree'), ('m', "Master's"), ('p', 'Doctorate'), ('jd-md', 'Professional degree (J.D., M.D.)'), ('prefer-not-to-say', 'Prefer not to say')], db_index=True, max_length=25, null=True), + ), + migrations.AddField( + model_name='courseenrollmentcelebration', + name='celebrate_weekly_goal', + field=models.BooleanField(default=False), + ), + ] diff --git a/common/djangoapps/student/migrations/0044_courseenrollmentcelebration_celebrate_weekly_goal.py b/common/djangoapps/student/migrations/0044_courseenrollmentcelebration_celebrate_weekly_goal.py deleted file mode 100644 index 59ca6827a559..000000000000 --- a/common/djangoapps/student/migrations/0044_courseenrollmentcelebration_celebrate_weekly_goal.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.11 on 2022-01-11 14:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('student', '0043_remove_userprofile_allow_certificate'), - ] - - operations = [ - migrations.AddField( - model_name='courseenrollmentcelebration', - name='celebrate_weekly_goal', - field=models.BooleanField(default=False), - ), - ] diff --git a/common/djangoapps/student/migrations/0045_alter_userprofile_gender.py b/common/djangoapps/student/migrations/0045_alter_userprofile_gender.py new file mode 100644 index 000000000000..3c7c15afeda9 --- /dev/null +++ b/common/djangoapps/student/migrations/0045_alter_userprofile_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-12 17:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('student', '0044_auto_20220808_2144'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='gender', + field=models.CharField(blank=True, choices=[('m', 'Male'), ('f', 'Female'), ('nbtg', 'Non-binary / third gender'), ('prefer-not-to-say', 'Prefer not to say')], db_index=True, max_length=25, null=True), + ), + ] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index b243083f7936..29f19419c1e3 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -530,16 +530,19 @@ class Meta: # Optional demographic data we started capturing from Fall 2012 this_year = datetime.now(UTC).year - VALID_YEARS = list(range(this_year, this_year - 120, -1)) + starting_year = 1923 + VALID_YEARS = list(range(starting_year, this_year + 1)) year_of_birth = models.IntegerField(blank=True, null=True, db_index=True) GENDER_CHOICES = ( ('m', gettext_noop('Male')), ('f', gettext_noop('Female')), + ('nbtg', gettext_noop('Non-binary / third gender')), # Translators: 'Other' refers to the student's gender - ('o', gettext_noop('Other/Prefer Not to Say')) + # ('o', gettext_noop('Self-describe/other')) # TODO - Need to change this option to a freetext input + ('prefer-not-to-say', gettext_noop('Prefer not to say')) ) gender = models.CharField( - blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES + blank=True, null=True, max_length=25, db_index=True, choices=GENDER_CHOICES ) # [03/21/2013] removed these, but leaving comment since there'll still be @@ -547,22 +550,22 @@ class Meta: # ('p_se', 'Doctorate in science or engineering'), # ('p_oth', 'Doctorate in another field'), LEVEL_OF_EDUCATION_CHOICES = ( + ('some-hs', gettext_noop("Some high school")), + ('hs', gettext_noop("High School or GED")), + ('some-college', gettext_noop("Some college")), + ('a', gettext_noop("2-year degree")), + ('b', gettext_noop("4-year degree")), + ('m', gettext_noop("Master's")), ('p', gettext_noop('Doctorate')), - ('m', gettext_noop("Master's or professional degree")), - ('b', gettext_noop("Bachelor's degree")), - ('a', gettext_noop("Associate degree")), - ('hs', gettext_noop("Secondary/high school")), - ('jhs', gettext_noop("Junior secondary/junior high/middle school")), - ('el', gettext_noop("Elementary/primary school")), - # Translators: 'None' refers to the student's level of education - ('none', gettext_noop("No formal education")), - # Translators: 'Other' refers to the student's level of education - ('other', gettext_noop("Other education")) + ('jd-md', gettext_noop("Professional degree (J.D., M.D.)")), + ('prefer-not-to-say', gettext_noop("Prefer not to say")), ) + level_of_education = models.CharField( - blank=True, null=True, max_length=6, db_index=True, + blank=True, null=True, max_length=25, db_index=True, choices=LEVEL_OF_EDUCATION_CHOICES ) + mailing_address = models.TextField(blank=True, null=True) city = models.TextField(blank=True, null=True) country = CountryField(blank=True, null=True) diff --git a/common/djangoapps/student/tests/test_events.py b/common/djangoapps/student/tests/test_events.py index a8b70ea4b889..7a95b772ca8f 100644 --- a/common/djangoapps/student/tests/test_events.py +++ b/common/djangoapps/student/tests/test_events.py @@ -62,11 +62,11 @@ def test_change_many_fields(self): Verify that we emit one event per field when many fields change on the user profile in one transaction. """ - self.profile.gender = 'o' + self.profile.gender = 'prefer-not-to-say' self.profile.bio = 'test bio' self.profile.save() self.assert_user_setting_event_emitted(setting='bio', old=None, new=self.profile.bio) - self.assert_user_setting_event_emitted(setting='gender', old='m', new='o') + self.assert_user_setting_event_emitted(setting='gender', old='m', new='prefer-not-to-say') def test_unicode(self): """ diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index 303dfde58860..0ace51fe76df 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -9,6 +9,7 @@ from django.db import transaction from django.http import Http404, HttpResponse from django.shortcuts import redirect +from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils import translation from django.utils.translation.trans_real import get_supported_language_variant @@ -34,6 +35,14 @@ def index(request): """ Redirects to main page -- info page if user authenticated, or marketing if not """ + # This is to redirect Microsites when a homepage is not necessarily needed. + if not request.user.is_authenticated: + if configuration_helpers.get_value( + 'ALWAYS_REDIRECT_HOMEPAGE_TO_LOGIN_FOR_UNAUTHENTICATED_USER', + settings.FEATURES.get('ALWAYS_REDIRECT_HOMEPAGE_TO_LOGIN_FOR_UNAUTHENTICATED_USER', True) + ): + return redirect(reverse('signin_user')) + if request.user.is_authenticated: # Only redirect to dashboard if user has # courses in their dashboard. Otherwise UX is a bit cryptic. diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index d0863efc14ce..853caab7579a 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -586,6 +586,11 @@ def get_last_accessed_courseware(course, request, user): if SelfPacedConfiguration.current().enable_course_home_improvements: context['resume_course_url'] = get_last_accessed_courseware(course, request, user) + # Redirect to Resume Course url if site configuration flag is enabled. + if context['resume_course_url'] and configuration_helpers.get_value( + 'ENABLE_REDIRECT_TO_RESUME_COURSE_ON_COURSE_INFO', False): + return redirect(context['resume_course_url']) + if not check_course_open_for_learner(user, course): # Disable student view button if user is staff and # course is not yet visible to students. diff --git a/lms/envs/common.py b/lms/envs/common.py index acd86c3e90eb..cd73ca85b0f8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -477,6 +477,11 @@ # .. toggle_tickets: https://github.com/edx/edx-platform/pull/5220 'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True, + # When an non logged in user (Anonymous) goes to the homepage ('/') shoud the user be + # redirected to the dashboard - this is default Microsite behaviour for EducateWorkforce. + # Set to False to not redirect the user. + 'ALWAYS_REDIRECT_HOMEPAGE_TO_LOGIN_FOR_UNAUTHENTICATED_USER': False, + # .. toggle_name: FEATURES['ENABLE_COURSE_SORTING_BY_START_DATE'] # .. toggle_implementation: DjangoSetting # .. toggle_default: True @@ -3601,6 +3606,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # .. toggle_warnings: Review FEATURES['ENABLE_OPENBADGES'] for further context. BADGR_ENABLE_NOTIFICATIONS = False +QUALTRICS_SCORE_ID = None ###################### Grade Downloads ###################### # These keys are used for all of our asynchronous downloadable files, including # the ones that contain information other than grades. diff --git a/lms/envs/production.py b/lms/envs/production.py index 787e295113ac..02a275c27a67 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -20,6 +20,7 @@ import copy import datetime import os +from os.path import abspath, dirname, join import yaml from corsheaders.defaults import default_headers as corsheaders_default_headers @@ -981,6 +982,11 @@ def get_env_setting(setting): # Load production.py in plugins add_plugins(__name__, ProjectType.LMS, SettingsType.PRODUCTION) +##################################################################### +# See if the developer has any local overrides. +if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')): + from .private import * # pylint: disable=import-error,wildcard-import + ############## Settings for Completion API ######################### # Once a user has watched this percentage of a video, mark it as complete: diff --git a/lms/envs/test.py b/lms/envs/test.py index b50f3e5e8a05..eea8ba2927be 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -628,6 +628,10 @@ # enable /api/v1/save/course/ api for testing ENABLE_SAVE_FOR_LATER = True +#################### Network configuration #################### +# Tests are not behind any proxies +CLOSEST_CLIENT_IP_FROM_HEADERS = [] + # rate limit for /api/v1/save/course/ api SAVE_FOR_LATER_IP_RATE_LIMIT = '5/d' diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index f2db31c28e1f..271931eeea27 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -310,14 +310,32 @@ ul.fa-ul{ margin: ($baseline/2) 0 0 0; - &.select-year_of_birth { - @include margin-left(15px); + // &.select-year_of_birth { + // @include margin-left(15px); + // } + + // &.select-gender, + // &.select-year_of_birth { + // display: inline-block; + // width: calc(50% - 10px); + // } + + &.select-level_of_education { + select#register-level_of_education { + height: 60px !important; + } } - &.select-gender, - &.select-year_of_birth { - display: inline-block; - width: calc(50% - 10px); + &.select-ethnicity { + select#register-ethnicity { + height: 60px !important; + } + } + + &.select-local_community_living { + select#register-local_community_living { + height: 80px !important; + } } &.checkbox-marketing_emails_opt_in { diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index ff078119bd12..0080e43bf417 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -756,7 +756,7 @@ def test_patch_account_unknown_user(self, api_client, user): @ddt.data( ("gender", "f", "not a gender", '"not a gender" is not a valid choice.'), - ("level_of_education", "none", "ȻħȺɍłɇs", '"ȻħȺɍłɇs" is not a valid choice.'), + ("level_of_education", "prefer-not-to-say", "ȻħȺɍłɇs", '"ȻħȺɍłɇs" is not a valid choice.'), ("country", "GB", "XY", '"XY" is not a valid choice.'), ("state", "MA", "PY", '"PY" is not a valid choice.'), ("year_of_birth", 2009, "not_an_int", "A valid integer is required."), diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 0321250a3458..7e55197d5f5c 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -609,7 +609,7 @@ def _add_level_of_education_field(self, form_desc, required=True): """ # Translators: This label appears above a dropdown menu on the registration # form used to select the user's highest completed level of education. - education_level_label = _("Highest level of education completed") + education_level_label = _("What is the highest level of education you have completed?") error_msg = accounts.REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG # The labels are marked for translation in UserProfile model definition. @@ -639,7 +639,7 @@ def _add_gender_field(self, form_desc, required=True): """ # Translators: This label appears above a dropdown menu on the registration # form used to select the user's gender. - gender_label = _("Gender") + gender_label = _("How do you describe yourself?") # The labels are marked for translation in UserProfile model definition. # pylint: disable=translation-of-non-string @@ -662,7 +662,7 @@ def _add_year_of_birth_field(self, form_desc, required=True): """ # Translators: This label appears above a dropdown menu on the registration # form used to select the user's year of birth. - yob_label = _("Year of birth") + yob_label = _("What year were you born?") options = [(str(year), str(year)) for year in UserProfile.VALID_YEARS] form_desc.add_field( diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 19b3dc51dc27..154c07ead3ab 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -848,18 +848,23 @@ def test_register_form_level_of_education(self): "name": "level_of_education", "type": "select", "required": False, - "label": "Highest level of education completed", + "label": "What is the highest level of education you have completed?", "options": [ {"value": "", "name": "--", "default": True}, + {"value": "some-hs", "name": "Some high school", "default": False}, + {"value": "hs", "name": "High School or GED", "default": False}, + {"value": "some-college", "name": "Some college", "default": False}, + {"value": "a", "name": "2-year degree", "default": False}, + {"value": "b", "name": "4-year degree", "default": False}, + {"value": "m", "name": "Master's", "default": False}, {"value": "p", "name": "Doctorate", "default": False}, - {"value": "m", "name": "Master's or professional degree", "default": False}, - {"value": "b", "name": "Bachelor's degree", "default": False}, - {"value": "a", "name": "Associate degree", "default": False}, - {"value": "hs", "name": "Secondary/high school", "default": False}, - {"value": "jhs", "name": "Junior secondary/junior high/middle school", "default": False}, - {"value": "el", "name": "Elementary/primary school", "default": False}, - {"value": "none", "name": "No formal education", "default": False}, - {"value": "other", "name": "Other education", "default": False}, + {"value": "jd-md", "name": "Professional degree (J.D., M.D.)", "default": False}, + {"value": "prefer-not-to-say", "name": "Prefer not to say", "default": False}, + # {"value": "hs", "name": "Secondary/high school", "default": False}, + # {"value": "jhs", "name": "Junior secondary/junior high/middle school", "default": False}, + # {"value": "el", "name": "Elementary/primary school", "default": False}, + # {"value": "none", "name": "No formal education", "default": False}, + # {"value": "other", "name": "Other education", "default": False}, ], "errorMessages": { "required": "Select the highest level of education you have completed." @@ -877,18 +882,23 @@ def test_register_form_level_of_education_translations(self, fake_gettext): "name": "level_of_education", "type": "select", "required": False, - "label": "Highest level of education completed TRANSLATED", + "label": "What is the highest level of education you have completed? TRANSLATED", "options": [ {"value": "", "name": "--", "default": True}, + {"value": "some-hs", "name": "Some high school TRANSLATED", "default": False}, + {"value": "hs", "name": "High School or GED TRANSLATED", "default": False}, + {"value": "some-college", "name": "Some college TRANSLATED", "default": False}, + {"value": "a", "name": "2-year degree TRANSLATED", "default": False}, + {"value": "b", "name": "4-year degree TRANSLATED", "default": False}, + {"value": "m", "name": "Master's TRANSLATED", "default": False}, {"value": "p", "name": "Doctorate TRANSLATED", "default": False}, - {"value": "m", "name": "Master's or professional degree TRANSLATED", "default": False}, - {"value": "b", "name": "Bachelor's degree TRANSLATED", "default": False}, - {"value": "a", "name": "Associate degree TRANSLATED", "default": False}, - {"value": "hs", "name": "Secondary/high school TRANSLATED", "default": False}, - {"value": "jhs", "name": "Junior secondary/junior high/middle school TRANSLATED", "default": False}, - {"value": "el", "name": "Elementary/primary school TRANSLATED", "default": False}, - {"value": "none", "name": "No formal education TRANSLATED", "default": False}, - {"value": "other", "name": "Other education TRANSLATED", "default": False}, + {"value": "jd-md", "name": "Professional degree (J.D., M.D.) TRANSLATED", "default": False}, + {"value": "prefer-not-to-say", "name": "Prefer not to say TRANSLATED", "default": False}, + # {"value": "hs", "name": "Secondary/high school TRANSLATED", "default": False}, + # {"value": "jhs", "name": "Junior secondary/junior high/middle school TRANSLATED", "default": False}, # pylint: disable=line-too-long + # {"value": "el", "name": "Elementary/primary school TRANSLATED", "default": False}, + # {"value": "none", "name": "No formal education TRANSLATED", "default": False}, + # {"value": "other", "name": "Other education TRANSLATED", "default": False}, ], "errorMessages": { "required": "Select the highest level of education you have completed." @@ -903,12 +913,14 @@ def test_register_form_gender(self): "name": "gender", "type": "select", "required": False, - "label": "Gender", + "label": "How do you describe yourself?", "options": [ {"value": "", "name": "--", "default": True}, {"value": "m", "name": "Male", "default": False}, {"value": "f", "name": "Female", "default": False}, - {"value": "o", "name": "Other/Prefer Not to Say", "default": False}, + {"value": "nbtg", "name": "Non-binary / third gender", "default": False}, + # {"value": "o", "name": "Self-describe/other", "default": False}, + {"value": "prefer-not-to-say", "name": "Prefer not to say", "default": False}, ], } ) @@ -923,12 +935,14 @@ def test_register_form_gender_translations(self, fake_gettext): "name": "gender", "type": "select", "required": False, - "label": "Gender TRANSLATED", + "label": "How do you describe yourself? TRANSLATED", "options": [ {"value": "", "name": "--", "default": True}, {"value": "m", "name": "Male TRANSLATED", "default": False}, {"value": "f", "name": "Female TRANSLATED", "default": False}, - {"value": "o", "name": "Other/Prefer Not to Say TRANSLATED", "default": False}, + {"value": "nbtg", "name": "Non-binary / third gender TRANSLATED", "default": False}, + # {"value": "o", "name": "Self-describe/other TRANSLATED", "default": False}, + {"value": "prefer-not-to-say", "name": "Prefer not to say TRANSLATED", "default": False}, ], } ) @@ -938,6 +952,7 @@ def test_register_form_year_of_birth(self): # but is failing for you locally, you probably did not cause the problem if you # are not working on registration. See VAN-900 for more details. this_year = datetime.now(UTC).year + starting_year = 1923 year_options = ( [ { @@ -951,7 +966,7 @@ def test_register_form_year_of_birth(self): "name": str(year), "default": False } - for year in range(this_year, this_year - 120, -1) + for year in range(starting_year, this_year + 1) ] ) self._assert_reg_field( @@ -960,7 +975,7 @@ def test_register_form_year_of_birth(self): "name": "year_of_birth", "type": "select", "required": False, - "label": "Year of birth", + "label": "What year were you born?", "options": year_options, } ) diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index e488936d7bc9..7b47f1947d99 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -28,6 +28,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.features.course_duration_limits.access import generate_course_expired_fragment from openedx.features.course_experience import ( COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, @@ -240,5 +241,11 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty 'has_discount': has_discount, 'show_search': show_search, } + + # Redirect to Resume Course url if site configuration flag is enabled. + if context['resume_course_url'] and configuration_helpers.get_value( + 'ENABLE_REDIRECT_TO_RESUME_COURSE_ON_COURSE_INFO', False): + raise CourseAccessRedirect(context['resume_course_url']) + html = render_to_string('course_experience/course-home-fragment.html', context) return Fragment(html)