From f3e94a97b3ae21c5a3d2f0a84295d67c1283f883 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 18 Jan 2019 16:19:14 -0500 Subject: [PATCH 001/119] Set RELEASE_LINE to 'ironwood' --- openedx/core/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/release.py b/openedx/core/release.py index 409f8aa6d919..5ca3f7f44df4 100644 --- a/openedx/core/release.py +++ b/openedx/core/release.py @@ -5,7 +5,7 @@ # The release line: an Open edX release name ("ficus"), or "master". # This should always be "master" on the master branch, and will be changed # manually when we start release-line branches, like open-release/ficus.master. -RELEASE_LINE = "master" +RELEASE_LINE = "ironwood" def doc_version(): From 89639e70d539a03d5b6dc7116f5cb64e6dcad12b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 18 Jan 2019 16:20:20 -0500 Subject: [PATCH 002/119] Ironwood translation resources --- .tx/config | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.tx/config b/.tx/config index 80921b483b6e..66f15a4c093f 100644 --- a/.tx/config +++ b/.tx/config @@ -54,3 +54,15 @@ file_filter = conf/locale//LC_MESSAGES/wiki.po source_file = conf/locale/en/LC_MESSAGES/wiki.po source_lang = en type = PO + +[open-edx-releases.release-ironwood] +file_filter = conf/locale//LC_MESSAGES/django.po +source_file = conf/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO + +[open-edx-releases.release-ironwood-js] +file_filter = conf/locale//LC_MESSAGES/djangojs.po +source_file = conf/locale/en/LC_MESSAGES/djangojs.po +source_lang = en +type = PO From 0f287f731697565ee88a148f7b2ea12861723c4c Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Sun, 20 Jan 2019 18:05:27 -0500 Subject: [PATCH 003/119] Revert "change banner date localization to use dateutilfactory" (cherry picked from commit 08cb56664b7101b806a3f303d2910d8bb1e42276) --- lms/djangoapps/courseware/tests/helpers.py | 22 +++--- .../features/course_duration_limits/access.py | 31 ++++---- .../tests/test_access.py | 71 ++++++++++--------- 3 files changed, 67 insertions(+), 57 deletions(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 0ac589cddfba..e8472a01a54e 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -355,7 +355,7 @@ def _create_mock_json_request(user, data, method='POST'): return request -def get_expiration_banner_text(user, course, language='en'): +def get_expiration_banner_text(user, course, language='en-us'): """ Get text for banner that messages user course expiration date for different tests that depend on it. @@ -367,17 +367,17 @@ def get_expiration_banner_text(user, course, language='en'): if upgrade_deadline is None or now() < upgrade_deadline: upgrade_deadline = enrollment.course_upgrade_deadline - date_string = '{formatted_date}' - formatted_expiration_date = date_string.format( - language=language, - formatted_date=strftime_localized(expiration_date, '%b. %-d, %Y') - ) + language_is_es = language and language.split('-')[0].lower() == 'es' + if language_is_es: + formatted_expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() + else: + formatted_expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') + if upgrade_deadline: - formatted_upgrade_deadline = date_string.format( - language=language, - formatted_date=strftime_localized(upgrade_deadline, '%b. %-d, %Y') - ) + if language_is_es: + formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%-d de %b. de %Y').lower() + else: + formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%b. %-d, %Y') bannerText = 'Audit Access Expires {expiration_date}
\ You lose all access to this course, including your progress, on {expiration_date}.\ diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index 15acb6a68d79..a024475a23cd 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -34,7 +34,10 @@ def __init__(self, user, course, expiration_date): error_code = "audit_expired" developer_message = "User {} had access to {} until {}".format(user, course, expiration_date) language = get_language() - expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') + if language and language.split('-')[0].lower() == 'es': + expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() + else: + expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') user_message = _("Access expired on {expiration_date}").format(expiration_date=expiration_date) try: course_name = CourseOverview.get_from_id(course.id).display_name_with_default @@ -151,17 +154,17 @@ def generate_course_expired_message(user, course): using_upgrade_messaging = False language = get_language() - date_string = '{formatted_date}' - formatted_expiration_date = date_string.format( - language=language, - formatted_date=strftime_localized(expiration_date, '%b. %-d, %Y') - ) + language_is_es = language and language.split('-')[0].lower() == 'es' + if language_is_es: + formatted_expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() + else: + formatted_expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') + if using_upgrade_messaging: - formatted_upgrade_deadline = date_string.format( - language=language, - formatted_date=strftime_localized(upgrade_deadline, '%b. %-d, %Y') - ) + if language_is_es: + formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%-d de %b. de %Y').lower() + else: + formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%b. %-d, %Y') return HTML(full_message).format( a_open=HTML('').format( @@ -170,17 +173,17 @@ def generate_course_expired_message(user, course): sronly_span_open=HTML(''), span_close=HTML(''), a_close=HTML(''), - expiration_date=HTML(formatted_expiration_date), + expiration_date=formatted_expiration_date, strong_open=HTML(''), strong_close=HTML(''), line_break=HTML('
'), - upgrade_deadline=HTML(formatted_upgrade_deadline) + upgrade_deadline=formatted_upgrade_deadline ) else: return HTML(full_message).format( span_close=HTML(''), - expiration_date=HTML(formatted_expiration_date), + expiration_date=formatted_expiration_date, strong_open=HTML(''), strong_close=HTML(''), line_break=HTML('
'), diff --git a/openedx/features/course_duration_limits/tests/test_access.py b/openedx/features/course_duration_limits/tests/test_access.py index a50d36bf537c..5ae99f30d474 100644 --- a/openedx/features/course_duration_limits/tests/test_access.py +++ b/openedx/features/course_duration_limits/tests/test_access.py @@ -8,6 +8,7 @@ from django.test import RequestFactory from django.utils import timezone from courseware.models import DynamicUpgradeDeadlineConfiguration +from mock import patch from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.features.course_duration_limits.access import ( @@ -32,11 +33,12 @@ def setUp(self): @ddt.data( *itertools.product( + ['en-us', 'es-419'], itertools.product([None, -2, -1, 1, 2], repeat=2), ) ) @ddt.unpack - def test_generate_course_expired_message(self, offsets): + def test_generate_course_expired_message(self, language, offsets): now = timezone.now() schedule_offset, course_offset = offsets @@ -51,40 +53,45 @@ def test_generate_course_expired_message(self, offsets): course_upgrade_deadline = None def format_date(date): - return strftime_localized(date, '%b. %-d, %Y') + if language.startswith('es-'): + return strftime_localized(date, '%-d de %b. de %Y').lower() + else: + return strftime_localized(date, '%b. %-d, %Y') - enrollment = CourseEnrollmentFactory.create( - course__start=datetime(2018, 1, 1, tzinfo=UTC), - course__self_paced=True, - ) - CourseModeFactory.create( - course_id=enrollment.course.id, - mode_slug=CourseMode.VERIFIED, - expiration_datetime=course_upgrade_deadline, - ) - CourseModeFactory.create( - course_id=enrollment.course.id, - mode_slug=CourseMode.AUDIT, - ) - ScheduleFactory.create( - enrollment=enrollment, - upgrade_deadline=schedule_upgrade_deadline, - ) + patch_lang = patch('openedx.features.course_duration_limits.access.get_language', return_value=language) + with patch_lang: + enrollment = CourseEnrollmentFactory.create( + course__start=datetime(2018, 1, 1, tzinfo=UTC), + course__self_paced=True, + ) + CourseModeFactory.create( + course_id=enrollment.course.id, + mode_slug=CourseMode.VERIFIED, + expiration_datetime=course_upgrade_deadline, + ) + CourseModeFactory.create( + course_id=enrollment.course.id, + mode_slug=CourseMode.AUDIT, + ) + ScheduleFactory.create( + enrollment=enrollment, + upgrade_deadline=schedule_upgrade_deadline, + ) - duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course) - self.assertIsNotNone(duration_limit_upgrade_deadline) + duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course) + self.assertIsNotNone(duration_limit_upgrade_deadline) - message = generate_course_expired_message(enrollment.user, enrollment.course) + message = generate_course_expired_message(enrollment.user, enrollment.course) - self.assertIn(format_date(duration_limit_upgrade_deadline), message) + self.assertIn(format_date(duration_limit_upgrade_deadline), message) - soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline - upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline - has_upgrade_deadline = course_upgrade_deadline is not None + soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline + upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline + has_upgrade_deadline = course_upgrade_deadline is not None - if upgradeable and soft_upgradeable: - self.assertIn(format_date(schedule_upgrade_deadline), message) - elif upgradeable and has_upgrade_deadline: - self.assertIn(format_date(course_upgrade_deadline), message) - else: - self.assertNotIn("Upgrade by", message) + if upgradeable and soft_upgradeable: + self.assertIn(format_date(schedule_upgrade_deadline), message) + elif upgradeable and has_upgrade_deadline: + self.assertIn(format_date(course_upgrade_deadline), message) + else: + self.assertNotIn("Upgrade by", message) From 1c1403027d4ee7c1ac67ce236c5060479fe3cf77 Mon Sep 17 00:00:00 2001 From: Brittney Exline Date: Tue, 22 Jan 2019 11:02:58 -0700 Subject: [PATCH 004/119] ENT-1467 Version bump for edx-enterprise to 1.2.8 (cherry picked from commit e4905a51f348abc350dcf2fa169007d969702c0c) --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 83f7600b1a17..e26fc977d3c0 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -117,7 +117,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.5 +edx-enterprise==1.2.8 edx-i18n-tools==0.4.6 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 569f291ee6c6..ff68b336f64f 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -135,7 +135,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.5 +edx-enterprise==1.2.8 edx-i18n-tools==0.4.6 edx-lint==1.0.0 edx-milestones==0.1.13 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index dba40d147db7..aafd60b31cb0 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -130,7 +130,7 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.5 +edx-enterprise==1.2.8 edx-i18n-tools==0.4.6 edx-lint==1.0.0 edx-milestones==0.1.13 From 2d4eed740bfd53fe90e074ec09385937f2f002f5 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Tue, 22 Jan 2019 12:47:40 -0500 Subject: [PATCH 005/119] make upgrade to version of edx-proctoring without blocking multiple concurrent sessions also updates trace amounts of copy duplicated in tests between edx-proctoring and edx-platform JIRA:EDUCATOR-3931 (cherry picked from commit 673fb797b811f2398cbec5dd5a62937f8438dbae) --- lms/djangoapps/courseware/tests/test_module_render.py | 4 ++-- requirements/edx/base.txt | 4 ++-- requirements/edx/development.txt | 4 ++-- requirements/edx/testing.txt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 931ef1014160..5f2225623bb3 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1267,14 +1267,14 @@ def test_proctored_exam_toc(self, enrollment_mode, is_practice_exam, CourseMode.VERIFIED, False, 'verified', - 'Your proctoring session was reviewed and passed all requirements', + 'Your proctoring session was reviewed successfully', False ), ( CourseMode.VERIFIED, False, 'rejected', - 'Your proctoring session was reviewed and did not pass requirements', + 'Your proctoring session was reviewed, but did not pass all requirements', True ), ( diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index e26fc977d3c0..574e877ff1b4 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -123,8 +123,8 @@ edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring-proctortrack==1.0.0 -edx-proctoring==1.5.6 +edx-proctoring-proctortrack==1.0.1 +edx-proctoring==1.5.7 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-submissions==2.0.12 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index ff68b336f64f..ce92b8abea5e 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -142,8 +142,8 @@ edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring-proctortrack==1.0.0 -edx-proctoring==1.5.6 +edx-proctoring-proctortrack==1.0.1 +edx-proctoring==1.5.7 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-sphinx-theme==1.4.0 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index aafd60b31cb0..ec0a427fceff 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -137,8 +137,8 @@ edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring-proctortrack==1.0.0 -edx-proctoring==1.5.6 +edx-proctoring-proctortrack==1.0.1 +edx-proctoring==1.5.7 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-submissions==2.0.12 From cf02924b85653ffe3516aef704cc3a44bc702260 Mon Sep 17 00:00:00 2001 From: Jeremy Bowman Date: Fri, 18 Jan 2019 14:03:54 -0500 Subject: [PATCH 006/119] Fix coverage on remote xdist nodes (cherry picked from commit f7799bad9ebe45f95fde20c9f2d895c77bc700a4) --- .coveragerc | 2 -- .../xmodule/partitions/tests/test_partitions.py | 4 ++-- scripts/Jenkinsfiles/python | 10 +++++++++- scripts/xdist/prepare_xdist_nodes.sh | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index a15cc0742105..2b417556204e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -49,6 +49,4 @@ output = reports/coverage.xml jenkins_source = /home/jenkins/workspace/$JOB_NAME /home/jenkins/workspace/$SUBSET_JOB - -devstack_source = /edx/app/edxapp/edx-platform diff --git a/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py b/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py index c1def1cbedb3..4eedea13229c 100644 --- a/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py +++ b/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py @@ -476,12 +476,12 @@ def test_get_user_group_id_for_partition(self): # get a group assigned to the user group1_id = self.partition_service.get_user_group_id_for_partition(self.user, user_partition_id) - self.assertEqual(group1_id, groups[0].id) + assert group1_id == groups[0].id # switch to the second group and verify that it is returned for the user self.user_partition.scheme.current_group = groups[1] group2_id = self.partition_service.get_user_group_id_for_partition(self.user, user_partition_id) - self.assertEqual(group2_id, groups[1].id) + assert group2_id == groups[1].id def test_caching(self): username = "psvc_cache_user" diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index 8a8f7c73ae65..a20d2183bd33 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -27,6 +27,14 @@ def pythonTestCleanup() { bash scripts/xdist/terminate_xdist_nodes.sh''' } +def xdist_git_branch() { + if (env.ghprbActualCommit) { + return "${ghprbActualCommit}" + } else { + return "${BRANCH_NAME}" + } +} + pipeline { agent { label "jenkins-worker" } options { @@ -37,7 +45,7 @@ pipeline { XDIST_CONTAINER_SUBNET = credentials('XDIST_CONTAINER_SUBNET') XDIST_CONTAINER_SECURITY_GROUP = credentials('XDIST_CONTAINER_SECURITY_GROUP') XDIST_CONTAINER_TASK_NAME = "jenkins-worker-task" - XDIST_GIT_BRANCH = "${ghprbActualCommit}" + XDIST_GIT_BRANCH = xdist_git_branch() } stages { stage('Mark build as pending on Github') { diff --git a/scripts/xdist/prepare_xdist_nodes.sh b/scripts/xdist/prepare_xdist_nodes.sh index 9f21424762e4..a647272d055d 100644 --- a/scripts/xdist/prepare_xdist_nodes.sh +++ b/scripts/xdist/prepare_xdist_nodes.sh @@ -13,7 +13,7 @@ do container_reqs_cmd="ssh -o StrictHostKeyChecking=no ubuntu@$ip 'cd /edx/app/edxapp; git clone --branch master --depth 1 --no-tags -q https://github.com/edx/edx-platform.git; cd edx-platform; git fetch --depth=1 --no-tags -q origin ${XDIST_GIT_BRANCH}; git checkout -q ${XDIST_GIT_BRANCH}; - source /edx/app/edxapp/edxapp_env; pip install -qr requirements/edx/testing.txt' & " + source /edx/app/edxapp/edxapp_env; pip install -qr requirements/edx/testing.txt; mkdir reports' & " cmd=$cmd$container_reqs_cmd done From bf3f5e333c1f4ae7835f5f13ebdc861d3d648012 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 28 Jan 2019 15:59:04 -0500 Subject: [PATCH 007/119] Don't fail if the JWK settings aren't set JWKs are used for micro-frontends and OAuth scopes. The Ironwood installation process doesn't yet create the JWKs needed. For Ironwood, at least don't fail trying to use the empty settings. By default, micro-frontends and OAuth scopes will be unavailable. --- openedx/core/djangoapps/user_authn/cookies.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openedx/core/djangoapps/user_authn/cookies.py b/openedx/core/djangoapps/user_authn/cookies.py index 865994281f6a..d36d299a3d00 100644 --- a/openedx/core/djangoapps/user_authn/cookies.py +++ b/openedx/core/djangoapps/user_authn/cookies.py @@ -259,6 +259,14 @@ def _create_and_set_jwt_cookies(response, request, cookie_settings, user=None, r if settings.FEATURES.get('DISABLE_SET_JWT_COOKIES_FOR_TESTS', False): return + # For Ironwood, we don't set JWK settings by default. Make sure we don't fail trying + # to use empty settings. This means by default, micro-frontends won't work, but Ironwood + # has none. Also, OAuth scopes won't work, but that is still a new and specialized feature. + # Installations that need them can create JWKs and add them to the settings. + private_signing_jwk = settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'] + if private_signing_jwk == "None" or not private_signing_jwk: + return + # For security reasons, the JWT that is embedded inside the cookie expires # much sooner than the cookie itself, per the following setting. expires_in = settings.JWT_AUTH['JWT_IN_COOKIE_EXPIRATION'] From 9dca19dd524ad3b1a0f866a9ec2391790f324ce2 Mon Sep 17 00:00:00 2001 From: Mahyar Damavand Date: Sun, 20 Jan 2019 16:01:46 +0330 Subject: [PATCH 008/119] tiny style modification (cherry picked from commit 84f609a2698fa3b5518f2a663ce5dbf981b603d3) --- lms/static/sass/_header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/_header.scss b/lms/static/sass/_header.scss index 8f3e6a0bb160..f3f15e5367b3 100644 --- a/lms/static/sass/_header.scss +++ b/lms/static/sass/_header.scss @@ -209,7 +209,7 @@ position: absolute; background-color: theme-color("inverse"); color: theme-color("secondary"); - right: 30px; + @include right(30px); top: 55px; z-index: 10; From 84cd0241f463067da953b29b6af80401a27451b9 Mon Sep 17 00:00:00 2001 From: Luis Moreno Date: Fri, 25 Jan 2019 15:15:07 -0400 Subject: [PATCH 009/119] Fix Collapse All/Expand All translation (cherry picked from commit 91ed45099b17174d8ad182ae9d0a87e280b38913) --- .../static/course_experience/js/CourseOutline.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js index 6a6a40cdb6b1..ca46563090ab 100644 --- a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js +++ b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js @@ -78,16 +78,17 @@ export class CourseOutline { // eslint-disable-line import/prefer-default-expor toggleAllButton.addEventListener('click', (event) => { const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true'; let sectionAction; + /* globals gettext */ if (toggleAllExpanded) { toggleAllButton.setAttribute('aria-expanded', 'false'); sectionAction = collapseSection; toggleAllSpan.classList.add(extraPaddingClass); - toggleAllSpan.innerText = 'Expand All'; + toggleAllSpan.innerText = gettext('Expand All'); } else { toggleAllButton.setAttribute('aria-expanded', 'true'); sectionAction = expandSection; toggleAllSpan.classList.remove(extraPaddingClass); - toggleAllSpan.innerText = 'Collapse All'; + toggleAllSpan.innerText = gettext('Collapse All'); } const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger')); sections.forEach((sectionToggleButton) => { From f000c15eab34c7dbf36aaa2f0b94e9d9fd8da688 Mon Sep 17 00:00:00 2001 From: Julia Eskew Date: Mon, 11 Feb 2019 11:08:56 -0500 Subject: [PATCH 010/119] Update Django version to 1.11.20 (cherry picked from commit 44ca4d48aecf0e23858cf68f6faa03495e23def3) --- requirements/edx/base.in | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/django.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.in b/requirements/edx/base.in index ba5e0acf7f07..d34e4668c996 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -35,7 +35,7 @@ boto3==1.4.8 # Amazon Web Services SDK for Python botocore==1.8.17 # via boto3, s3transfer celery==3.1.25 # Asynchronous task execution library defusedxml==0.4.1 # XML bomb protection for common XML parsers -Django==1.11.18 # Web application framework +Django==1.11.20 # Web application framework django-babel-underscore # underscore template extractor for django-babel (internationalization utilities) django-config-models>=0.2.2 # Configuration models for Django allowing config management with auditing django-cors-headers==2.1.0 # Used to allow to configure CORS headers for cross-domain requests diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 574e877ff1b4..5ee64a5c51c3 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -100,7 +100,7 @@ django-storages==1.4.1 django-user-tasks==0.1.5 django-waffle==0.12.0 django-webpack-loader==0.6.0 -django==1.11.18 +django==1.11.20 djangorestframework-jwt==1.11.0 djangorestframework-xml==1.3.0 # via edx-enterprise dm.xmlsec.binding==1.3.3 # via python-saml diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index ce92b8abea5e..7c7ba8c0853d 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -118,7 +118,7 @@ django-storages==1.4.1 django-user-tasks==0.1.5 django-waffle==0.12.0 django-webpack-loader==0.6.0 -django==1.11.18 +django==1.11.20 djangorestframework-jwt==1.11.0 djangorestframework-xml==1.3.0 dm.xmlsec.binding==1.3.3 diff --git a/requirements/edx/django.txt b/requirements/edx/django.txt index f5a633f3946d..86583ad88484 100644 --- a/requirements/edx/django.txt +++ b/requirements/edx/django.txt @@ -1 +1 @@ -django==1.11.18 +django==1.11.20 From e4dfb790f0163fccd4be005bb4e9dc0fde8c58b8 Mon Sep 17 00:00:00 2001 From: DawoudSheraz Date: Mon, 18 Feb 2019 17:20:04 +0500 Subject: [PATCH 011/119] update django wiki for ironwood release --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/github.in | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 5ee64a5c51c3..1c5354195a94 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -15,7 +15,7 @@ git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f3 git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1 git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1 git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3 --e git+https://github.com/edx/django-wiki.git@v0.0.20#egg=django-wiki +-e git+https://github.com/edx/django-wiki.git@v0.0.21#egg=django-wiki git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 -e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 7c7ba8c0853d..3f954ac785e4 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -17,7 +17,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1 git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1 git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3 --e git+https://github.com/edx/django-wiki.git@v0.0.20#egg=django-wiki +-e git+https://github.com/edx/django-wiki.git@v0.0.21#egg=django-wiki git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 -e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock diff --git a/requirements/edx/github.in b/requirements/edx/github.in index 0c00c6036bad..4f917bdf63a9 100644 --- a/requirements/edx/github.in +++ b/requirements/edx/github.in @@ -62,7 +62,7 @@ # Third-party: -e git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3 --e git+https://github.com/edx/django-wiki.git@v0.0.20#egg=django-wiki +-e git+https://github.com/edx/django-wiki.git@v0.0.21#egg=django-wiki -e git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1 -e git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0 -e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index ec0a427fceff..94487ac08071 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -15,7 +15,7 @@ git+https://github.com/edx/django-celery.git@756cb57aad765cb2b0d37372c1855b8f5f3 git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1 git+https://github.com/edx/django-openid-auth.git@0.15.1#egg=django-openid-auth==0.15.1 git+https://github.com/jazzband/django-pipeline.git@d068a019169c9de5ee20ece041a6dea236422852#egg=django-pipeline==1.5.3 --e git+https://github.com/edx/django-wiki.git@v0.0.20#egg=django-wiki +-e git+https://github.com/edx/django-wiki.git@v0.0.21#egg=django-wiki git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 -e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock From bc2b1b1e53619ba538f65277d223c73185892b5f Mon Sep 17 00:00:00 2001 From: Daniel Clemente Laboreo Date: Mon, 15 Oct 2018 16:57:01 +0300 Subject: [PATCH 012/119] Fix error when saving CourseEnrollment in admin (cherry picked from commit e0b0c375bed6d783b2df7e2dfd53ecfc7dc936d0) --- common/djangoapps/course_modes/admin.py | 2 +- common/djangoapps/student/admin.py | 18 +++++++- .../student/tests/test_admin_views.py | 46 ++++++++++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py index d738dd3673d7..38e6cda82376 100644 --- a/common/djangoapps/course_modes/admin.py +++ b/common/djangoapps/course_modes/admin.py @@ -54,7 +54,7 @@ class Meta(object): def __init__(self, *args, **kwargs): # If args is a QueryDict, then the ModelForm addition request came in as a POST with a course ID string. # Change the course ID string to a CourseLocator object by copying the QueryDict to make it mutable. - if len(args) > 0 and 'course' in args[0] and isinstance(args[0], QueryDict): + if args and 'course' in args[0] and isinstance(args[0], QueryDict): args_copy = args[0].copy() args_copy['course'] = CourseKey.from_string(args_copy['course']) args = [args_copy] diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 2b23bef96849..716c9eb89b17 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -7,6 +7,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.forms import ReadOnlyPasswordHashField, UserChangeForm as BaseUserChangeForm from django.db import models +from django.http.request import QueryDict from django.utils.translation import ugettext_lazy as _ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -150,13 +151,26 @@ class Meta(object): class CourseEnrollmentForm(forms.ModelForm): def __init__(self, *args, **kwargs): + # If args is a QueryDict, then the ModelForm addition request came in as a POST with a course ID string. + # Change the course ID string to a CourseLocator object by copying the QueryDict to make it mutable. + if args and 'course' in args[0] and isinstance(args[0], QueryDict): + args_copy = args[0].copy() + try: + args_copy['course'] = CourseKey.from_string(args_copy['course']) + except InvalidKeyError: + raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(args_copy['course'])) + args = [args_copy] + super(CourseEnrollmentForm, self).__init__(*args, **kwargs) if self.data.get('course'): try: self.data['course'] = CourseKey.from_string(self.data['course']) - except InvalidKeyError: - raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(self.data['course'])) + except AttributeError: + # Change the course ID string to a CourseLocator. + # On a POST request, self.data is a QueryDict and is immutable - so this code will fail. + # However, the args copy above before the super() call handles this case. + pass def clean_course_id(self): course_id = self.cleaned_data['course'] diff --git a/common/djangoapps/student/tests/test_admin_views.py b/common/djangoapps/student/tests/test_admin_views.py index ef5d2cb882c3..e60170f8e239 100644 --- a/common/djangoapps/student/tests/test_admin_views.py +++ b/common/djangoapps/student/tests/test_admin_views.py @@ -4,6 +4,7 @@ import ddt from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import User +from django.forms import ValidationError from django.urls import reverse from django.test import TestCase from mock import Mock @@ -209,7 +210,7 @@ def setUp(self): super(CourseEnrollmentAdminTest, self).setUp() self.user = UserFactory.create(is_staff=True, is_superuser=True) self.course = CourseFactory() - CourseEnrollmentFactory( + self.course_enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, # pylint: disable=no-member ) @@ -254,3 +255,46 @@ def test_username_exact_match(self): # Locate the column containing the username user_field = next(col for col in response.context['results'][idx] if "field-user" in col) self.assertIn(username, user_field) + + def test_save_toggle_active(self): + """ + Edit a CourseEnrollment to toggle its is_active checkbox, save it and verify that it was toggled. + When the form is saved, Django uses a QueryDict object which is immutable and needs special treatment. + This test implicitly verifies that the POST parameters are handled correctly. + """ + # is_active will change from True to False + self.assertTrue(self.course_enrollment.is_active) + data = { + 'user': unicode(self.course_enrollment.user.id), + 'course': unicode(self.course_enrollment.course.id), + 'is_active': 'false', + 'mode': self.course_enrollment.mode, + } + + with COURSE_ENROLLMENT_ADMIN_SWITCH.override(active=True): + response = self.client.post( + reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )), + data=data, + ) + self.assertEqual(response.status_code, 302) + + self.course_enrollment.refresh_from_db() + self.assertFalse(self.course_enrollment.is_active) + + def test_save_invalid_course_id(self): + """ + Send an invalid course ID instead of "org.0/course_0/Run_0" when saving, and verify that it fails. + """ + data = { + 'user': unicode(self.course_enrollment.user.id), + 'course': 'invalid-course-id', + 'is_active': 'true', + 'mode': self.course_enrollment.mode, + } + + with COURSE_ENROLLMENT_ADMIN_SWITCH.override(active=True): + with self.assertRaises(ValidationError): + self.client.post( + reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )), + data=data, + ) From 3e085ea3df9c152de0da752e1af2693c54c30459 Mon Sep 17 00:00:00 2001 From: rabiaiftikhar Date: Wed, 23 Jan 2019 17:15:25 +0500 Subject: [PATCH 013/119] EDUCATOR-3930 fix video player speed adjustments (cherry picked from commit a85256e10915803c25cf9a470de2d43603afd1de) --- common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js | 4 ++-- common/lib/xmodule/xmodule/js/src/video/03_video_player.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index b18208f18380..429cdb8247af 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -904,9 +904,9 @@ function(VideoPlayer, HLS, _) { it('set video speed to the new speed', function() { VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.setSpeed).toHaveBeenCalledWith('0.75'); + expect(state.setSpeed).toHaveBeenCalledWith(0.75); expect(state.videoPlayer.setPlaybackRate) - .toHaveBeenCalledWith('0.75'); + .toHaveBeenCalledWith(0.75); }); }); }); diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 8a625723a5c8..3894a28632c3 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -427,7 +427,7 @@ function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) { ); } - newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0'); + newSpeed = parseFloat(newSpeed); this.setSpeed(newSpeed); this.videoPlayer.setPlaybackRate(newSpeed); } From bb13168ddb672d0a381199621b9d49c29646f8ce Mon Sep 17 00:00:00 2001 From: Luis Moreno Date: Tue, 19 Feb 2019 20:38:32 -0400 Subject: [PATCH 014/119] Fix date format (cherry picked from commit 12931ae44200c2f571cf5641440aa1c984b128d8) --- lms/templates/course.html | 4 ++-- lms/templates/courseware/course_about.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/templates/course.html b/lms/templates/course.html index 295101da8241..884e08b81647 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -29,7 +29,7 @@

% if course.advertised_start is not None: % else: - + % endif
@@ -39,7 +39,7 @@

% if course.advertised_start is not None:
  • ${_("Starts")}:
  • % else: -
  • ${_("Starts")}:
  • +
  • ${_("Starts")}:
  • % endif

    diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 6fb28f0084b7..2db23882cd38 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -243,7 +243,7 @@

    <% course_date_string = course_start_date.strftime('%Y-%m-%dT%H:%M:%S%z') %> - + % endif % endif @@ -263,7 +263,7 @@

    <% course_date_string = course_end_date.strftime('%Y-%m-%dT%H:%M:%S%z') %> - + % endif % endif From baf7e2b8baa2ed7382dc0c9259963c4186ca634e Mon Sep 17 00:00:00 2001 From: Luis Moreno Date: Thu, 21 Feb 2019 08:54:27 -0400 Subject: [PATCH 015/119] Fix to pass xss commit linter (cherry picked from commit c5f396b142087581ba81acc5e2a0fc9f81acca46) --- lms/templates/courseware/course_about.html | 70 ++++++++++++---------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 2db23882cd38..83d77a5155d0 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -1,3 +1,4 @@ +<%page expression_filter="h" /> <%namespace name='static' file='../static_content.html'/> <%! from django.utils.translation import ugettext as _ @@ -7,7 +8,7 @@ from django.conf import settings from six import text_type from edxmako.shortcuts import marketing_link -from openedx.core.djangolib.markup import HTML +from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib.courses import course_image_url from six import string_types %> @@ -16,7 +17,7 @@ <%block name="headextra"> ## OG (Open Graph) title and description added below to give social media info to display ## (https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content#tags) - + @@ -31,21 +32,21 @@ % if can_add_course_to_cart: add_course_complete_handler = function(jqXHR, textStatus) { if (jqXHR.status == 200) { - location.href = "${cart_link}"; + location.href = "${cart_link | n, decode.utf8}"; } if (jqXHR.status == 400) { - $("#register_error") - .html(jqXHR.responseText ? jqXHR.responseText : "${_("An error occurred. Please try again later.")}") + $("#register_error").text( + jqXHR.responseText ? jqXHR.responseText : "${_("An error occurred. Please try again later.") | n, decode.utf8}") .css("display", "block"); } else if (jqXHR.status == 403) { - location.href = "${reg_then_add_to_cart_link}"; + location.href = "${reg_then_add_to_cart_link | n, decode.utf8}"; } }; $("#add_to_cart_post").click(function(event){ $.ajax({ - url: "${reverse('add_course_to_cart', args=[text_type(course.id)])}", + url: "${reverse('add_course_to_cart', args=[text_type(course.id)]) | n, decode.utf8}", type: "POST", /* Rant: HAD TO USE COMPLETE B/C PROMISE.DONE FOR SOME REASON DOES NOT WORK ON THIS PAGE. */ complete: add_course_complete_handler @@ -57,23 +58,23 @@ ## making the conditional around this entire JS block for sanity %if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: <% - perms_error = _('The currently logged-in user account does not have permission to enroll in this course. ' + perms_error = Text(_('The currently logged-in user account does not have permission to enroll in this course. ' 'You may need to {start_logout_tag}log out{end_tag} then try the enroll button again. ' - 'Please visit the {start_help_tag}help page{end_tag} for a possible solution.').format( - start_help_tag="".format(url=marketing_link('FAQ')), end_tag='', - start_logout_tag="".format(url=reverse('logout')) + 'Please visit the {start_help_tag}help page{end_tag} for a possible solution.')).format( + start_help_tag=HTML("").format(url=marketing_link('FAQ')), end_tag=HTML(''), + start_logout_tag=HTML("").format(url=reverse('logout')) ) %> $('#class_enroll_form').on('ajax:complete', function(event, xhr) { if(xhr.status == 200) { - location.href = "${reverse('dashboard')}"; + location.href = "${reverse('dashboard') | n, decode.utf8}"; } else if (xhr.status == 403) { - location.href = "${reverse('course-specific-register', args=[text_type(course.id)])}?course_id=${course.id | u}&enrollment_action=enroll"; + location.href = "${reverse('course-specific-register', args=[text_type(course.id)]) | n, decode.utf8 }?course_id=${course.id | n, decode.utf8 }&enrollment_action=enroll"; } else if (xhr.status == 400) { //This means the user did not have permission - $('#register_error').html("${perms_error}").css("display", "block"); + $('#register_error').text("${perms_error | n, decode.utf8}").css("display", "block"); } else { - $('#register_error').html( - (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}") + $('#register_error').text( + (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.") | n, decode.utf8}") ).css("display", "block"); } }); @@ -83,16 +84,16 @@ $('#class_enroll_form').on('ajax:complete', function(event, xhr) { if(xhr.status == 200) { if (xhr.responseText == "") { - location.href = "${reverse('dashboard')}"; + location.href = "${reverse('dashboard') | n, decode.utf8}"; } else { location.href = xhr.responseText; } } else if (xhr.status == 403) { - location.href = "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll"; + location.href = "${reverse('register_user') | n, decode.utf8 }?course_id=${course.id | n, decode.utf8 }&enrollment_action=enroll"; } else { - $('#register_error').html( - (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}") + $('#register_error').text( + (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.") | n, decode.utf8}") ).css("display", "block"); } }); @@ -105,7 +106,7 @@ -<%block name="pagetitle">${course.display_name_with_default_escaped} +<%block name="pagetitle">${course.display_name_with_default}
    @@ -116,10 +117,10 @@

    - ${course.display_name_with_default_escaped} + ${course.display_name_with_default}


    - ${course.display_org_with_default | h} + ${course.display_org_with_default}
    @@ -229,7 +233,7 @@

    <%block name="course_about_important_dates">
      -
    1. ${_("Course Number")}

      ${course.display_number_with_default | h}
    2. +
    3. ${_("Course Number")}

      ${course.display_number_with_default}
    4. % if not course.start_date_is_still_default: <% course_start_date = course.advertised_start or course.start @@ -290,9 +294,9 @@

      ## Multiple pre-requisite courses are not supported on frontend that's why we are pulling first element ${pre_requisite_courses[0]['display']}

      - ${_("You must successfully complete {link_start}{prc_display}{link_end} before you begin this course.").format( - link_start=''.format(prc_target), - link_end='', + ${Text(_("You must successfully complete {link_start}{prc_display}{link_end} before you begin this course.")).format( + link_start=HTML('').format(prc_target), + link_end=HTML(''), prc_display=pre_requisite_courses[0]['display'], )}

      @@ -348,7 +352,7 @@

      MITOpenCourseware

      ${pgettext("self","Enroll")} - +
      From 7ee117371d57ed0283b064ccd1f7bb8c833aaebd Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Thu, 7 Feb 2019 12:33:59 +0530 Subject: [PATCH 016/119] Implement public cohort This PR is based on #19284 and is part of the series of work related to the proposal #18134. This PR avoids the assignment of anonymous/unenrolled users to any cohort when course is public. Anonymous or unenrolled users will only see content that does not have a content group assigned. The "View Course" link to the course outline is shown on the course about page for a course marked public/public outline. It also makes course handouts available for public courses (not for public_outline). This PR also hides the different warnings and messages asking the user to sign-in and enroll in the course, when the course is marked public. It modifies the default public_view text to include the component display_name when unenrolled access is not available. --- .../xmodule/tests/test_xblock_wrappers.py | 38 +++++++++++++++++-- common/lib/xmodule/xmodule/x_module.py | 18 ++++++++- .../transformers/library_content.py | 2 +- lms/djangoapps/course_blocks/utils.py | 3 ++ lms/djangoapps/courseware/courses.py | 12 ++++++ lms/djangoapps/courseware/models.py | 12 ++++++ lms/djangoapps/courseware/tests/test_about.py | 33 +++++++++++++++- lms/djangoapps/courseware/tests/test_views.py | 4 ++ lms/djangoapps/courseware/views/index.py | 30 ++++++++------- lms/djangoapps/courseware/views/views.py | 27 +++++++++---- lms/templates/courseware/course_about.html | 6 +++ .../course_groups/tests/test_cohorts.py | 4 ++ .../tests/views/test_course_home.py | 13 ++++--- .../course_experience/views/course_home.py | 2 + .../views/course_home_messages.py | 4 +- 15 files changed, 172 insertions(+), 36 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py index 4e7112e171a2..d82913e16a28 100644 --- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py +++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tests for the wrapping layer that provides the XBlock API using XModule/Descriptor functionality @@ -27,7 +28,7 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator -from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem, STUDENT_VIEW, STUDIO_VIEW +from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem, STUDENT_VIEW, STUDIO_VIEW, PUBLIC_VIEW from xmodule.annotatable_module import AnnotatableDescriptor from xmodule.capa_module import CapaDescriptor from xmodule.course_module import CourseDescriptor @@ -63,8 +64,8 @@ CONTAINER_XMODULES = { ConditionalDescriptor: [{}], CourseDescriptor: [{}], - RandomizeDescriptor: [{}], - SequenceDescriptor: [{}], + RandomizeDescriptor: [{'display_name': 'Test String Display'}], + SequenceDescriptor: [{'display_name': u'Test Unicode हिंदी Display'}], VerticalBlock: [{}], WrapperBlock: [{}], } @@ -433,3 +434,34 @@ def check_property(self, descriptor): self.assertEquals(list(xmodule_api_fs.walk()), list(xblock_api_fs.walk())) self.assertEquals(etree.tostring(xmodule_node), etree.tostring(xblock_node)) + + +class TestPublicView(XBlockWrapperTestMixin, TestCase): + """ + This tests that default public_view shows the correct message. + """ + shard = 1 + + def skip_if_invalid(self, descriptor_cls): + pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor) + if pure_xblock_class: + public_view = descriptor_cls.public_view + else: + public_view = descriptor_cls.module_class.public_view + if public_view != XModule.public_view: + raise SkipTest(descriptor_cls.__name__ + " implements public_view") + + def check_property(self, descriptor): + """ + Assert that public_view contains correct message. + """ + if descriptor.display_name: + self.assertIn( + descriptor.display_name, + descriptor.render(PUBLIC_VIEW).content + ) + else: + self.assertIn( + "This content is only accessible", + descriptor.render(PUBLIC_VIEW).content + ) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 299a3e3d8b33..521d2fd4453a 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -72,7 +72,10 @@ # Views that present a "preview" view of an xblock (as opposed to an editing view). PREVIEW_VIEWS = [STUDENT_VIEW, PUBLIC_VIEW, AUTHOR_VIEW] -DEFAULT_PUBLIC_VIEW_MESSAGE = u'Please enroll to view this content.' +DEFAULT_PUBLIC_VIEW_MESSAGE = ( + u'This content is only accessible to enrolled learners. ' + u'Sign in or register, and enroll in this course to view it.' +) # Make '_' a no-op so we can scrape strings. Using lambda instead of # `django.utils.translation.ugettext_noop` because Django cannot be imported in this file @@ -766,7 +769,18 @@ def public_view(self, _context): u'' u'
      {}
      ' ) - return Fragment(alert_html.format(DEFAULT_PUBLIC_VIEW_MESSAGE)) + + if self.display_name: + display_text = _( + u'{display_name} is only accessible to enrolled learners. ' + 'Sign in or register, and enroll in this course to view it.' + ).format( + display_name=self.display_name + ) + else: + display_text = _(DEFAULT_PUBLIC_VIEW_MESSAGE) + + return Fragment(alert_html.format(display_text)) class ProxyAttribute(object): diff --git a/lms/djangoapps/course_blocks/transformers/library_content.py b/lms/djangoapps/course_blocks/transformers/library_content.py index eb893a459df6..b92b58dbebfe 100644 --- a/lms/djangoapps/course_blocks/transformers/library_content.py +++ b/lms/djangoapps/course_blocks/transformers/library_content.py @@ -98,7 +98,7 @@ def transform_block_filters(self, usage_info, block_structure): # Save back any changes if any(block_keys[changed] for changed in ('invalid', 'overlimit', 'added')): state_dict['selected'] = list(selected) - StudentModule.objects.update_or_create( + StudentModule.save_state( # pylint: disable=no-value-for-parameter student=usage_info.user, course_id=usage_info.course_key, module_state_key=block_key, diff --git a/lms/djangoapps/course_blocks/utils.py b/lms/djangoapps/course_blocks/utils.py index b2db403d0b6c..b36a691477ce 100644 --- a/lms/djangoapps/course_blocks/utils.py +++ b/lms/djangoapps/course_blocks/utils.py @@ -18,6 +18,9 @@ def get_student_module_as_dict(user, course_key, block_key): Returns: StudentModule as a (possibly empty) dict. """ + if not user.is_authenticated(): + return {} + try: student_module = StudentModule.objects.get( student=user, diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index caccff13132d..10f6d526e794 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -35,6 +35,8 @@ from opaque_keys.edx.keys import UsageKey from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.lib.api.view_utils import LazySequence +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG from path import Path as path from six import text_type from static_replace import replace_static_urls @@ -629,3 +631,13 @@ def get_course_chapter_ids(course_key): log.exception('Failed to retrieve course from modulestore.') return [] return [unicode(chapter_key) for chapter_key in chapter_keys if chapter_key.block_type == 'chapter'] + + +def allow_public_access(course, visibilities): + """ + This checks if the unenrolled access waffle flag for the course is set + and the course visibility matches any of the input visibilities. + """ + unenrolled_access_flag = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course.id) + allow_access = unenrolled_access_flag and course.course_visibility in visibilities + return allow_access diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index abeff618376e..d54fcb96dfde 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -161,6 +161,18 @@ def get_state_by_params(cls, course_id, module_state_keys, student_id=None): module_states = module_states.filter(student_id=student_id) return module_states + @classmethod + def save_state(cls, student, course_id, module_state_key, defaults): + if not student.is_authenticated(): + return + else: + cls.objects.update_or_create( + student=student, + course_id=course_id, + module_state_key=module_state_key, + defaults=defaults, + ) + class BaseStudentModuleHistory(models.Model): """Abstract class containing most fields used by any class diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py index 5f1189543a36..84a8a8656035 100644 --- a/lms/djangoapps/courseware/tests/test_about.py +++ b/lms/djangoapps/courseware/tests/test_about.py @@ -3,6 +3,7 @@ """ import datetime import ddt +import mock import pytz from ccx_keys.locator import CCXLocator from django.conf import settings @@ -16,14 +17,22 @@ from course_modes.models import CourseMode from lms.djangoapps.ccx.tests.factories import CcxFactory from openedx.core.lib.tests import attr +from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag 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 openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG from shoppingcart.models import Order, PaidCourseRegistration from student.models import CourseEnrollment from student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory from track.tests import EventTrackingTestCase from util.milestones_helpers import get_prerequisite_courses_display, set_prerequisite_courses -from xmodule.course_module import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE +from xmodule.course_module import ( + CATALOG_VISIBILITY_ABOUT, + CATALOG_VISIBILITY_NONE, + COURSE_VISIBILITY_PRIVATE, + COURSE_VISIBILITY_PUBLIC_OUTLINE, + COURSE_VISIBILITY_PUBLIC +) from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MIXED_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE, @@ -41,6 +50,7 @@ SHIB_ERROR_STR = "The currently logged-in user account does not have permission to enroll in this course." +@ddt.ddt @attr(shard=1) class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTrackingTestCase, MilestonesTestCaseMixin): """ @@ -222,6 +232,27 @@ def test_about_page_unfulfilled_prereqs(self): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) + @ddt.data( + [COURSE_VISIBILITY_PRIVATE], + [COURSE_VISIBILITY_PUBLIC_OUTLINE], + [COURSE_VISIBILITY_PUBLIC], + ) + @ddt.unpack + def test_about_page_public_view(self, course_visibility): + """ + Assert that anonymous or unenrolled users see View Course option + when unenrolled access flag is set + """ + with mock.patch('xmodule.course_module.CourseDescriptor.course_visibility', course_visibility): + with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True): + url = reverse('about_course', args=[text_type(self.course.id)]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + if course_visibility == COURSE_VISIBILITY_PUBLIC or course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE: + self.assertIn("View Course", resp.content) + else: + self.assertIn("Enroll in", resp.content) + @attr(shard=1) class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index da7819f33d4e..6bc6c9256e68 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -2472,6 +2472,10 @@ def test_courseware_access(self, waffle_override, course_visibility, user_type, self.assertIn('xblock-public_view-vertical', response.content) self.assertIn('xblock-public_view-html', response.content) self.assertIn('xblock-public_view-video', response.content) + if user_type == CourseUserType.ANONYMOUS and course_visibility == COURSE_VISIBILITY_PRIVATE: + self.assertIn('To see course content', response.content) + if user_type == CourseUserType.UNENROLLED and course_visibility == COURSE_VISIBILITY_PRIVATE: + self.assertIn('You must be enrolled', response.content) else: self.assertIn('data-save-position="true"', response.content) self.assertIn('data-show-completion="true"', response.content) diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index a9768fc1af51..c0ea97a9faab 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -25,6 +25,7 @@ from edxmako.shortcuts import render_to_response, render_to_string +from lms.djangoapps.courseware.courses import allow_public_access from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.gating.api import get_entrance_exam_score_ratio, get_entrance_exam_usage_key @@ -195,20 +196,23 @@ def render(self, request): 'email_opt_in': False, }) - PageLevelMessages.register_warning_message( - request, - Text(_("You are not signed in. To see additional course content, {sign_in_link} or " - "{register_link}, and enroll in this course.")).format( - sign_in_link=HTML('{sign_in_label}').format( - sign_in_label=_('sign in'), - url='{}?{}'.format(reverse('signin_user'), qs), - ), - register_link=HTML('{register_label}').format( - register_label=_('register'), - url='{}?{}'.format(reverse('register_user'), qs), - ), + allow_anonymous = allow_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) + + if not allow_anonymous: + PageLevelMessages.register_warning_message( + request, + Text(_("You are not signed in. To see additional course content, {sign_in_link} or " + "{register_link}, and enroll in this course.")).format( + sign_in_link=HTML('{sign_in_label}').format( + sign_in_label=_('sign in'), + url='{}?{}'.format(reverse('signin_user'), qs), + ), + register_link=HTML('{register_label}').format( + register_label=_('register'), + url='{}?{}'.format(reverse('register_user'), qs), + ), + ) ) - ) return render_to_response('courseware/courseware.html', self._create_courseware_context(request)) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 18a5b220ac9d..73dabbce09e2 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -65,6 +65,7 @@ from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.commerce.utils import EcommerceService +from lms.djangoapps.courseware.courses import allow_public_access from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory @@ -87,7 +88,11 @@ from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_duration_limits.access import generate_course_expired_fragment -from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name +from openedx.features.course_experience import ( + UNIFIED_COURSE_TAB_FLAG, + COURSE_ENABLE_UNENROLLED_ACCESS_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 ENABLE_COURSE_ABOUT_SIDEBAR_HTML @@ -101,6 +106,7 @@ from util.db import outer_atomic from util.milestones_helpers import get_prerequisite_courses_display from util.views import _record_feedback_in_zendesk, ensure_valid_course_key, ensure_valid_usage_key +from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from web_fragments.fragment import Fragment from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem @@ -459,7 +465,7 @@ def get(self, request, course_id, tab_slug, **kwargs): raise Http404 # Show warnings if the user has limited access - CourseTabView.register_user_access_warning_messages(request, course_key) + CourseTabView.register_user_access_warning_messages(request, course) return super(StaticCourseTabView, self).get(request, course=course, tab=tab, **kwargs) @@ -504,7 +510,7 @@ def get(self, request, course_id, tab_type, **kwargs): # Show warnings if the user has limited access # Must come after masquerading on creation of page context - self.register_user_access_warning_messages(request, course_key) + self.register_user_access_warning_messages(request, course) set_custom_metrics_for_course_key(course_key) return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs) @@ -522,11 +528,13 @@ def url_to_enroll(course_key): return url_to_enroll @staticmethod - def register_user_access_warning_messages(request, course_key): + def register_user_access_warning_messages(request, course): """ Register messages to be shown to the user if they have limited access. """ - if request.user.is_anonymous: + allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC]) + + if request.user.is_anonymous and not allow_anonymous: PageLevelMessages.register_warning_message( request, Text(_("To see course content, {sign_in_link} or {register_link}.")).format( @@ -541,10 +549,10 @@ def register_user_access_warning_messages(request, course_key): ) ) else: - if not CourseEnrollment.is_enrolled(request.user, course_key): + if not CourseEnrollment.is_enrolled(request.user, course.id) and not allow_anonymous: # Only show enroll button if course is open for enrollment. - if course_open_for_self_enrollment(course_key): - enroll_message = _('You must be enrolled in the course to see course content. \ + if course_open_for_self_enrollment(course.id): + enroll_message = _(u'You must be enrolled in the course to see course content. \ {enroll_link_start}Enroll now{enroll_link_end}.') PageLevelMessages.register_warning_message( request, @@ -842,6 +850,8 @@ def course_about(request, course_id): sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML) + allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE]) + # 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. @@ -880,6 +890,7 @@ def course_about(request, course_id): 'course_image_urls': overview.image_urls, 'reviews_fragment_view': reviews_fragment_view, 'sidebar_html_enabled': sidebar_html_enabled, + 'allow_anonymous': allow_anonymous, } return render_to_response('courseware/course_about.html', context) diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 83d77a5155d0..6104d8fc031f 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -172,6 +172,12 @@

      price=course_price)}
      + %elif allow_anonymous: + %if show_courseware_link: + + ${_("View Course")} + + %endif %else: <% if ecommerce_checkout: diff --git a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py index 785a1c6e7e73..d9de491e36eb 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py +++ b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py @@ -391,6 +391,10 @@ def test_anonymous_user_cohort(self): Anonymous user is not assigned to any cohort group. """ course = modulestore().get_course(self.toy_course_key) + + # verify cohorts is None when course is not cohorted + self.assertIsNone(cohorts.get_cohort(AnonymousUser(), course.id)) + config_course_cohorts( course, is_cohorted=True, diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 048ebcf8c4eb..9773a44291a5 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -312,12 +312,13 @@ def test_home_page( self.assertContains(response, TEST_CHAPTER_NAME, count=(1 if expected_course_outline else 0)) # Verify that the expected message is shown to the user - self.assertContains( - response, 'To see course content', count=(1 if is_anonymous else 0) - ) - self.assertContains(response, '
      '), + close_enroll_link=HTML('') ), title=Text(_('Welcome to {course_display_name}')).format( course_display_name=course.display_name From fdd3c1f1f79102e3f025367e2e8bb53c0d775a5f Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Mon, 25 Feb 2019 14:09:40 -0500 Subject: [PATCH 017/119] Fix import error introduced by 7ee11737 cherry pick --- lms/djangoapps/courseware/courses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 10f6d526e794..dfa0571afc60 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -35,7 +35,6 @@ from opaque_keys.edx.keys import UsageKey from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.core.lib.api.view_utils import LazySequence from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG from path import Path as path from six import text_type From 71a824eaf4a420e3c436d730a4bdd0ef68e0333a Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Mon, 25 Feb 2019 12:52:46 -0500 Subject: [PATCH 018/119] Switch workers to ironwood --- scripts/Jenkinsfiles/bokchoy | 4 ++-- scripts/Jenkinsfiles/lettuce | 6 +++--- scripts/Jenkinsfiles/python | 10 +++++----- scripts/Jenkinsfiles/quality | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/Jenkinsfiles/bokchoy b/scripts/Jenkinsfiles/bokchoy index 543d721cf228..fb6e3b0ecdd0 100644 --- a/scripts/Jenkinsfiles/bokchoy +++ b/scripts/Jenkinsfiles/bokchoy @@ -26,7 +26,7 @@ def bokchoyTestCleanup() { } pipeline { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } options { timestamps() timeout(60) @@ -62,7 +62,7 @@ pipeline { for (int i = 1; i <= 22; i++) { int index = i parallel_stages["${index}"] = { - node('jenkins-worker') { + node('ironwood-jenkins-worker') { withEnv(["SHARD=${index}","TEST_SUITE=bok-choy"]) { try { stage("Bokchoy shard: ${index}") { diff --git a/scripts/Jenkinsfiles/lettuce b/scripts/Jenkinsfiles/lettuce index 5bdce3df51eb..1cf344707db2 100644 --- a/scripts/Jenkinsfiles/lettuce +++ b/scripts/Jenkinsfiles/lettuce @@ -25,7 +25,7 @@ def lettuceTestCleanup() { } pipeline { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } options { timestamps() timeout(60) @@ -57,7 +57,7 @@ pipeline { stage('Run Tests') { parallel { stage("lms-acceptance") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "lms-acceptance" } @@ -75,7 +75,7 @@ pipeline { } } stage("cms-acceptance") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "cms-acceptance" } diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index a20d2183bd33..dee1eaede871 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -36,7 +36,7 @@ def xdist_git_branch() { } pipeline { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } options { timestamps() timeout(60) @@ -44,7 +44,7 @@ pipeline { environment { XDIST_CONTAINER_SUBNET = credentials('XDIST_CONTAINER_SUBNET') XDIST_CONTAINER_SECURITY_GROUP = credentials('XDIST_CONTAINER_SECURITY_GROUP') - XDIST_CONTAINER_TASK_NAME = "jenkins-worker-task" + XDIST_CONTAINER_TASK_NAME = "ironwood-jenkins-worker-task" XDIST_GIT_BRANCH = xdist_git_branch() } stages { @@ -74,7 +74,7 @@ pipeline { stage('Run Tests') { parallel { stage("lms-unit") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "lms-unit" XDIST_NUM_TASKS = 10 @@ -94,7 +94,7 @@ pipeline { } } stage("cms-unit") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "cms-unit" XDIST_NUM_TASKS = 3 @@ -114,7 +114,7 @@ pipeline { } } stage("commonlib-unit") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "commonlib-unit" XDIST_NUM_TASKS = 3 diff --git a/scripts/Jenkinsfiles/quality b/scripts/Jenkinsfiles/quality index 146d6f532b13..2c520326e84f 100644 --- a/scripts/Jenkinsfiles/quality +++ b/scripts/Jenkinsfiles/quality @@ -40,7 +40,7 @@ def qualityTestCleanup() { } pipeline { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } options { timestamps() timeout(60) @@ -72,7 +72,7 @@ pipeline { stage('Run Tests') { parallel { stage("commonlib pylint") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 1 @@ -91,7 +91,7 @@ pipeline { } } stage("lms pylint") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 2 @@ -110,7 +110,7 @@ pipeline { } } stage("cms/openedx/pavelib pylint") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 3 @@ -129,7 +129,7 @@ pipeline { } } stage("Other quality checks") { - agent { label "jenkins-worker" } + agent { label "ironwood-jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 4 From c86ed71606ec0cf85751a79d5b0f2fb07ac6b1f9 Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Tue, 26 Feb 2019 15:08:43 -0500 Subject: [PATCH 019/119] Jenkinsfile typo --- scripts/Jenkinsfiles/python | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index dee1eaede871..104a88243d13 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -63,7 +63,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), - tring(name: 'CONTEXT', value: 'jenkins/python'), + string(name: 'CONTEXT', value: 'jenkins/python'), string(name: 'CREATE_DEPLOYMENT', value: 'false'), string(name: 'BUILD_STATUS', value: 'pending') ], From 4690f8de2f2e9de8eabd4e68e14d8cbe1f07b449 Mon Sep 17 00:00:00 2001 From: stv Date: Wed, 20 Feb 2019 11:36:27 -0800 Subject: [PATCH 020/119] Handle out-of-memory exception on Sysadmin Courses We've been running into OoM issues with this functionality while executing on one of our smaller application servers. - Sometimes this manifests by preventing the entire page from loading (exception during the GET). - At others, it occurs using the `Delete course from site` functionality by breaking during the POST handler. This case is particularly frustrating because the course actually is deleted, but looks like it may not have been. Internally, this is because the 500 error occurs _after_ the course has been successfully deleted, though this is opaque to the end-user. While this doesn't address the underlying memory issue, it does at least allow the app to recover gracefully. (cherry picked from commit 25be88932c27a4f714a0d2024469f59cb1c89435) --- lms/djangoapps/dashboard/sysadmin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lms/djangoapps/dashboard/sysadmin.py b/lms/djangoapps/dashboard/sysadmin.py index e348e3db6ad7..fd811b24c7c1 100644 --- a/lms/djangoapps/dashboard/sysadmin.py +++ b/lms/djangoapps/dashboard/sysadmin.py @@ -357,6 +357,8 @@ def git_info_for_course(self, cdir): info = [output_json['commit'], output_json['date'], output_json['author'], ] + except OSError as error: + log.warning(text_type(u"Error fetching git data: %s - %s"), text_type(cdir), text_type(error)) except (ValueError, subprocess.CalledProcessError): pass From 64dce6841b930dd66ce52b70c0684599fd06fda7 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 28 Sep 2018 18:54:14 -0400 Subject: [PATCH 021/119] Remove course publish from CCX data migration. This removes the portion of the CCX course names data migration that triggers course publishing. While course publishing is the desired behavior, doing so within the migration makes it so that this migration is not practical to run if you have hundreds or thousands of CCX courses (we run out of memory). By doing the data migration without the accompanying course publishes, this means that CCX course names will still show up incorrectly for courses until there is an edit or publish of either that CCX course or the underlying course that the CCX course extends. But we at least set up the data so that it's gradually self-correcting over time, and do so with little coding effort or operational risk/weirdness. A more comprehensive fix for this would involve: * Fixing the memory leak around CourseOverviews creation and/or; * Moving CourseOverview generation to happen asynchronously and/or; * Being more careful about transactionality in the data migration and running it repeatedly until it all passes. Full details of our migration issues are at: https://github.com/edx/edx-platform/pull/18808#issuecomment-422930820 (cherry picked from commit 5f97cecefbeb7ed9583c6257a78bb2e08d7f7b4f) --- .../0006_set_display_name_as_override.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lms/djangoapps/ccx/migrations/0006_set_display_name_as_override.py b/lms/djangoapps/ccx/migrations/0006_set_display_name_as_override.py index c6a2e2f3c070..ff7ca5d64ea4 100644 --- a/lms/djangoapps/ccx/migrations/0006_set_display_name_as_override.py +++ b/lms/djangoapps/ccx/migrations/0006_set_display_name_as_override.py @@ -10,7 +10,6 @@ from ccx_keys.locator import CCXLocator from courseware.courses import get_course_by_id -from xmodule.modulestore.django import SignalHandler log = logging.getLogger(__name__) @@ -49,19 +48,6 @@ def save_display_name(apps, schema_editor): defaults={'value': serialized_display_name}, ) - # Publish change - responses = SignalHandler.course_published.send( - sender=ccx, - course_key=CCXLocator.from_course_locator(course.id, unicode(ccx.id)) - ) - for rec, response in responses: - log.info( - 'Signal fired when course is published. Course %s. Receiver: %s. Response: %s', - ccx.course_id, - rec, - response - ) - class Migration(migrations.Migration): From 5d24537520d020420785a151b87d83a1b10065bc Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Thu, 28 Feb 2019 10:20:27 -0500 Subject: [PATCH 022/119] Move context to env var --- scripts/Jenkinsfiles/bokchoy | 4 ++-- scripts/Jenkinsfiles/lettuce | 4 ++-- scripts/Jenkinsfiles/python | 4 ++-- scripts/Jenkinsfiles/quality | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/Jenkinsfiles/bokchoy b/scripts/Jenkinsfiles/bokchoy index fb6e3b0ecdd0..d0639e24ce06 100644 --- a/scripts/Jenkinsfiles/bokchoy +++ b/scripts/Jenkinsfiles/bokchoy @@ -47,7 +47,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), - string(name: 'CONTEXT', value: 'jenkins/bokchoy'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: 'false'), string(name: 'BUILD_STATUS', value: 'pending') ], @@ -107,7 +107,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), - string(name: 'CONTEXT', value: 'jenkins/bokchoy'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: create_deployment), string(name: 'BUILD_STATUS', value: build_status) ], diff --git a/scripts/Jenkinsfiles/lettuce b/scripts/Jenkinsfiles/lettuce index 1cf344707db2..852fee6ca34d 100644 --- a/scripts/Jenkinsfiles/lettuce +++ b/scripts/Jenkinsfiles/lettuce @@ -46,7 +46,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), - string(name: 'CONTEXT', value: 'jenkins/lettuce'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: 'false'), string(name: 'BUILD_STATUS', value: 'pending') ], @@ -122,7 +122,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), - string(name: 'CONTEXT', value: 'jenkins/lettuce'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: create_deployment), string(name: 'BUILD_STATUS', value: build_status) ], diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index 104a88243d13..412e9a6e55c9 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -63,7 +63,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), - string(name: 'CONTEXT', value: 'jenkins/python'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: 'false'), string(name: 'BUILD_STATUS', value: 'pending') ], @@ -212,7 +212,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), - string(name: 'CONTEXT', value: 'jenkins/python'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: create_deployment), string(name: 'BUILD_STATUS', value: build_status) ], diff --git a/scripts/Jenkinsfiles/quality b/scripts/Jenkinsfiles/quality index 2c520326e84f..396b2f258333 100644 --- a/scripts/Jenkinsfiles/quality +++ b/scripts/Jenkinsfiles/quality @@ -61,7 +61,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), - string(name: 'CONTEXT', value: 'jenkins/quality'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: 'false'), string(name: 'BUILD_STATUS', value: 'pending') ], @@ -224,7 +224,7 @@ pipeline { string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), - string(name: 'CONTEXT', value: 'jenkins/quality'), + string(name: 'CONTEXT', value: "${GITHUB_CONTEXT}"), string(name: 'CREATE_DEPLOYMENT', value: create_deployment), string(name: 'BUILD_STATUS', value: build_status) ], From 370e82cea1820d40c3b29fe6c62c4aa2f21fbff2 Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Thu, 28 Feb 2019 13:50:49 -0500 Subject: [PATCH 023/119] Change git checkout on xdist workers --- scripts/xdist/prepare_xdist_nodes.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/xdist/prepare_xdist_nodes.sh b/scripts/xdist/prepare_xdist_nodes.sh index a647272d055d..af221f8f968a 100644 --- a/scripts/xdist/prepare_xdist_nodes.sh +++ b/scripts/xdist/prepare_xdist_nodes.sh @@ -7,12 +7,19 @@ python scripts/xdist/pytest_container_manager.py -a up -n ${XDIST_NUM_TASKS} \ -s ${XDIST_CONTAINER_SUBNET} \ -sg ${XDIST_CONTAINER_SECURITY_GROUP} +# Need to map remote branch to local branch when fetching a branch other than master +if [ "$XDIST_GIT_BRANCH" == "master" ]; then + XDIST_GIT_FETCH_STRING="$XDIST_GIT_BRANCH" +else + XDIST_GIT_FETCH_STRING="$XDIST_GIT_BRANCH:$XDIST_GIT_BRANCH" +fi + ip_list=$( Date: Wed, 6 Mar 2019 17:18:39 -0500 Subject: [PATCH 024/119] production.py should fallback to aws.py incase the plugin has not yet been migrated to use production.py --- cms/envs/production.py | 11 +++++++++-- lms/envs/production.py | 13 ++++++++++--- openedx/core/djangoapps/plugins/constants.py | 3 ++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/cms/envs/production.py b/cms/envs/production.py index f732ec9e1d1c..d560729ed105 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -11,6 +11,7 @@ from path import Path as path from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed +from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants from .common import * @@ -593,8 +594,14 @@ ####################### Plugin Settings ########################## # This is at the bottom because it is going to load more settings after base settings are loaded -from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants # pylint: disable=wrong-import-order, wrong-import-position -plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.AWS) + +# Load aws.py in plugins for reverse compatibility. This can be removed after aws.py +# is officially removed. +plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, + plugin_constants.SettingsType.AWS) + +# We continue to load production.py over aws.py +plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.PRODUCTION) ########################## Derive Any Derived Settings ####################### diff --git a/lms/envs/production.py b/lms/envs/production.py index 4bafbae2b6b3..b1baf043c9d0 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -23,11 +23,12 @@ import os import dateutil -from corsheaders.defaults import default_headers as corsheaders_default_headers from path import Path as path from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed +from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants from .common import * + from openedx.core.lib.derived import derive_settings # pylint: disable=wrong-import-order from openedx.core.lib.logsettings import get_logger_config # pylint: disable=wrong-import-order @@ -1101,8 +1102,14 @@ ############################### Plugin Settings ############################### # This is at the bottom because it is going to load more settings after base settings are loaded -from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants # pylint: disable=wrong-import-order, wrong-import-position -plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.AWS) + +# Load aws.py in plugins for reverse compatibility. This can be removed after aws.py +# is officially removed. +plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, + plugin_constants.SettingsType.AWS) + +# Load production.py in plugins +plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.PRODUCTION) ########################## Derive Any Derived Settings ####################### diff --git a/openedx/core/djangoapps/plugins/constants.py b/openedx/core/djangoapps/plugins/constants.py index e9acdb370c5a..ea4671acacb1 100644 --- a/openedx/core/djangoapps/plugins/constants.py +++ b/openedx/core/djangoapps/plugins/constants.py @@ -29,7 +29,8 @@ class SettingsType(object): See https://github.com/edx/edx-platform/master/lms/envs/docs/README.rst for further information on each Settings Type. """ - AWS = u'aws' + AWS = u'aws' # aws.py has been deprecated. See https://openedx.atlassian.net/browse/DEPR-14 + PRODUCTION = u'production' COMMON = u'common' DEVSTACK = u'devstack' TEST = u'test' From ef2381a4213e9be829391fdd38fe8d0fce372390 Mon Sep 17 00:00:00 2001 From: Cory Lee Date: Thu, 14 Mar 2019 10:37:15 -0400 Subject: [PATCH 025/119] Readding erroneously removed import --- lms/envs/production.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/envs/production.py b/lms/envs/production.py index b1baf043c9d0..b27360730eeb 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -23,6 +23,7 @@ import os import dateutil +from corsheaders.defaults import default_headers as corsheaders_default_headers from path import Path as path from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants From 6c4982f2736ef9d6fd6ca1e9f8e7a18373488d2f Mon Sep 17 00:00:00 2001 From: Cory Lee Date: Thu, 14 Mar 2019 10:38:04 -0400 Subject: [PATCH 026/119] Update production.py --- lms/envs/production.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/envs/production.py b/lms/envs/production.py index b27360730eeb..4bea786ac863 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -29,7 +29,6 @@ from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants from .common import * - from openedx.core.lib.derived import derive_settings # pylint: disable=wrong-import-order from openedx.core.lib.logsettings import get_logger_config # pylint: disable=wrong-import-order From e77c2bf7a9f7d89c9642efaa9b63c420c0a4724d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 14 Mar 2019 11:32:16 -0400 Subject: [PATCH 027/119] Skip tests of help doc links on named release branches. @skip_unless_master is used to skip tests because on named release branches, most work happens leading up to the first release on the branch, and that is before the docs have been published. Tests that check readthedocs for the right doc page will fail during this time, and it's just a big distraction. Also, if we bork the docs, it's not the end of the world, and we can fix it easily, so this is a good tradeoff. (cherry picked from commit 561f0752ca34e4b1199bcfa09e3b6a8e16070f01) --- .../acceptance/tests/lms/test_lms_help.py | 10 ++++++ .../tests/studio/test_studio_help.py | 32 +++++++++++++++++++ openedx/core/release.py | 13 ++++++++ 3 files changed, 55 insertions(+) diff --git a/common/test/acceptance/tests/lms/test_lms_help.py b/common/test/acceptance/tests/lms/test_lms_help.py index b2befb9f20a4..25a302fb1f96 100644 --- a/common/test/acceptance/tests/lms/test_lms_help.py +++ b/common/test/acceptance/tests/lms/test_lms_help.py @@ -12,8 +12,17 @@ url_for_help, click_and_wait_for_window ) +from openedx.core.release import skip_unless_master +# @skip_unless_master is used throughout this file because on named release +# branches, most work happens leading up to the first release on the branch, and +# that is before the docs have been published. Tests that check readthedocs for +# the right doc page will fail during this time, and it's just a big +# distraction. Also, if we bork the docs, it's not the end of the world, and we +# can fix it easily, so this is a good tradeoff. + +@skip_unless_master # See note at the top of the file. class TestCohortHelp(ContainerBase, CohortTestMixin): """ Tests help links in Cohort page @@ -78,6 +87,7 @@ def test_automatic_cohort_help(self): self.verify_help_link(href) +@skip_unless_master # See note at the top of the file. class InstructorDashboardHelp(BaseInstructorDashboardTest): """ Tests opening help from the general Help button in the instructor dashboard. diff --git a/common/test/acceptance/tests/studio/test_studio_help.py b/common/test/acceptance/tests/studio/test_studio_help.py index 8f2846ef6971..520decf45b76 100644 --- a/common/test/acceptance/tests/studio/test_studio_help.py +++ b/common/test/acceptance/tests/studio/test_studio_help.py @@ -35,6 +35,14 @@ ) from common.test.acceptance.tests.studio.base_studio_test import ContainerBase, StudioCourseTest, StudioLibraryTest from openedx.core.lib.tests import attr +from openedx.core.release import skip_unless_master + +# @skip_unless_master is used throughout this file because on named release +# branches, most work happens leading up to the first release on the branch, and +# that is before the docs have been published. Tests that check readthedocs for +# the right doc page will fail during this time, and it's just a big +# distraction. Also, if we bork the docs, it's not the end of the world, and we +# can fix it easily, so this is a good tradeoff. def _get_expected_documentation_url(path): @@ -45,6 +53,7 @@ def _get_expected_documentation_url(path): @attr(shard=20) +@skip_unless_master # See note at the top of the file. class StudioHelpTest(StudioCourseTest): """Tests for Studio help.""" @@ -85,6 +94,7 @@ def test_studio_help_links(self): @attr(shard=20) +@skip_unless_master class HomeHelpTest(StudioCourseTest): """ Tests help links on 'Home'(Courses tab) page. @@ -134,6 +144,7 @@ def test_course_home_side_bar_help(self): @attr(shard=20) +@skip_unless_master class NewCourseHelpTest(AcceptanceTest): """ Test help links while creating a new course. @@ -187,6 +198,7 @@ def test_course_create_side_bar_help(self): @attr(shard=20) +@skip_unless_master class NewLibraryHelpTest(AcceptanceTest): """ Test help links while creating a new library @@ -240,6 +252,7 @@ def test_library_create_side_bar_help(self): @attr(shard=20) +@skip_unless_master class LibraryTabHelpTest(AcceptanceTest): """ Test help links on the library tab present at dashboard. @@ -273,6 +286,7 @@ def test_library_tab_nav_help(self): @attr(shard=20) +@skip_unless_master class LibraryHelpTest(StudioLibraryTest): """ Test help links on a Library page. @@ -306,6 +320,7 @@ def test_library_user_access_setting_nav_help(self): @attr(shard=20) +@skip_unless_master class LibraryImportHelpTest(StudioLibraryTest): """ Test help links on a Library import and export pages. @@ -354,6 +369,7 @@ def test_library_import_side_bar_help(self): @attr(shard=20) +@skip_unless_master class LibraryExportHelpTest(StudioLibraryTest): """ Test help links on a Library export pages. @@ -402,6 +418,7 @@ def test_library_export_side_bar_help(self): @attr(shard=20) +@skip_unless_master class CourseOutlineHelpTest(StudioCourseTest): """ Tests help links on course outline page. @@ -457,6 +474,7 @@ def test_course_outline_side_bar_help(self): @attr(shard=20) +@skip_unless_master class CourseUpdateHelpTest(StudioCourseTest): """ Test help links on Course Update page @@ -491,6 +509,7 @@ def test_course_update_nav_help(self): @attr(shard=20) +@skip_unless_master class AssetIndexHelpTest(StudioCourseTest): """ Test help links on Course 'Files & Uploads' page @@ -525,6 +544,7 @@ def test_asset_index_nav_help(self): @attr(shard=20) +@skip_unless_master class CoursePagesHelpTest(StudioCourseTest): """ Test help links on Course 'Pages' page @@ -559,6 +579,7 @@ def test_course_page_nav_help(self): @attr(shard=20) +@skip_unless_master class UploadTextbookHelpTest(StudioCourseTest): """ Test help links on Course 'Textbooks' page @@ -612,6 +633,7 @@ def test_course_textbook_side_bar_help(self): @attr(shard=20) +@skip_unless_master class StudioUnitHelpTest(ContainerBase): """ Tests help links on Unit page. @@ -661,6 +683,7 @@ def test_unit_page_nav_help(self): @attr(shard=20) +@skip_unless_master class SettingsHelpTest(StudioCourseTest): """ Tests help links on Schedule and Details Settings page @@ -697,6 +720,7 @@ def test_settings_page_nav_help(self): @attr(shard=20) +@skip_unless_master class GradingPageHelpTest(StudioCourseTest): """ Tests help links on Grading page @@ -733,6 +757,7 @@ def test_grading_page_nav_help(self): @attr(shard=20) +@skip_unless_master class CourseTeamSettingsHelpTest(StudioCourseTest): """ Tests help links on Course Team settings page @@ -769,6 +794,7 @@ def test_course_course_team_nav_help(self): @attr(shard=20) +@skip_unless_master class CourseGroupConfigurationHelpTest(StudioCourseTest): """ Tests help links on course Group Configurations settings page @@ -826,6 +852,7 @@ def test_course_group_conf_content_group_side_bar_help(self): @attr(shard=20) +@skip_unless_master class AdvancedSettingHelpTest(StudioCourseTest): """ Tests help links on course Advanced Settings page. @@ -862,6 +889,7 @@ def test_advanced_settings_nav_help(self): @attr(shard=20) +@skip_unless_master class CertificatePageHelpTest(StudioCourseTest): """ Tests help links on course Certificate settings page. @@ -917,6 +945,7 @@ def test_certificate_page_side_bar_help(self): @attr(shard=20) +@skip_unless_master class GroupExperimentConfigurationHelpTest(ContainerBase): """ Tests help links on course Group Configurations settings page @@ -970,6 +999,7 @@ def test_course_group_configuration_experiment_side_bar_help(self): @attr(shard=20) +@skip_unless_master class ToolsImportHelpTest(StudioCourseTest): """ Tests help links on tools import pages. @@ -1025,6 +1055,7 @@ def test_tools_import_side_bar_help(self): @attr(shard=20) +@skip_unless_master class ToolsExportHelpTest(StudioCourseTest): """ Tests help links on tools export pages. @@ -1080,6 +1111,7 @@ def test_tools_import_side_bar_help(self): @attr(shard=20) +@skip_unless_master # See note at the top of the file. class StudioWelcomeHelpTest(AcceptanceTest): """ Tests help link on 'Welcome' page ( User not logged in) diff --git a/openedx/core/release.py b/openedx/core/release.py index 5ca3f7f44df4..c6f312abc488 100644 --- a/openedx/core/release.py +++ b/openedx/core/release.py @@ -2,6 +2,9 @@ Information about the release line of this Open edX code. """ +import unittest + + # The release line: an Open edX release name ("ficus"), or "master". # This should always be "master" on the master branch, and will be changed # manually when we start release-line branches, like open-release/ficus.master. @@ -17,3 +20,13 @@ def doc_version(): return "latest" else: return "open-release-{}.master".format(RELEASE_LINE) + + +def skip_unless_master(func_or_class): + """ + Only run the decorated test for code on master or destined for master. + + Use this to skip tests that we expect to fail on a named release branch. + Please use carefully! + """ + return unittest.skipUnless(RELEASE_LINE == "master", "Test often fails on named releases")(func_or_class) From f8319d8ecdc589dd4d0c4b1404618b93e90cecd8 Mon Sep 17 00:00:00 2001 From: Felipe Montoya Date: Wed, 20 Feb 2019 16:48:46 -0500 Subject: [PATCH 028/119] Make the studio login over the lms optional using a feature flag Squashed from: 80b977fff4 Make the studio login over the lms optional using a feature flag 14b4223b5e Addressing feedback 9195ec9f30 Addressing second feedback about redirect logic on logout behing feature flag 923a91734d Fixing lettuce tests (cherry picked from commit 6f91a0d9e8ea2364f58a2cd38e804ac57f8f416b) Commit 6f91a0d9 was a squashed version of the four original commits, to make resolving conflicts easier. --- cms/envs/common.py | 3 +++ cms/envs/production.py | 10 ++++++++++ cms/templates/widgets/header.html | 3 +-- cms/templates/widgets/user_dropdown.html | 5 +---- lms/envs/test.py | 4 ++++ openedx/core/djangoapps/user_authn/views/logout.py | 8 ++++---- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 527089085443..a46ca5731dbf 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -440,6 +440,9 @@ LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/" ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' ENTERPRISE_CONSENT_API_URL = LMS_INTERNAL_ROOT_URL + '/consent/api/v1/' +FRONTEND_LOGIN_URL = LOGIN_URL +FRONTEND_LOGOUT_URL = lambda settings: settings.LMS_ROOT_URL + '/logout' +derived('FRONTEND_LOGOUT_URL') # These are standard regexes for pulling out info like course_ids, usage_ids, etc. # They are used so that URLs with deprecated-format strings still work. diff --git a/cms/envs/production.py b/cms/envs/production.py index d560729ed105..86915074a452 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -12,6 +12,7 @@ from path import Path as path from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants +from django.core.urlresolvers import reverse_lazy from .common import * @@ -294,6 +295,15 @@ CAS_ATTRIBUTE_CALLBACK['function'] ) +# Login using the LMS as the identity provider. +# Turning the flag to True means that the LMS will NOT be used as the Identity Provider (idp) +if FEATURES.get('DISABLE_STUDIO_SSO_OVER_LMS', False): + LOGIN_URL = reverse_lazy('login') + FRONTEND_LOGIN_URL = LOGIN_URL + FRONTEND_LOGOUT_URL = reverse_lazy('logout') + +LOGIN_REDIRECT_WHITELIST = [reverse_lazy('home')] + # Specific setting for the File Upload Service to store media in a bucket. FILE_UPLOAD_STORAGE_BUCKET_NAME = ENV_TOKENS.get('FILE_UPLOAD_STORAGE_BUCKET_NAME', FILE_UPLOAD_STORAGE_BUCKET_NAME) FILE_UPLOAD_STORAGE_PREFIX = ENV_TOKENS.get('FILE_UPLOAD_STORAGE_PREFIX', FILE_UPLOAD_STORAGE_PREFIX) diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 11229321f2e1..1b69f2411ea8 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -230,7 +230,6 @@

      @@ -245,7 +244,7 @@

      ${_("Account Navigation")}

      % endif

    diff --git a/cms/templates/widgets/user_dropdown.html b/cms/templates/widgets/user_dropdown.html index 1b05fb24cfe3..a59fc3b75bd5 100644 --- a/cms/templates/widgets/user_dropdown.html +++ b/cms/templates/widgets/user_dropdown.html @@ -39,9 +39,6 @@

    - <% - logout_url = settings.LMS_ROOT_URL + '/logout' - %>
    diff --git a/lms/envs/test.py b/lms/envs/test.py index d5f094f6eb26..17959b4e3ad5 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -560,6 +560,10 @@ LMS_ROOT_URL = "http://localhost:8000" +# TODO (felipemontoya): This key is only needed during lettuce tests. +# To be removed during https://openedx.atlassian.net/browse/DEPR-19 +FRONTEND_LOGOUT_URL = LMS_ROOT_URL + '/logout' + ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/' ENTERPRISE_API_URL = 'http://enterprise.example.com/enterprise/api/v1/' ENTERPRISE_CONSENT_API_URL = 'http://enterprise.example.com/consent/api/v1/' diff --git a/openedx/core/djangoapps/user_authn/views/logout.py b/openedx/core/djangoapps/user_authn/views/logout.py index c1767e310d40..043c7e500641 100644 --- a/openedx/core/djangoapps/user_authn/views/logout.py +++ b/openedx/core/djangoapps/user_authn/views/logout.py @@ -49,11 +49,11 @@ def dispatch(self, request, *args, **kwargs): logout(request) - # If we don't need to deal with OIDC logouts, just redirect the user. - if self.oauth_client_ids: - response = super(LogoutView, self).dispatch(request, *args, **kwargs) - else: + # If we are using studio logout directly and there is not OIDC logouts we can just redirect the user + if settings.FEATURES.get('DISABLE_STUDIO_SSO_OVER_LMS', False) and not self.oauth_client_ids: response = redirect(self.target) + else: + response = super(LogoutView, self).dispatch(request, *args, **kwargs) # Clear the cookie used by the edx.org marketing site delete_logged_in_cookies(response) From e3830d244399e436d682b8c5be95e2522e0ebed7 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Tue, 12 Feb 2019 11:03:55 -0500 Subject: [PATCH 029/119] Additionally logout from a settings list of extra logout URIs Currently, the LMS logout endpoint should iframe in the logout pages of all the IDAs you were logged into. In short, this was made possible with DOP because keeping track of the logout URIs and leaving a trail of evidence in the user cookies was part of what we added in our fork of DOP. In the case of DOT, we don't have time or desire to fork DOT to mirror this behavior, so our stop-gap solution is to log out the user from a list of logout URIs in settings. (cherry picked from commit 10afe5e52f19e045863243e992f3433ce36b482a) --- .../contentstore/tests/test_contentstore.py | 2 +- cms/envs/common.py | 2 + lms/djangoapps/courseware/tests/helpers.py | 3 +- lms/envs/common.py | 4 ++ lms/envs/devstack.py | 4 ++ .../external_auth/tests/test_ssl.py | 4 +- .../djangoapps/user_authn/views/logout.py | 22 ++++++- .../user_authn/views/tests/test_login.py | 11 ++-- .../user_authn/views/tests/test_logout.py | 66 ++++++++++++++++++- 9 files changed, 105 insertions(+), 13 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index cf7123020191..a4b76ced37a5 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -2200,7 +2200,7 @@ def test_login(self): def test_logout(self): # Logout redirects. - self._test_page("/logout", 302) + self._test_page("/logout", 200) @override_switch( '{}.{}'.format(waffle.WAFFLE_NAMESPACE, waffle.ENABLE_ACCESSIBILITY_POLICY_PAGE), diff --git a/cms/envs/common.py b/cms/envs/common.py index a46ca5731dbf..ee7d9fce5bdd 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -141,6 +141,8 @@ RETIREMENT_SERVICE_WORKER_USERNAME, RETIREMENT_STATES, + IDA_LOGOUT_URI_LIST, + # Methods to derive settings _make_mako_template_dirs, _make_locale_paths, diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index e8472a01a54e..787a1e3653f0 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -208,8 +208,7 @@ def logout(self): Logout; check that the HTTP response code indicates redirection as expected. """ - # should redirect - self.assert_request_status_code(302, reverse('logout')) + self.assert_request_status_code(200, reverse('logout')) def create_account(self, username, email, password): """ diff --git a/lms/envs/common.py b/lms/envs/common.py index 28f77bff388e..919f90c6e070 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -66,6 +66,10 @@ # This setting is used when a site does not define its own choices via site configuration MANUAL_ENROLLMENT_ROLE_CHOICES = ['Learner', 'Support', 'Partner'] +# List of logout URIs for each IDA that the learner should be logged out of when they logout of the LMS. Only applies to +# IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = [] + # Features FEATURES = { 'DISPLAY_DEBUG_INFO_TO_STAFF': True, diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 662d9a19691c..3fa20c2cb80b 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -24,6 +24,10 @@ LMS_ROOT_URL = "http://localhost:8000" LMS_INTERNAL_ROOT_URL = LMS_ROOT_URL ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/' +IDA_LOGOUT_URI_LIST = [ + 'http://localhost:18130/logout/', # ecommerce + 'http://localhost:18150/logout/', # credentials +] ################################ LOGGERS ###################################### diff --git a/openedx/core/djangoapps/external_auth/tests/test_ssl.py b/openedx/core/djangoapps/external_auth/tests/test_ssl.py index 056211ed6400..ea8dc31f8896 100644 --- a/openedx/core/djangoapps/external_auth/tests/test_ssl.py +++ b/openedx/core/djangoapps/external_auth/tests/test_ssl.py @@ -2,9 +2,9 @@ Provides unit tests for SSL based authentication portions of the external_auth app. """ -# pylint: disable=no-member from contextlib import contextmanager import copy +from unittest import skip from mock import Mock, patch from django.conf import settings @@ -395,6 +395,8 @@ def test_ssl_cms_redirection(self): response.redirect_chain[-1]) self.assertIn(SESSION_KEY, self.client.session) + @skip("This is causing tests to fail for DOP deprecation. Skip this test" + "because we are deprecating external_auth anyway (See DEPR-6 for more info).") @skip_unless_lms @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE) def test_ssl_logout(self): diff --git a/openedx/core/djangoapps/user_authn/views/logout.py b/openedx/core/djangoapps/user_authn/views/logout.py index 043c7e500641..e040770b5337 100644 --- a/openedx/core/djangoapps/user_authn/views/logout.py +++ b/openedx/core/djangoapps/user_authn/views/logout.py @@ -26,6 +26,14 @@ class LogoutView(TemplateView): # Keep track of the page to which the user should ultimately be redirected. default_target = reverse_lazy('cas-logout') if settings.FEATURES.get('AUTH_USE_CAS') else '/' + def post(self, request, *args, **kwargs): + """ + Proxy to the GET handler. + + TODO: remove GET as an allowed method, and update all callers to use POST. + """ + return self.get(request, *args, **kwargs) + @property def target(self): """ @@ -80,13 +88,23 @@ def get_context_data(self, **kwargs): context = super(LogoutView, self).get_context_data(**kwargs) # Create a list of URIs that must be called to log the user out of all of the IDAs. - uris = Client.objects.filter(client_id__in=self.oauth_client_ids, - logout_uri__isnull=False).values_list('logout_uri', flat=True) + uris = [] + + # Add the logout URIs for IDAs that the user was logged into (according to the session). This line is specific + # to DOP. + uris += Client.objects.filter(client_id__in=self.oauth_client_ids, + logout_uri__isnull=False).values_list('logout_uri', flat=True) + + # Add the extra logout URIs from settings. This is added as a stop-gap solution for sessions that were + # established via DOT. + uris += settings.IDA_LOGOUT_URI_LIST referrer = self.request.META.get('HTTP_REFERER', '').strip('/') logout_uris = [] for uri in uris: + # Only include the logout URI if the browser didn't come from that IDA's logout endpoint originally, + # avoiding a double-logout. if not referrer or (referrer and not uri.startswith(referrer)): logout_uris.append(self._build_logout_url(uri)) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index 23ffa1a90a9e..f97bfd206e17 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -208,7 +208,7 @@ def test_logout_logging(self): logout_url = reverse('logout') with patch('student.models.AUDIT_LOG') as mock_audit_log: response = self.client.post(logout_url) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) self._assert_audit_log(mock_audit_log, 'info', [u'Logout', u'test']) def test_login_user_info_cookie(self): @@ -256,7 +256,10 @@ def test_unicode_mktg_cookie_names(self): self._assert_response(response, success=True) response = self.client.post(reverse('logout')) - self.assertRedirects(response, "/") + expected = { + 'target': '/', + } + self.assertDictContainsSubset(expected, response.context_data) @patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True}) def test_logout_logging_no_pii(self): @@ -265,7 +268,7 @@ def test_logout_logging_no_pii(self): logout_url = reverse('logout') with patch('student.models.AUDIT_LOG') as mock_audit_log: response = self.client.post(logout_url) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) self._assert_audit_log(mock_audit_log, 'info', [u'Logout']) self._assert_not_in_audit_log(mock_audit_log, 'info', [u'test']) @@ -398,7 +401,7 @@ def test_single_session_with_url_not_having_login_required_decorator(self): url = reverse('logout') response = client1.get(url) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) def test_change_enrollment_400(self): """ diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py index 16eff1eb4f35..f64a9c68b289 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py @@ -8,6 +8,7 @@ from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse +from mock import patch from edx_oauth2_provider.constants import AUTHORIZED_CLIENTS_SESSION_KEY from edx_oauth2_provider.tests.factories import ( ClientFactory, @@ -72,11 +73,17 @@ def test_logout_redirect_success(self, redirect_url, host): redirect_url=redirect_url ) response = self.client.get(url, HTTP_HOST=host) - self.assertRedirects(response, redirect_url, fetch_redirect_response=False) + expected = { + 'target': redirect_url, + } + self.assertDictContainsSubset(expected, response.context_data) def test_no_redirect_supplied(self): response = self.client.get(reverse('logout'), HTTP_HOST='testserver') - self.assertRedirects(response, '/', fetch_redirect_response=False) + expected = { + 'target': '/', + } + self.assertDictContainsSubset(expected, response.context_data) @ddt.data( ('https://www.amazon.org', 'edx.org'), @@ -88,7 +95,10 @@ def test_logout_redirect_failure(self, redirect_url, host): redirect_url=redirect_url ) response = self.client.get(url, HTTP_HOST=host) - self.assertRedirects(response, '/', fetch_redirect_response=False) + expected = { + 'target': '/', + } + self.assertDictContainsSubset(expected, response.context_data) def test_client_logout(self): """ Verify the context includes a list of the logout URIs of the authenticated OpenID Connect clients. @@ -103,6 +113,56 @@ def test_client_logout(self): } self.assertDictContainsSubset(expected, response.context_data) + @patch( + 'django.conf.settings.IDA_LOGOUT_URI_LIST', + ['http://fake.ida1/logout', 'http://fake.ida2/accounts/logout', ] + ) + def test_client_logout_with_dot_idas(self): + """ + Verify the context includes a list of the logout URIs of the authenticated OpenID Connect clients AND OAuth2/DOT + clients. + + The list should only include URIs of the OIDC clients for which the user has been authenticated, and all the + configured DOT clients regardless of login status.. + """ + client = self._create_oauth_client() + response = self._assert_session_logged_out(client) + # Add the logout endpoints for the IDAs where auth was established via OIDC. + expected_logout_uris = [client.logout_uri + '?no_redirect=1'] + # Add the logout endpoints for the IDAs where auth was established via DOT/OAuth2. + expected_logout_uris += [ + 'http://fake.ida1/logout?no_redirect=1', + 'http://fake.ida2/accounts/logout?no_redirect=1', + ] + expected = { + 'logout_uris': expected_logout_uris, + 'target': '/', + } + self.assertDictContainsSubset(expected, response.context_data) + + @patch( + 'django.conf.settings.IDA_LOGOUT_URI_LIST', + ['http://fake.ida1/logout', 'http://fake.ida2/accounts/logout', ] + ) + def test_client_logout_with_dot_idas_and_no_oidc_idas(self): + """ + Verify the context includes a list of the logout URIs of the OAuth2/DOT clients, even if there are no currently + authenticated OpenID Connect clients. + + The list should include URIs of all the configured DOT clients. + """ + response = self.client.get(reverse('logout')) + # Add the logout endpoints for the IDAs where auth was established via DOT/OAuth2. + expected_logout_uris = [ + 'http://fake.ida1/logout?no_redirect=1', + 'http://fake.ida2/accounts/logout?no_redirect=1', + ] + expected = { + 'logout_uris': expected_logout_uris, + 'target': '/', + } + self.assertDictContainsSubset(expected, response.context_data) + def test_filter_referring_service(self): """ Verify that, if the user is directed to the logout page from a service, that service's logout URL is not included in the context sent to the template. From 3a389d5efebf16b7243bd25c9a35c8bd1b367931 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 13 Feb 2019 12:08:42 -0500 Subject: [PATCH 030/119] Load IDA_LOGOUT_URI_LIST in all the necessary places I missed the LMS production settings, and Studio in its entirety. (cherry picked from commit 11c3588fcc56411b24f447a368af78dc172f4851) --- cms/envs/aws.py | 4 ++++ cms/envs/common.py | 4 ++++ cms/envs/devstack.py | 5 +++++ cms/envs/production.py | 4 ++++ lms/envs/aws.py | 4 ++++ lms/envs/production.py | 4 ++++ 6 files changed, 25 insertions(+) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 875454baf19f..14e76e83fa22 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -144,6 +144,10 @@ ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', LMS_INTERNAL_ROOT_URL + '/consent/api/v1/') # Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file. +# List of logout URIs for each IDA that the learner should be logged out of when they logout of +# Studio. Only applies to IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) + SITE_NAME = ENV_TOKENS['SITE_NAME'] ALLOWED_HOSTS = [ diff --git a/cms/envs/common.py b/cms/envs/common.py index ee7d9fce5bdd..109c0f5491a0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -446,6 +446,10 @@ FRONTEND_LOGOUT_URL = lambda settings: settings.LMS_ROOT_URL + '/logout' derived('FRONTEND_LOGOUT_URL') +# List of logout URIs for each IDA that the learner should be logged out of when they logout of +# Studio. Only applies to IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = [] + # These are standard regexes for pulling out info like course_ids, usage_ids, etc. # They are used so that URLs with deprecated-format strings still work. from lms.envs.common import ( diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index d9fc25b8f481..6bc302c61e24 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -166,6 +166,11 @@ def should_show_debug_toolbar(request): ), }) +IDA_LOGOUT_URI_LIST = [ + 'http://localhost:18130/logout/', # ecommerce + 'http://localhost:18150/logout/', # credentials +] + ##################################################################### from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.DEVSTACK) diff --git a/cms/envs/production.py b/cms/envs/production.py index 86915074a452..3f33bb57b7fb 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -146,6 +146,10 @@ ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', LMS_INTERNAL_ROOT_URL + '/consent/api/v1/') # Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file. +# List of logout URIs for each IDA that the learner should be logged out of when they logout of +# Studio. Only applies to IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) + SITE_NAME = ENV_TOKENS['SITE_NAME'] ALLOWED_HOSTS = [ diff --git a/lms/envs/aws.py b/lms/envs/aws.py index d828f4d69ce9..20ff1231c393 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -175,6 +175,10 @@ LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL') LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) +# List of logout URIs for each IDA that the learner should be logged out of when they logout of the LMS. Only applies to +# IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) + ENV_FEATURES = ENV_TOKENS.get('FEATURES', {}) for feature, value in ENV_FEATURES.items(): FEATURES[feature] = value diff --git a/lms/envs/production.py b/lms/envs/production.py index 4bea786ac863..a7470fd4ced4 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -180,6 +180,10 @@ LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL') LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL) +# List of logout URIs for each IDA that the learner should be logged out of when they logout of the LMS. Only applies to +# IDA for which the social auth flow uses DOT (Django OAuth Toolkit). +IDA_LOGOUT_URI_LIST = ENV_TOKENS.get('IDA_LOGOUT_URI_LIST', []) + ENV_FEATURES = ENV_TOKENS.get('FEATURES', {}) for feature, value in ENV_FEATURES.items(): FEATURES[feature] = value From 0d1a7e1a7200db172431ed13daf95c9c228c553b Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Tue, 25 Dec 2018 23:18:56 +0530 Subject: [PATCH 031/119] Fix issue with multiple responses for a single user returned by generate_report_data Before this fix if an XBlock's generate_report_data method returned multiple responses for the same user from a single user state, the report generator would end up overwriting each response over the previous one such that only the last response would be preserved. (cherry picked from commit a76c6dca87bda71dc23792818f5c04c2012239ac) --- .../instructor_task/tasks_helper/grades.py | 45 +++++++++++-------- .../tests/test_tasks_helper.py | 21 ++++++--- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index db3cabb90b54..a9d8c439f41e 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -3,7 +3,7 @@ """ import logging import re -from collections import OrderedDict +from collections import defaultdict, OrderedDict from datetime import datetime from itertools import chain, izip, izip_longest from time import time @@ -615,15 +615,15 @@ def _build_problem_list(cls, course_blocks, root, path=None): Tuple[str, List[str], UsageKey]: tuple of a block's display name, path, and usage key """ - display_name = course_blocks.get_xblock_field(root, 'display_name') + name = course_blocks.get_xblock_field(root, 'display_name') or root.category if path is None: - path = [display_name] + path = [name] - yield display_name, path, root + yield name, path, root for block in course_blocks.get_children(root): - display_name = course_blocks.get_xblock_field(block, 'display_name') - for result in cls._build_problem_list(course_blocks, block, path + [display_name]): + name = course_blocks.get_xblock_field(block, 'display_name') or block.category + for result in cls._build_problem_list(course_blocks, block, path + [name]): yield result @classmethod @@ -664,33 +664,42 @@ def _build_student_data(cls, user_id, course_key, usage_key_str): continue block = store.get_item(block_key) - generated_report_data = {} + generated_report_data = defaultdict(list) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block(block_key) - generated_report_data = { - username: state - for username, state in - block.generate_report_data(user_state_iterator, max_count) - } + for username, state in block.generate_report_data(user_state_iterator, max_count): + generated_report_data[username].append(state) except NotImplementedError: pass - responses = list_problem_responses(course_key, block_key, max_count) + responses = [] - student_data += responses - for response in responses: + for response in list_problem_responses(course_key, block_key, max_count): response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(path) # A machine-friendly location for the current block response['block_key'] = str(block_key) - user_data = generated_report_data.get(response['username'], {}) - response.update(user_data) - student_data_keys = student_data_keys.union(user_data.keys()) + # A block that has a single state per user can contain multiple responses + # within the same state. + user_states = generated_report_data.get(response['username'], []) + if user_states: + # For each response in the block, copy over the basic data like the + # title, location, block_key and state, and add in the responses + for user_state in user_states: + user_response = response.copy() + user_response.update(user_state) + student_data_keys = student_data_keys.union(user_state.keys()) + responses.append(user_response) + else: + responses.append(response) + + student_data += responses + if max_count is not None: max_count -= len(responses) if max_count <= 0: diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 858ed24245c8..3c22788aebf0 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -558,24 +558,35 @@ def test_build_student_data_for_block_with_mock_generate_report_data(self, mock_ """ self.define_option_problem(u'Problem1') self.submit_student_answer(self.student.username, u'Problem1', ['Option 1']) - state = {'some': 'state', 'more': 'state!'} + state1 = {'some': 'state1', 'more': 'state1!'} + state2 = {'some': 'state2', 'more': 'state2!'} mock_generate_report_data.return_value = iter([ - ('student', state), + ('student', state1), + ('student', state2), ]) student_data, _ = ProblemResponses._build_student_data( user_id=self.instructor.id, course_key=self.course.id, usage_key_str=str(self.course.location), ) - self.assertEquals(len(student_data), 1) + self.assertEquals(len(student_data), 2) self.assertDictContainsSubset({ 'username': 'student', 'location': 'test_course > Section > Subsection > Problem1', 'block_key': 'i4x://edx/1.23x/problem/Problem1', 'title': 'Problem1', - 'some': 'state', - 'more': 'state!', + 'some': 'state1', + 'more': 'state1!', }, student_data[0]) + self.assertDictContainsSubset({ + 'username': 'student', + 'location': 'test_course > Section > Subsection > Problem1', + 'block_key': 'i4x://edx/1.23x/problem/Problem1', + 'title': 'Problem1', + 'some': 'state2', + 'more': 'state2!', + }, student_data[1]) + self.assertEquals(student_data[0]['state'], student_data[1]['state']) def test_build_student_data_for_block_with_real_generate_report_data(self): """ From 71b47af4b3a3a9344b3c4fb47cd0879996bb8102 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Thu, 4 Apr 2019 22:15:17 -0400 Subject: [PATCH 032/119] Fix SSO Login when JWT_PRIVATE_SIGNING_JWK is not set --- openedx/core/djangoapps/user_authn/cookies.py | 38 ++++++++++++------- .../user_authn/tests/test_cookies.py | 28 +++++++++----- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/openedx/core/djangoapps/user_authn/cookies.py b/openedx/core/djangoapps/user_authn/cookies.py index d36d299a3d00..37c90ead55da 100644 --- a/openedx/core/djangoapps/user_authn/cookies.py +++ b/openedx/core/djangoapps/user_authn/cookies.py @@ -57,7 +57,7 @@ def are_logged_in_cookies_set(request): """ Check whether the request has logged in cookies set. """ - if settings.FEATURES.get('DISABLE_SET_JWT_COOKIES_FOR_TESTS', False): + if _are_jwt_cookies_disabled(): cookies_that_should_exist = DEPRECATED_LOGGED_IN_COOKIE_NAMES else: cookies_that_should_exist = ALL_LOGGED_IN_COOKIE_NAMES @@ -252,19 +252,7 @@ def _get_user_info_cookie_data(request, user): def _create_and_set_jwt_cookies(response, request, cookie_settings, user=None, refresh_token=None): """ Sets a cookie containing a JWT on the response. """ - # Skip setting JWT cookies for most unit tests, since it raises errors when - # a login oauth client cannot be found in the database in ``_get_login_oauth_client``. - # This solution is not ideal, but see https://github.com/edx/edx-platform/pull/19180#issue-226706355 - # for a discussion of alternative solutions that did not work or were halted. - if settings.FEATURES.get('DISABLE_SET_JWT_COOKIES_FOR_TESTS', False): - return - - # For Ironwood, we don't set JWK settings by default. Make sure we don't fail trying - # to use empty settings. This means by default, micro-frontends won't work, but Ironwood - # has none. Also, OAuth scopes won't work, but that is still a new and specialized feature. - # Installations that need them can create JWKs and add them to the settings. - private_signing_jwk = settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'] - if private_signing_jwk == "None" or not private_signing_jwk: + if _are_jwt_cookies_disabled(): return # For security reasons, the JWT that is embedded inside the cookie expires @@ -337,3 +325,25 @@ def _get_login_oauth_client(): raise AuthFailedError( u"OAuth Client for the Login service, '{}', is not configured.".format(login_client_id) ) + + +def _are_jwt_cookies_disabled(): + """ + Returns whether the use of JWT cookies is disabled. + """ + # Skip JWT cookies for most unit tests, since it raises errors when + # a login oauth client cannot be found in the database in ``_get_login_oauth_client``. + # This solution is not ideal, but see https://github.com/edx/edx-platform/pull/19180#issue-226706355 + # for a discussion of alternative solutions that did not work or were halted. + if settings.FEATURES.get('DISABLE_SET_JWT_COOKIES_FOR_TESTS', False): + return True + + # For Ironwood, we don't set JWK settings by default. Make sure we don't fail trying + # to use empty settings. This means by default, micro-frontends won't work, but Ironwood + # has none. Also, OAuth scopes won't work, but that is still a new and specialized feature. + # Installations that need them can create JWKs and add them to the settings. + private_signing_jwk = settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'] + if private_signing_jwk == "None" or not private_signing_jwk: + return True + + return False diff --git a/openedx/core/djangoapps/user_authn/tests/test_cookies.py b/openedx/core/djangoapps/user_authn/tests/test_cookies.py index 3d0e1fcdcb08..5036d5a871f5 100644 --- a/openedx/core/djangoapps/user_authn/tests/test_cookies.py +++ b/openedx/core/djangoapps/user_authn/tests/test_cookies.py @@ -1,6 +1,9 @@ # pylint: disable=missing-docstring from __future__ import unicode_literals +import itertools + +import ddt from mock import MagicMock, patch import six from django.conf import settings @@ -17,6 +20,7 @@ from student.tests.factories import UserFactory, AnonymousUserFactory +@ddt.ddt class CookieTests(TestCase): def setUp(self): super(CookieTests, self).setUp() @@ -117,16 +121,20 @@ def test_set_logged_in_jwt_cookies(self): self._assert_consistent_expires(response) self._assert_recreate_jwt_from_cookies(response, can_recreate=True) - @patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False}) - def test_delete_and_are_logged_in_cookies_set(self): - setup_login_oauth_client() - response = cookies_api.set_logged_in_cookies(self.request, HttpResponse(), self.user) - self._copy_cookies_to_request(response, self.request) - self.assertTrue(cookies_api.are_logged_in_cookies_set(self.request)) - - cookies_api.delete_logged_in_cookies(response) - self._copy_cookies_to_request(response, self.request) - self.assertFalse(cookies_api.are_logged_in_cookies_set(self.request)) + @ddt.data(*itertools.product([True, False], [True, False])) + @ddt.unpack + def test_delete_and_are_logged_in_cookies_set(self, jwt_cookies_disabled, jwk_is_set): + jwt_private_signing_jwk = settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'] if jwk_is_set else None + with patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": jwt_cookies_disabled}): + with patch.dict("django.conf.settings.JWT_AUTH", {"JWT_PRIVATE_SIGNING_JWK": jwt_private_signing_jwk}): + setup_login_oauth_client() + response = cookies_api.set_logged_in_cookies(self.request, HttpResponse(), self.user) + self._copy_cookies_to_request(response, self.request) + self.assertTrue(cookies_api.are_logged_in_cookies_set(self.request)) + + cookies_api.delete_logged_in_cookies(response) + self._copy_cookies_to_request(response, self.request) + self.assertFalse(cookies_api.are_logged_in_cookies_set(self.request)) @patch.dict("django.conf.settings.FEATURES", {"DISABLE_SET_JWT_COOKIES_FOR_TESTS": False}) def test_refresh_jwt_cookies(self): From ac0d4055ec0c3cb024d47ef250579723695fffac Mon Sep 17 00:00:00 2001 From: Michael Youngstrom Date: Thu, 18 Apr 2019 10:44:45 -0400 Subject: [PATCH 033/119] Fix slack messaging in pipelines (cherry picked from commit 640ff4ed109ee9989bf49bd012dcbb28438cf582) --- scripts/Jenkinsfiles/bokchoy | 6 ++++-- scripts/Jenkinsfiles/python | 6 ++++-- scripts/Jenkinsfiles/quality | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/Jenkinsfiles/bokchoy b/scripts/Jenkinsfiles/bokchoy index d0639e24ce06..2d92adabd0a2 100644 --- a/scripts/Jenkinsfiles/bokchoy +++ b/scripts/Jenkinsfiles/bokchoy @@ -114,7 +114,8 @@ pipeline { propagate: false, wait: false if (currentBuild.currentResult != "SUCCESS"){ - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" email_body = "See: <${BUILD_URL}>\n\nChanges:\n" change_sets = currentBuild.changeSets @@ -128,7 +129,8 @@ pipeline { emailext body: email_body, subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index 412e9a6e55c9..e2b11c1a5e32 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -219,7 +219,8 @@ pipeline { propagate: false, wait: false if (currentBuild.currentResult != "SUCCESS"){ - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" email_body = "See: <${BUILD_URL}>\n\nChanges:\n" change_sets = currentBuild.changeSets @@ -233,7 +234,8 @@ pipeline { emailext body: email_body, subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } diff --git a/scripts/Jenkinsfiles/quality b/scripts/Jenkinsfiles/quality index 396b2f258333..07a94aaae2e3 100644 --- a/scripts/Jenkinsfiles/quality +++ b/scripts/Jenkinsfiles/quality @@ -231,7 +231,8 @@ pipeline { propagate: false, wait: false if (currentBuild.currentResult != "SUCCESS"){ - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: ${currentBuild.currentResult} after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" email_body = "See: <${BUILD_URL}>\n\nChanges:\n" change_sets = currentBuild.changeSets @@ -245,7 +246,8 @@ pipeline { emailext body: email_body, subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { - slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" + slackSend botUser: true, + message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' } From fd396be6919555619b0e34406788ae732898970a Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Wed, 10 Apr 2019 18:47:09 +0500 Subject: [PATCH 034/119] Fix Facebook and Google social auth buttons. Facebook emailed that edX doesn't comply with Platform Policy 8.3, also fixed Google button according to their policy. PROD-174 --- common/static/sass/_mixins.scss | 4 +- lms/static/sass/multicourse/_account.scss | 12 +++--- .../partials/lms/theme/_variables-v1.scss | 9 +++-- lms/static/sass/views/_login-register.scss | 38 +++++++++++++------ 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 0e2813edc4c4..c530acb4d54f 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -471,8 +471,8 @@ .icon-image { width: auto; height: auto; - max-height: 1.4em; - max-width: 1.4em; + max-height: 2em; + max-width: 2em; margin-top: -2px; } } diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 0899945a1496..e418ec72d67c 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -593,22 +593,22 @@ &.button-oa2-google-oauth2:hover, &.button-oa2-google-oauth2:focus { - background-color: $google-red; - border: 1px solid #a5382b; + background-color: $google-focus-blue; + border: 1px solid $google-focus-blue; } &.button-oa2-google-oauth2:hover { - box-shadow: 0 2px 1px 0 #8d3024; + box-shadow: 0 2px 1px 0 $google-focus-blue; } &.button-oa2-facebook:hover, &.button-oa2-facebook:focus { - background-color: $facebook-blue; - border: 1px solid #263a62; + background-color: $facebook-focus-blue; + border: 1px solid $facebook-focus-blue; } &.button-oa2-facebook:hover { - box-shadow: 0 2px 1px 0 #30487c; + box-shadow: 0 2px 1px 0 $facebook-focus-blue; } &.button-oa2-linkedin-oauth2:hover, diff --git a/lms/static/sass/partials/lms/theme/_variables-v1.scss b/lms/static/sass/partials/lms/theme/_variables-v1.scss index db3a5e8d447a..971b1951009e 100644 --- a/lms/static/sass/partials/lms/theme/_variables-v1.scss +++ b/lms/static/sass/partials/lms/theme/_variables-v1.scss @@ -186,10 +186,13 @@ $ui-notification-height: ($baseline*10); // social platforms $twitter-blue: #55acee; -$facebook-blue: #3b5998; +$facebook-blue: #4267b2; +$facebook-focus-blue: #29487d; $linkedin-blue: #0077b5; -$google-red: #d73924; -$microsoft-black: #000; +$google-blue: #4285f4; +$google-focus-blue: #287ae6; +$microsoft-black: #2f2f2f; +$microsoft-focus-black: #000; // shadows $shadow: rgba(0, 0, 0, 0.2) !default; diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index 274f75eb2d52..e0867237e02b 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -557,31 +557,43 @@ } &.button-oa2-google-oauth2 { - color: $google-red; + color: white; + border-color: $google-blue; + background-color: $google-blue; .icon { - background: $google-red; + background: transparent; + + .icon-image { + margin-left: 2px; + } } &:hover, &:focus { - background-color: $google-red; - border: 1px solid #a5382b; + background-color: $google-focus-blue; + border: 1px solid $google-focus-blue; color: $white; } } &.button-oa2-facebook { - color: $facebook-blue; + color: white; + border-color: $facebook-blue; + background-color: $facebook-blue; .icon { - background: $facebook-blue; + background: transparent; + + .icon-image { + margin-left: 2px; + } } &:hover, &:focus { - background-color: $facebook-blue; - border: 1px solid #263a62; + background-color: $facebook-focus-blue; + border: 1px solid $facebook-focus-blue; color: $white; } } @@ -602,16 +614,18 @@ } &.button-oa2-azuread-oauth2 { - color: $microsoft-black; + color: white; + border-color: $microsoft-black; + background-color: $microsoft-black; .icon { - background: $microsoft-black; + background: transparent; } &:hover, &:focus { - background-color: $microsoft-black; - border: 1px solid $microsoft-black; + background-color: $microsoft-focus-black; + border: 1px solid $microsoft-focus-black; color: $white; } } From bee111bd673863868a0d0f8664aeb3d2b23bb630 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Thu, 25 Apr 2019 16:13:51 +0500 Subject: [PATCH 035/119] Fix search to show courses one time The course_listing.js view injects courses for search in ul element containing class courses-listing. Removing this class from journal and journal bundles will solve the problem LEARNER-7210 (cherry picked from commit 14fdc28daa00d07d9176c9dbd85b6a2d7ca3e473) --- lms/templates/courses_list.html | 4 ++-- lms/templates/courseware/courses.html | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lms/templates/courses_list.html b/lms/templates/courses_list.html index f6448050aaff..5eeaeca2bba6 100644 --- a/lms/templates/courses_list.html +++ b/lms/templates/courses_list.html @@ -7,14 +7,14 @@ % if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
    -
      +
        % for bundle in journal_info.get('journal_bundles'):
      • <%include file="journals/bundle_card.html" args="bundle=bundle"/>
      • % endfor
      -
        +
          %for journal in journal_info.get('journals'):
        • <%include file="journals/journal_card.html" args="journal=journal" /> diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 1e0c7e391cff..06f6bbf2d37b 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -1,7 +1,8 @@ +<%page expression_filter="h"/> <%! import json from django.utils.translation import ugettext as _ - from openedx.core.djangolib.js_utils import dump_js_escaped_json + from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_json %> <%inherit file="../main.html" /> <% @@ -21,8 +22,8 @@ DiscoveryFactory( ${course_discovery_meanings | n, dump_js_escaped_json}, getParameterByName('search_query'), - "${user_language}", - "${user_timezone}" + "${user_language | n, js_escaped_string}", + "${user_timezone | n, js_escaped_string}" ); @@ -56,14 +57,14 @@ % endif
          -
            +
              % for bundle in journal_info.get('journal_bundles'):
            • <%include file="../journals/bundle_card.html" args="bundle=bundle" />
            • % endfor
            -
              +
                %for journal in journal_info.get('journals'):
              • <%include file="../journals/journal_card.html" args="journal=journal" /> From c6962c18000a070fc50d8d16641d0c4bd0ceedcc Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Mon, 13 May 2019 14:03:42 +0500 Subject: [PATCH 036/119] Revert "Making honor code not eligible for Certificate" This reverts commit 25130ae3c8333e5353ab88860c76619ebb9098f9. --- common/djangoapps/course_modes/models.py | 13 +- .../course_modes/tests/test_models.py | 2 +- lms/djangoapps/certificates/models.py | 1 - lms/djangoapps/courseware/tests/test_views.py | 195 ++++++------------ lms/djangoapps/courseware/views/views.py | 10 +- 5 files changed, 75 insertions(+), 146 deletions(-) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index c2645fdbee3c..1f6190819930 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -687,16 +687,13 @@ def min_course_price_for_currency(cls, course_id, currency): def is_eligible_for_certificate(cls, mode_slug): """ Returns whether or not the given mode_slug is eligible for a - certificate. Currently all modes other than 'audit' and `honor` - grant a certificate. Note that audit enrollments which existed - prior to December 2015 *were* given certificates, so there will - be GeneratedCertificate records with mode='audit' which are + certificate. Currently all modes other than 'audit' grant a + certificate. Note that audit enrollments which existed prior + to December 2015 *were* given certificates, so there will be + GeneratedCertificate records with mode='audit' which are eligible. """ - if mode_slug == cls.AUDIT or mode_slug == cls.HONOR: - return False - - return True + return mode_slug != cls.AUDIT def to_tuple(self): """ diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 46062cca5deb..d15eba64e2fa 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -456,7 +456,7 @@ def test_expiration_datetime_explicitly_set_to_none(self): @ddt.data( (CourseMode.AUDIT, False), - (CourseMode.HONOR, False), + (CourseMode.HONOR, True), (CourseMode.VERIFIED, True), (CourseMode.CREDIT_MODE, True), (CourseMode.PROFESSIONAL, True), diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 08ba906556f6..5f4ad230269b 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -90,7 +90,6 @@ class CertificateStatuses(object): auditing = 'auditing' audit_passing = 'audit_passing' audit_notpassing = 'audit_notpassing' - honor_passing = 'honor_passing' unverified = 'unverified' invalidated = 'invalidated' requesting = 'requesting' diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 6bc6c9256e68..667e681a24bd 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1351,7 +1351,7 @@ def test_view_certificate_link(self): course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", - mode='verified' + mode='honor' ) # Enable the feature, but do not enable it for this course @@ -1377,34 +1377,29 @@ def test_view_certificate_link(self): self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - resp = self._get_progress_page() + resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") + self.assertContains(resp, u"View Certificate") - self.assertContains(resp, u"earned a certificate for this course") - cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid) - self.assertContains(resp, cert_url) + self.assertContains(resp, u"earned a certificate for this course") + cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid) + self.assertContains(resp, cert_url) - # when course certificate is not active - certificates[0]['is_active'] = False - self.store.update_item(self.course, self.user.id) + # when course certificate is not active + certificates[0]['is_active'] = False + self.store.update_item(self.course, self.user.id) - resp = self._get_progress_page() - self.assertNotContains(resp, u"View Your Certificate") - self.assertNotContains(resp, u"You can now view your certificate") - self.assertContains(resp, "Your certificate is available") - self.assertContains(resp, "earned a certificate for this course.") + resp = self._get_progress_page() + self.assertNotContains(resp, u"View Your Certificate") + self.assertNotContains(resp, u"You can now view your certificate") + self.assertContains(resp, "Your certificate is available") + self.assertContains(resp, "earned a certificate for this course.") @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False}) @@ -1418,7 +1413,7 @@ def test_view_certificate_link_hidden(self): course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", - mode='verified' + mode='honor' ) # Enable the feature, but do not enable it for this course @@ -1427,19 +1422,13 @@ def test_view_certificate_link_hidden(self): # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - resp = self._get_progress_page() - self.assertContains(resp, u"Download Your Certificate") + resp = self._get_progress_page() + self.assertContains(resp, u"Download Your Certificate") @ddt.data( (True, 56), @@ -1509,7 +1498,7 @@ def test_show_certificate_request_button(self, course_mode, user_verified): resp = self._get_progress_page() - cert_button_hidden = course_mode in (CourseMode.AUDIT, CourseMode.HONOR) or \ + cert_button_hidden = course_mode is CourseMode.AUDIT or \ course_mode in CourseMode.VERIFIED_MODES and not user_verified self.assertEqual( @@ -1524,7 +1513,7 @@ def test_page_with_invalidated_certificate_with_html_view(self): re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations @@ -1543,22 +1532,17 @@ def test_page_with_invalidated_certificate_with_html_view(self): self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = { - 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} - } + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = { + 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} + } - resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") - self.assert_invalidate_certificate(generated_certificate) + resp = self._get_progress_page() + self.assertContains(resp, u"View Certificate") + self.assert_invalidate_certificate(generated_certificate) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_page_with_whitelisted_certificate_with_html_view(self): @@ -1567,7 +1551,7 @@ def test_page_with_whitelisted_certificate_with_html_view(self): appearing on dashboard """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations @@ -1591,22 +1575,17 @@ def test_page_with_whitelisted_certificate_with_html_view(self): course_id=self.course.id, whitelist=True ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = False - course_grade.summary = { - 'grade': 'Fail', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} - } + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = False + course_grade.summary = { + 'grade': 'Fail', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} + } - resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") - self.assert_invalidate_certificate(generated_certificate) + resp = self._get_progress_page() + self.assertContains(resp, u"View Certificate") + self.assert_invalidate_certificate(generated_certificate) @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) def test_page_with_invalidated_certificate_with_pdf(self): @@ -1615,44 +1594,17 @@ def test_page_with_invalidated_certificate_with_pdf(self): re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - - resp = self._get_progress_page() - self.assertContains(resp, u'Download Your Certificate') - self.assert_invalidate_certificate(generated_certificate) - - @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) - @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) - def test_message_for_audit_mode(self): - """ Verify that message appears on progress page, if learner is enrolled - in audit mode. - """ - user = UserFactory.create() - self.assertTrue(self.client.login(username=user.username, password='test')) - CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - response = self._get_progress_page() - - self.assertContains( - response, - u'You are enrolled in the audit track for this course. The audit track does not include a certificate.' - ) + resp = self._get_progress_page() + self.assertContains(resp, u'Download Your Certificate') + self.assert_invalidate_certificate(generated_certificate) @ddt.data( *itertools.product( @@ -1719,13 +1671,13 @@ def test_progress_without_course_duration_limits(self, course_mode): @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) - def test_message_for_honor_mode(self): + def test_message_for_audit_mode(self): """ Verify that message appears on progress page, if learner is enrolled - in honor mode. + in audit mode. """ user = UserFactory.create() self.assertTrue(self.client.login(username=user.username, password='test')) - CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.HONOR) + CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value @@ -1736,7 +1688,7 @@ def test_message_for_honor_mode(self): self.assertContains( response, - u'You are enrolled in the honor track for this course. The honor track does not include a certificate.' + u'You are enrolled in the audit track for this course. The audit track does not include a certificate.' ) def test_invalidated_cert_data(self): @@ -1744,7 +1696,7 @@ def test_invalidated_cert_data(self): Verify that invalidated cert data is returned if cert is invalidated. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) CertificateInvalidationFactory.create( @@ -1753,7 +1705,7 @@ def test_invalidated_cert_data(self): ) # Invalidate user certificate generated_certificate.invalidate() - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'invalidated') self.assertEqual(response.title, 'Your certificate has been invalidated') @@ -1762,17 +1714,11 @@ def test_downloadable_get_cert_data(self): Verify that downloadable cert data is returned if cert is downloadable. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', - return_value=self.mock_certificate_downloadable_status(is_downloadable=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', + return_value=self.mock_certificate_downloadable_status(is_downloadable=True)): + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'downloadable') self.assertEqual(response.title, 'Your certificate is available') @@ -1782,11 +1728,11 @@ def test_generating_get_cert_data(self): Verify that generating cert data is returned if cert is generating. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_generating=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'generating') self.assertEqual(response.title, "We're working on it...") @@ -1796,11 +1742,11 @@ def test_unverified_get_cert_data(self): Verify that unverified cert data is returned if cert is unverified. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_unverified=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'unverified') self.assertEqual(response.title, "Certificate unavailable") @@ -1810,16 +1756,11 @@ def test_request_get_cert_data(self): Verify that requested cert data is returned if cert is to be requested. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', - return_value=self.mock_certificate_downloadable_status()): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', + return_value=self.mock_certificate_downloadable_status()): + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'requesting') self.assertEqual(response.title, "Congratulations, you qualified for a certificate!") diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 73dabbce09e2..f1d5a1afb329 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -135,14 +135,6 @@ cert_web_view_url=None ) -HONOR_PASSING_CERT_DATA = CertData( - CertificateStatuses.honor_passing, - _('Your enrollment: Honor track'), - _('You are enrolled in the honor track for this course. The honor track does not include a certificate.'), - download_url=None, - cert_web_view_url=None -) - GENERATING_CERT_DATA = CertData( CertificateStatuses.generating, _("We're working on it..."), @@ -1075,7 +1067,7 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None): returns dict if course certificate is available else None. """ if not CourseMode.is_eligible_for_certificate(enrollment_mode): - return AUDIT_PASSING_CERT_DATA if enrollment_mode == CourseMode.AUDIT else HONOR_PASSING_CERT_DATA + return AUDIT_PASSING_CERT_DATA certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id) if course_grade is None: From c26d689745309d0eaed3510b18cffefa2fb8e884 Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Tue, 14 May 2019 15:34:40 +0500 Subject: [PATCH 037/119] Add feature flag to disable honor mode certificates. Added a feature flag to disable honor mode certificates for edx.org, by default set to false to allow honor mode certificates for open community. PROD-269 --- common/djangoapps/course_modes/models.py | 7 +++++- .../course_modes/tests/test_models.py | 23 ++++++++++++------- lms/djangoapps/certificates/models.py | 1 + lms/djangoapps/courseware/tests/test_views.py | 18 +++++++++------ lms/djangoapps/courseware/views/views.py | 15 +++++++++++- lms/envs/common.py | 12 ++++++++++ 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 1f6190819930..520958b47305 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -693,7 +693,12 @@ def is_eligible_for_certificate(cls, mode_slug): GeneratedCertificate records with mode='audit' which are eligible. """ - return mode_slug != cls.AUDIT + ineligible_modes = [cls.AUDIT] + + if settings.FEATURES['DISABLE_HONOR_CERTIFICATES']: + ineligible_modes.append(cls.HONOR) + + return mode_slug not in ineligible_modes def to_tuple(self): """ diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index d15eba64e2fa..7cb2e28ed0c6 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -455,17 +455,24 @@ def test_expiration_datetime_explicitly_set_to_none(self): self.assertIsNone(verified_mode.expiration_datetime) @ddt.data( - (CourseMode.AUDIT, False), - (CourseMode.HONOR, True), - (CourseMode.VERIFIED, True), - (CourseMode.CREDIT_MODE, True), - (CourseMode.PROFESSIONAL, True), - (CourseMode.NO_ID_PROFESSIONAL_MODE, True), + (False, CourseMode.AUDIT, False), + (False, CourseMode.HONOR, True), + (False, CourseMode.VERIFIED, True), + (False, CourseMode.CREDIT_MODE, True), + (False, CourseMode.PROFESSIONAL, True), + (False, CourseMode.NO_ID_PROFESSIONAL_MODE, True), + (True, CourseMode.AUDIT, False), + (True, CourseMode.HONOR, False), + (True, CourseMode.VERIFIED, True), + (True, CourseMode.CREDIT_MODE, True), + (True, CourseMode.PROFESSIONAL, True), + (True, CourseMode.NO_ID_PROFESSIONAL_MODE, True), ) @ddt.unpack - def test_eligible_for_cert(self, mode_slug, expected_eligibility): + def test_eligible_for_cert(self, disable_honor_cert, mode_slug, expected_eligibility): """Verify that non-audit modes are eligible for a cert.""" - self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility) + with override_settings(FEATURES={'DISABLE_HONOR_CERTIFICATES': disable_honor_cert}): + self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility) @ddt.data( (CourseMode.AUDIT, False), diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 5f4ad230269b..08ba906556f6 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -90,6 +90,7 @@ class CertificateStatuses(object): auditing = 'auditing' audit_passing = 'audit_passing' audit_notpassing = 'audit_notpassing' + honor_passing = 'honor_passing' unverified = 'unverified' invalidated = 'invalidated' requesting = 'requesting' diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 667e681a24bd..d9dcc6dc6ad7 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -93,6 +93,9 @@ QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES +FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy() +FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True + @attr(shard=5) class TestJumpTo(ModuleStoreTestCase): @@ -1671,13 +1674,15 @@ def test_progress_without_course_duration_limits(self, course_mode): @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) - def test_message_for_audit_mode(self): + @override_settings(FEATURES=FEATURES_WITH_DISABLE_HONOR_CERTIFICATE) + @ddt.data(CourseMode.AUDIT, CourseMode.HONOR) + def test_message_for_ineligible_mode(self, course_mode): """ Verify that message appears on progress page, if learner is enrolled - in audit mode. + in an ineligible mode. """ user = UserFactory.create() self.assertTrue(self.client.login(username=user.username, password='test')) - CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) + CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value @@ -1686,10 +1691,9 @@ def test_message_for_audit_mode(self): response = self._get_progress_page() - self.assertContains( - response, - u'You are enrolled in the audit track for this course. The audit track does not include a certificate.' - ) + expected_message = (u'You are enrolled in the {mode} track for this course. ' + u'The {mode} track does not include a certificate.').format(mode=course_mode) + self.assertContains(response, expected_message) def test_invalidated_cert_data(self): """ diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index f1d5a1afb329..923f180d959a 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -135,6 +135,19 @@ cert_web_view_url=None ) +HONOR_PASSING_CERT_DATA = CertData( + CertificateStatuses.honor_passing, + _('Your enrollment: Honor track'), + _('You are enrolled in the honor track for this course. The honor track does not include a certificate.'), + download_url=None, + cert_web_view_url=None +) + +INELIGIBLE_PASSING_CERT_DATA = { + CourseMode.AUDIT: AUDIT_PASSING_CERT_DATA, + CourseMode.HONOR: HONOR_PASSING_CERT_DATA +} + GENERATING_CERT_DATA = CertData( CertificateStatuses.generating, _("We're working on it..."), @@ -1067,7 +1080,7 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None): returns dict if course certificate is available else None. """ if not CourseMode.is_eligible_for_certificate(enrollment_mode): - return AUDIT_PASSING_CERT_DATA + return INELIGIBLE_PASSING_CERT_DATA.get(enrollment_mode) certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id) if course_grade is None: diff --git a/lms/envs/common.py b/lms/envs/common.py index 919f90c6e070..d3b1231bf346 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -183,6 +183,18 @@ # Toggle to enable certificates of courses on dashboard 'ENABLE_VERIFIED_CERTIFICATES': False, + # .. toggle_name: DISABLE_HONOR_CERTIFICATES + # .. toggle_type: feature_flag + # .. toggle_default: False + # .. toggle_description: Set to True to disable honor certificates. Typically used when your installation only allows verified certificates, like courses.edx.org. + # .. toggle_category: certificates + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2019-05-14 + # .. toggle_expiration_date: None + # .. toggle_tickets: https://openedx.atlassian.net/browse/PROD-269 + # .. toggle_status: supported + 'DISABLE_HONOR_CERTIFICATES': False, # Toggle to disable honor certificates + # for acceptance and load testing 'AUTOMATIC_AUTH_FOR_TESTING': False, From dc366b3ee47a020ae280b50d4c79418c2be9c224 Mon Sep 17 00:00:00 2001 From: Awais Jibran Date: Wed, 8 May 2019 17:38:51 +0500 Subject: [PATCH 038/119] Fix Elevation in permission over OAuth --- openedx/core/djangoapps/oauth_dispatch/jwt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx/core/djangoapps/oauth_dispatch/jwt.py b/openedx/core/djangoapps/oauth_dispatch/jwt.py index 519d07ec0914..d6b008c13244 100644 --- a/openedx/core/djangoapps/oauth_dispatch/jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/jwt.py @@ -183,6 +183,7 @@ def _attach_profile_claim(payload, user): 'family_name': user.last_name, 'given_name': user.first_name, 'administrator': user.is_staff, + 'superuser': user.is_superuser, }) From f0777ab5b69439a8dff958671f4ef035323ffdf7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 28 May 2019 09:04:25 -0400 Subject: [PATCH 039/119] Move a message to make it extractable (cherry picked from commit 7c6b49e460dce88b127d978a5aadbd2fc29fd478) --- lms/templates/problem.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/templates/problem.html b/lms/templates/problem.html index f4d196bc5e0f..b85a7cfdc5ca 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -115,11 +115,14 @@

                + <% + notification_message=_('Answers are displayed within the problem') + %> <%include file="problem_notifications.html" args=" notification_type='general', notification_icon='fa-info-circle', notification_name='show-answer', - notification_message=_('Answers are displayed within the problem'), + notification_message=notification_message, is_hidden=True" />

          From d39be559489e5e4f161906a042cc0737108ea9bb Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 30 May 2019 22:13:59 +0930 Subject: [PATCH 040/119] SE-985 Bump completion to 1.0.3 for Ironwood (#20369) * Bump completion to 1.0.3 Fixes authentication issue with mobile apps. * Merge pull request #19907 from edx/ormsbee/increment-dependencies Update edx-platform dependencies -- pulled constraints.txt only (cherry picked from commit 15b845a88197a0230cdacec50daba5384fe378cb) * Updates edx-platform dependencies * Pins edx-proctoring versions to work with Ironwood * Use edx-completion>=1.0.3,<2.0 Reverts previous changes, and restricts completion library to avoid incompatibilities that may be introduced for 2.0+. * Master uses completion 2.0.0 so use that here too --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 1c5354195a94..f18564ec15e1 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -111,7 +111,7 @@ edx-ace==0.1.10 edx-analytics-data-api-client==0.15.2 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 -edx-completion==1.0.1 +edx-completion==2.0.0 edx-django-oauth2-provider==1.3.5 edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 3f954ac785e4..3134b6dd96c4 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -129,7 +129,7 @@ edx-ace==0.1.10 edx-analytics-data-api-client==0.15.2 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 -edx-completion==1.0.1 +edx-completion==2.0.0 edx-django-oauth2-provider==1.3.5 edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 94487ac08071..9d55925b1a24 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -124,7 +124,7 @@ edx-ace==0.1.10 edx-analytics-data-api-client==0.15.2 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 -edx-completion==1.0.1 +edx-completion==2.0.0 edx-django-oauth2-provider==1.3.5 edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 From afbff0b72d75058d60843588d2b400a99584bed3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 16 May 2019 13:50:07 -0400 Subject: [PATCH 041/119] Use PLATFORM_NAME on the home page (cherry picked from commit cf2432c151fd110b5b004603f0fc640a08c2bacf) --- common/test/acceptance/pages/lms/index.py | 2 +- lms/templates/index_overlay.html | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/common/test/acceptance/pages/lms/index.py b/common/test/acceptance/pages/lms/index.py index e6dcb84d67bd..984c192621e6 100644 --- a/common/test/acceptance/pages/lms/index.py +++ b/common/test/acceptance/pages/lms/index.py @@ -22,7 +22,7 @@ def is_browser_on_page(self): Returns a browser query object representing the video modal element """ element = self.q(css=BANNER_SELECTOR) - return element.visible and element.text[0].startswith("Welcome to the Open edX") + return element.visible and element.text[0].startswith("Welcome to ") @property def banner_element(self): diff --git a/lms/templates/index_overlay.html b/lms/templates/index_overlay.html index 393cc7764e04..eb69e0532b62 100644 --- a/lms/templates/index_overlay.html +++ b/lms/templates/index_overlay.html @@ -5,7 +5,6 @@ from openedx.core.djangolib.markup import HTML, Text %> +

          ${Text(_(u"Welcome to {platform_name}")).format(platform_name=settings.PLATFORM_NAME)}

          ## Translators: 'Open edX' is a registered trademark, please keep this untranslated. See http://open.edx.org for more information. -

          ${Text(_(u"Welcome to the Open edX{registered_trademark} platform!")).format(registered_trademark=HTML("®"))}

          -## Translators: 'Open edX' is a registered trademark, please keep this untranslated. See http://open.edx.org for more information. -

          ${_("It works! This is the default homepage for this Open edX instance.")}

          +

          ${Text(_("It works! Powered by Open edX{registered_trademark}")).format(registered_trademark=HTML("®"))}

          From 32fb4748fef1d26428f6c14a452bfc0293fd2767 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 May 2019 15:15:49 -0400 Subject: [PATCH 042/119] A trademark-less favicon (cherry picked from commit a3baaf8336277eace18b39ce81471adf3f603a06) --- lms/static/images/favicon.ico | Bin 8168 -> 30237 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/lms/static/images/favicon.ico b/lms/static/images/favicon.ico index 1a379c82de946710c89fe908d5741403fdb1e403..0347608a33d73a38c7164dd1c22c32070dae19c9 100644 GIT binary patch literal 30237 zcmeHv2RxSD|L}!mm6=g?Dk~ugHz6}SBSNx55gKOJEker5D4Q}9A$zA}C32^-vMYP< zalhyCJf45Q{(2sF_4Mm~KkxPF+d0=c-+kuwJ?92NSP%{bKUfeGM9l(0M?qd({Ev4w z2?$aGWlT(eyr+;tkgW*>-8`u*yOVGaAwbp_8lg$da;BNjHVs{B@qrZT>@Q~DA?Z`-8C(XlsIw1&fcyus&m@4)FJ4i zsactOcvnog&{bkV|GtfX!9>q(AXYi8m;TM^^UMx2PjAgh=LksH2WF($;kGWy7Gp<~ zO`WX8X^`DV(jpS*EQR|VZ^3Q1h2V~bVdlxxR8c8Yo;Z{XWE4+ z1mmM+VUpWveDqveh1dp_35(G+0Wt0l`ncQOqqf=b(bwa*&`fNvRMXJEuYC8))gB#G z)|DHaP&iLnHcO|lEABa--kmbD=cMi@8Q%ySDi;siyF{1OW_C~XBLylbJCqX|R8#9D znUsN8#LIjGtr2%ZcQMYK3Y0$o$jg_Kv%}W2y8xRs;Z8*G#JPQS_)`7?^sEZ$vbLhn zKV{og8BwYDMG~Lm(}L9a9oK@Srn#{SxAm~0E?>Eofy=-`62e0NudnFF zvGe0DcqLgrQKkrg95xG5^zkEern|t+hAs4OXV+S2lFj`%Z-vJ|9ZvG42;mLY-3^qeK!snHp?H zSNrT@X>PHhW7Qa{D8DfptJoK&kRMJQOi*|Vl23be{C+w1_6Yx8&+xK<#}uhFIu{Uf zMFTQeY^g(jQdaG)?$wvu%z5AFE4BKBCV3<%*y+#gIo8&<((Zcw)3Gtcq@dga-JV5e zv$J%phr3luiUo&b_-j5;i4Rt;k-9)Ze2TJ{v)wb87#OQ_46P%S$rBD<#8RY`KcMF) z6)WC<0(BwkB6BI(I9`aS!fY#Sa_E_FR9$!xQ^qw!uJ%D6|Ls8zn3>0P*gZ~vZC8rB2pTvab= zQ3eR5!y|{F{VKCnoD1|dgMg{fsL9aKA%l-K0g8BXA*Hx+IUg@haF7unp|84(Bw;#w zJ=@UjDE&UNo=EK^E_n-AWDVa~x4lFH$sU)Rcci=ED!W`rCSZ=np;gI8;I+-J>I*R( zWouEs@g_;ZIX8~s8aSKC7TLRQ3km|_l+)h01t!*)GtL>-kFr71p?Kt@CHVd4P^xsv z4H}=Gn2y?4ip(1>5LF~pFQ07c(GF~k^zRiUP+!G~JbJr7BP%!Q!4cLDSWSGX7#yJv zAWtr0ild#@>(cPVPEbktp%|(MT_9`6{q2+kayJ{h;LViA!cERXYMLta~eO45dmx zkr5vSK53Td*C76^+VI>PanB#ykOSI(s?$&Hl^(5P^5E#>nR!Gs582)es=W!PpAE#_R(l4e-IkG zAIUF7_E0R@xyCQ)sC5|>BDzZJc{$10vjZoFE5J13k%EP9go629y>hEa#-YM@$A*~(JsPrK;9FJ9K2y2 zsT|BJ&7VvW#7VZ>FVf3}RqvIVb!@HPyEkU$x5Bw00S!Cm@@c|(f;dj3u!GyEv7xKZ z!O;U+w)=!6(k6~lIOd2U+G%+^@*on6oB>Q!DcUhfgYwW8w<4KRoXFHEX&c$JzX{zzTNMbyE>ht3!w|AO7k z1SN)-xO`3AG-*4nerpi%Xw30_-V773=bxgPO(fmw6avaD5PqRyB1OJuXXl&yJo%ZD zKfRGQj;F@$Y{$2CC)?5*#ueeT3_A|5JudJNYe?FOCbDiJmIx;_AFw$? zKsG3wY-gWfIXkGs3p-b)|4xq8vm-4673G0bm8X&(^hdE11$cb!h@#=qH1=R;oHJ@l zQ_?4j%NdG#B(`99RM(<>gxFQG&I=v7GuPv!7|Z<22H%c)PwmCykNJ;rAup5cYMSpZ z-%A%7HXE5#gxfN%&YRP0v&$^`j`ZE$+R}4Z^NqUD+D&}KiOIMCvsT+_;+fOJ&g5bb zv2z?=BA(;11kMHs`8JKgRtsh7Hm;CVYP7)RflCgqUva^CT`qA`)9U_p}6>tt#g zNP3WP4VHPY_RC&OUFE0?2I$^V zvXl3%x98>=2y5)pa-(nLJ6fW;r{QDz@}WL~{1=LNm)p7r9C=ID0$x8>(U&Sahfwa8 zQjXK=yr`M+CPa%;ioxL1o6fuCUNr>n>3lmhl2t_$p4;qTl~a$2Avj}=keTQei+eOt zrtyxvgS|raZa_nsN9+~*3Ehm561g~maDOtvS*smJR_+Fb6}h3Kq}di1#ahBcVg;%I z6UapJfQMW0AdNfupwd(KcZo$22880?$`p3~w6XhVl6QxEo7cifyfGSV-4*E|n-OGfoD z&8JFcUqd+n>^;mhXGnEoa99>3o<%Ik&K8^|BW-WyAtc)85njgPjy=I~D8*`gPpJ&|R08`Eg)+z?!Dg0smxg|C z`m&oLz-dybcOzbsMe$VENSW%aw8~J}8 zwredZR!8Vr_Sfyd?GzW6ev4HTUZ+V9DF|xZTUb-+h+|NGVs+@uEhBq@OFxYEWa0{!U9KX zut)DsF|6Js<9#Y+PZJg`hvM7kurEoPP)oIS3Vbs(H^EDVMVj{;mSRy&l`_Mr)2Qa zvs{R9bG5jYlMbCI4q_UNl^3CR<|r6wwggV5DspAAXfqsx(b314}PQ(l$qqgsgvVvHf8<>vXVUq+CJe-g4?>@&o zJKFI$gestKl^RkR;e;MCW{*$`)Fe}s%vw+(UC$w8Dm~S8B194furF1Oust+OfB3DQHbPHl@ z<3674RKbdC74VE$m{yX#bj7{A!sfas0iIe>_f;i*t1{?JekNW`&)}hHMe*YY+Z8Z4_;hTH$fnS)em-bwdp`Qg8KOUvuQ9tpYe7rNJHiP zk{S_ig)ncYHa3+RDp^6%`_h$~w@Y_3E&C06J|}VKo`hzm2Var14K#M}=fP8$cL%RI z#lt+kt3&k*b^cCenbue|df|98jT-mG@o&Mbn{OrI?33|ipr4BnvFXC*$?OYy+pwB^ zxqgi$>QP*o%X0MisO=s=chWj`+Oui8?=fhO{y49DpoTSl7erP}h|Ko7z%oK#)SC6~ zWe}tGyiHtl9XsU2P3pQ7F}O{cSY7j;e;{d)tCMTHB9VAN&FOmv1>SCS!6O`yeAlsZ z%a401J`j1vWH;MvWA(m?CP9&rKfvR=HZ#^&R%ija9FAZ;)n?H3AY6IQXIceR?j;|X zAhK7XFSEzGw;-hw$Ak*MleBQLYcg+;MwvLRYa-9~0q&>VuFkryVyOR9Ys(6?OF(<| z2oL?UTEqM^MLP#vP`um#Wf!porKHffiydOO; zXnl>a$ujoFGsPBLEW4M}{l>7)T$Iq$Y$~L)DdKt`saCORd{~$=R^}t<#ELqKj1KQ< z#_Gx38&pV3+zNXxh;`daf#q_~*zWQODkRQfx_$h-M}qDOXF zShu?tEZ;XlSwMDr4Y%!b(gp|4C12kTdW<;{7o=L8VM?_S4LTuR$q8y;0Si|Sqyaj` zo~*Rt?BM8&?IekbRFL^QcBHQ!dP3PQ+e++WDlSu4F7!Nqcu<;E_@>!A*OZH$vnHry ztkFG2x(UUgy*%J%ELxr%NXp0Ja>~#i*;DntR@tcYd|NRd&xLEWO&Qos8IT}zcY7YA zyaH}TcD!GA;9Kg;QAa2aazb3u*giU%&_Ro9LfUnCRN@NBkhwOIxKJhV$niHBtC>|6 zP@dklU4zQ6gYER(ZL{m2vtGMyvIil#3xYon{33hS24-6 z$9kVtTSkSva(xhAS$X-+AVjbf=s3Ni*EOOCSryvQKBVPiV5jT_?H6K+Yw%dp73D9E zG`Skh;(fBDn+eRkbVn|%F*B8dIi{)D$@Pgf7O7rz>b|EpW6lHZJnoaVi>_kn<$khn z$blRqY|e#iq+bze7D5m<~UOeRaB^ zJhklYOyWl{D2FeQP#hW9m7)9o8JOGfcAaAIsjCH(`pdfmO8VxhP)Wob&v6P7gw%*d zn9$r!J5lNjsk3yN4=+CM{8V7Ib~kWAG_;a%DB2ovJs4*f?ferr{6zIp?|eM_q4CC!RpX-UguA&YY*LG69LdX@-@D>Gs*Y35?XT#L}ynXa>(;+9Q8hb0PphSMx*GinJQyofJ9 z1om0%^0RBMC1`A(Pjxg8xwq|MyGnT8%%a6kB9Up_+|wB>^$%1=#*@?M&8Qp?XCB0aKS2hncd_Z?4&SGbpOk-;pyWgmL6l3%G+$Xmf3gTlV z%Vd@AKvP-jOb2skH-((D>)oNUH*T*<6stgiCNemL(==c99$40!R~GuxxtjB2_qfbC zF_2kZ@v*=aloR^gltv0787%B|rsn9*A-$43N~G6PvUudE+&NqoD}!E1da$cF*u3y2 zwlsF_vvG&5l8c2xZaVamZjp>*x%3{X)fN0vlAHxGRBgeS-T_^~;YPQrzVZw8MwW~% zQs+^Gk_x56?5RVRU}x-pCwXb_8L&5*nz2kG(QOOc-+mT4deL=O~>U(jFu5X#`l9! zo3HfE66Q&4(spOYqxw58zF{C__~_fJiaH)e3U3HLu$&fZYh-XPeUcdDrgO&utKh;* z(>GJP*P%)#V#LTNm>*9j%Fie2Jm4pqXyp>gy*7HMW8^?qkv5yF+W1b2c%M7cX7~Ji zXf)JZ4mix&ldHQO1&b}$>-_y%km+q_ym*e(c`t=+L4_c|T4O_6jT}yMV*YSuIcc#? zmVUOKqG%ysVXC~~g_Ximso~z4s`|+K?o=|AYSfM*BfAj0v-|gIHwkN?1%h)ljP32gead?jwH@fl!@W<~&7et310uc3W9vB7xS;{b z*j#&trypyu9D=&|Kv+8|SP+ZjAp`Q`F&vwt^!u4|M<&$QCU$HTnq1cbN`_o;NvE8$ox7xgz`Rw<-!qYd!yR0UpCg>!{>BH ze`;CayoiK8N)`J-QAB#KdxsZE29DIr*mHvyE0>vZChjZTJ7Ze_R3(?Eo~yRV-xb87 zC`%AxPL?0gO7{HWbxsIH)Fm?zQ}LFXL6+R_ph+EeF(X9-zd99zx71y1?{I(egL=wU zQ(f>Uf3digbpcsRBetFyjn9l%$^HIU74u!g<@mzT*&9{nXZRb~?OywQ?^?`%SHCRckjcI;ds=$H}N zE|fm8Q_4!EzUZ0pHu~a~@Y-9-AOxHodJy3;N56Kb1y5}0G=kXmgccU`zRZfFuK0qk z8x4Le3r9U$`Xc)S+4eQXLcQ%OekZe2&V72G+v-qM7@T3UJU2P0<8;ki>`*!a@1}^w zJWsu4qQZHf0OJ>#=zGd{W_`^?Rh~PM8U_xsU7!x2JZyPdsYo++4d<$SgQfQLXA=vJ z{5Zwl*!f1&4E-~0$2qCLrWKR;zh(49q*c$Ukxush=Dk> z<7-OsG2*=VN5@xnQpJSJJifAI}1#R`Y~vi3g)7ebfAz$qNZX>q$X~+xYg7jL~E;p9^j4%hkm@Ls8_O{lRDB`N3XB4=;-3cB5~6p?cyT9QroFlC!4a!*S|MGtv0( zXxd!`OYsnQ3(&erlx2np#W>IjE-l49nJl@5BqAL&N2ob0+*LpNrp7t)BVh%YzRIo) z1^es}2xES!p@l`nboN;hNf_^^x_g5s2#VvgiC+8b%a$-#;7fJvx~P=;;lv`Taqydn z(fh*dM{ngmHF7dnJ)^UC&uBO8XRAMrUZagah)=;VrufM!xmWrX^lJMtmikzHCBGrb z5R%21TKtd8pQ*(z^8^ud2ir;tBnDeaYZI^F&RDa#8LuvL^0hS@^imwV@NVo@MOKP4 znIY-k>O20t2eN~vM64|tDoc1}s$U<`hoGc`3)m6)FQciG#3#u<$lCJPIQgI%>h?$p z#1)b03-neCkWc56HCQv{WyLhK4~I{yMiF#QpLaVgy3^KCO86s1B2kQuY=W?aeMpSY z9FEYA0iM8Tq{K&7Idv6mchg!l4vg{YD#;SaVnJ-Fh@CfA7=o1HlZU0xSB5OgpDGz2 zjZ}t@C7Kp0WS{GXpyoZIM_S&${uIUIx0f>G_`xI+z3YsOH_cyMlMqstNhyn&5IXWoaHP8YM(?WZZk-^=;XJO<4(SAb+aJ<9H{4gNX zY8KZ}bb)z<3#ljni4k|Pac^@Yd}0);ro31$_N3?OAz?zOB4N=xP@hMPRpz?w#Z%tn z`pQ*JvPyUt(k{hfk6ZvJRj21aXsM>dCscFsMZ{uuc}jR<@84Q4=RTxS*e>!)+hto2 z_}1<&=hmGT)49Gp^A*2gpcmd zA8dB9zWc1%1fmz_vo)G_)X;nWq>5ug{8lB#VcF!VjO%BA4-)Fu;KRxW5>jg_t4vka zXpM0bkEGh0g%urC#DTGha3BANEy_+4c z-FcP!Jg77VL!bS;J#WlyT%lH*sE&l4Ot&WbNU|5BImU`!H0%rA1`gp*M4D#?nY zX1U@yDA=k**-z>0cF0zk0ffEYMk=2JHx;gHs9oi#WSfv-a(-$P^{DB+p&9Q(6}2v& z*l-rH!}RQt;2{1`BEKy1HY5X?6(y5V#52a1AkBB`4yP3wzd3-q311orhl8tAh!}#y zG(k_9^eb(Tws}}dXy2X^Is0t7mFH~?*BLAxL;K?suduf>N8RJ2h~p5=Bs}dubfsj0 zvm~c?r+xja9a@lt;lq5msoC+A5}8pitwM{7 z>4WE$S->re5mps?@#PYCDHA3h(sN98Wa9dsF-*)0Xp~aQqwRWrnR{B?+*dqKcgmZV zl(l&g9KNcJZ^y*S{(y5@@Nl_6(Dkx77t%q$Ci)ys4n6m1w*=1n(c)984U+rD%iV&# z8_`>#g^aarC3eXR3}(UjS_$@VnUkl^zqh*Py-srXPU3wy$LVM?3rub)bE`ezp&? zuR-|0w*Tfh$JEb-Kg$Ps4iFKred%{-TMTum{ZStzUWRah%vlWOeygpiFUQ3*LX$+*#a8=)8YG?i!$nZ}0z<2k3G12XsLLUBod_B+mun{a$&X&3rPQAZ$$J zZ3Q>&JcL!?3^{(MUaMP?v4I0*%zcj>;O9ddJpQc|j3ut`(GAeFZbc`6XZ{`hSHOOp zVk`WA1`hBu)pzKK2KytPzmuQi2yOpPzQ4@&4gG-+;B{jwbMTiqGaK}qrGVd6OnCx5 z1UBRKE6TuFV8d8n7fAM7x?#!-;75In*RLuE{ z;06zne~SU_iw(NpEd_m`90NP1H}uDV2L6^X-=O>5QqTu@FtBAA)*q~UG#JpJ!5)$D zce8s#AJ8^%tmc2_slU&!e-nZ4HB7$sW;bO0jw!Hz8wU2HVH*IxGzK*7zoXq>>V$zU z@K}b&i#i50KO9s0QrCYY4{Uk4ReM0+R|B~4IqX))Ov%5Y^Uvf7Y)aX(J1J9XX^TI@B)20WXtwALv{dHcgy(TJo?|z`DgM3_6Kd*9_RJ%eT^;SyFfqfT;XTh z{3S2Y|Kql55AfA_fGfLIe1KQ=rL5mE4|IICReON_3IO2qh^@|l?|(;|ztjoXvgNa? zd?)At=r8mb*gy59tlur4=d^xawbiy<-~YfiU|>Hw#|0wz-R$1b2ec)|``yLkqzAejVcCoe~p_B`rjc1 z{Spro8$e&VzM(gmH!%7J268+%Xn&U!*aPq7w>hEePm$Qb$_Ie=yyj4{a#U;eBVwB4~E&_rXtT!|(AW(Dh&WJqQ>J!r!CM zFQozZ!Tzotf3yzxb|woeSu>aKW;sekx{r*Ycf2$t&z~;Z` zhjg&VVEngi!-OyJ!5IKQyk2j`m!Q49K+KL76a9V_F4()U02?j=c(WB-wumze@PdKe zfBWtFt8Cn=oFqF48|V$6ujvB_1imQ-X%~Qb0AH_<@V;jd*jNkFI55v%0(nW$&Xik~ z_y6%Q_W=5@XAOeZ^B}a#2xlrF2nSAKo*-F|9EhRO>yckZG`bru$6H5RM?}K!STH&X zKtZo_gCMsRm`Mek0^A8mYyoBt{#$?muN8=K0&+tGWI%%k4@A#~*eiOs+q&KB z{s4YhfPmr?5aYU^x*@SO*q2D)90(Evu6F}@`1#Y{pspK!1wz-`1JJHV^%!BE;1L79 zK!Ow`FygvyfD0r1j{d`y>sd6K4PY?=5C`zH{!RoKzXt$Z1CRj#{;sJ4@DxB6fa?GR zfw-Ug0G^Bh;BTsF0AJ57o4qdsJa|6(=I45{~#N-#S;L0 z9=r$akw`#F0RaBJj4lAUKf-dq_1ntSZ}|c6g5yFk|F89i-{HF#*U~Qi^Y8xv9=tBr z0oaVs3@HC!XN;f>A3*vyXyD(>@_$S3&B_5jF@SH~mwFHCL^kL^9t!{*i`q=zNl^Y> zV;ZdSaJ$_52H(Hh0o23iyNR3N&4DtP4L!dn1@!v?yxxTOJ5aVhF2Oj5`_N{wnGfG1 z^GljEhjrg-Z6Y7G%?HK>sp2N}@IK1TfX#k~(E!TX0l=}cujTiD$>T@!fdA=DWVC}aIRNEf)xmw?r^MGb^alJyzmfsl z+YbPqYiqu$TLJHx8?-+o1^fg7!0Y0de*O&JO#p1nnHzO5-s7JkcY_Du2g`@&&vyC{cp250FDX6>%8?Q^5OA!@F&?0 zkH1@+$bj#YvjX7Tqz;~c{<=r=BYFTn@EC8|M8^7^Td+n8~A`0yl=K=f#U;b zHt;Y>0o@(|1pvOb3w}@ek{5V~ec=8LT-aA;U*i8$9^k|Flz)R?E3o%3+c$WDGC2MU z`(-nIVc+BfVA#O_Rtj(~0D#BOW_)0OuDAPU^q_7Z0NCEmXgA8>J-rVAEdY`L;Po4h z3uOR+_e&dio26-+$=xUeK7jXPhXBBOZbttjWm$PP(6$X(f0qJ1=z;vkpQHmkj<)pu z@ug0=j_cn+=74to2lRmV7(aAY3;wnO9P82nP`O1tV11(iV6tQ??kbnlJg?V%{F5pezRFJ^DVHqIy@mI9s{{Ww`YGVKZ literal 8168 zcmZ9Rbx_pb*T+8_G%QO>N;gP{h_Fa1g0v{oCEdLXOM^5h9Seelw17x1ASy~D(z0|) zcmDX!JbyhiXXc)B=bYEfne(1IbLYfpYpIbDGY|s+K=xQ&Sr-66w=M`EgxnsE6-N31 z06x)vs;_bz&d;vSO)dOe*zooZBzv>?f!b(%ad%;MwUo{{G6M5~^7;GkhxCbyg@r|w z@r%CxD0dH^A_@bq#=ZA&{QxA|yL-PQrieUtq2ZxV05bRswZnowMkY9k*-+5Fgw6D$1nx&du)>J+qRJ?^ld(S&eZ!4rSBn{x4w4OPI>vqE{g=ahD})V zKl;>FH&37cP`Bj#N%q`{m$lnK*vd!xS9fzKrE|AxM4UQIqSL9K5+`hdBi5l&8z^+x zEvL=sO}AM*GB~sh_M9$xT?(_UQMOql>)qVai5IQVR;V*2ZxxlvEumfB zzgY?O@nhAW$Z`iKx3Gv9?u_k5NsrqS{{U26e5zB^UWI_Yr>Bo_-o9MXZU*yn4=WJ}Bhl8>>>gfzmXs{i2I+(bneN`f1)wJ-M0zGP z%p_^&VD0oWT4O(S7$|yy;90V>s~3Pswc`~he39qn7lfjeCiUxmeeZrOg(9&{b#Pz+ z5V9^}mSkt|EN7iaY*ndk9~~Aq2)>nmrXldBKL}I)gB06U+nL8mcP=y8qSP}V7lJhTkCFlEW9{dlkQ_9FgG#MofvTa?Q>0K zak}31b?t|O9LHDJn{DY4rb;^#-#<4r)LQ6GH&y#O=v`l3Ud)VjcVbHm)BE11`&wHV z%gy)ob-##V7OaJFu6;P(}Fv zv#+jx=}rR%bP`?v!Zy5SR7kH`Mx0lOm4o8sPRY$%UOgS&vdU12Tfq1C=JoMj=QS&} z6+fuM68sw^SS*SVu7u-CUatZ zBG{%k111O3fLb$lO!uNMd5f&g%)DUG!)DPZPod_P#$aXVd#ox4H$xs@lo~UemW~X- z{DZf}(lof$^zI#i%7p**lS+tRPO*+|_>cUB2-&9+bb6rbaOE3@Z}x#+lt(QUqb5w; zpSTgfZNa7_F>#&Lg4y=r8}45Fg64mq(DoSMZZuo9Tt?(CyJ4YtD1wjvbtMrzkP2k% ze+lcG|A9YX3ua}m;#La;D;Ei)zvx~ZnBUz&(~76MQ+Hd<>SHM)5PDcuQbLsWeAGy| zC^ejn+(!>2r^Yr!iPJDf!+v{Ns@rp;$)0Ct6Qf}f2n__gHot*-sU^G@%cEPdX2Qs(SE$s5IWsbJel}wvYwd!Prt@%DMA%~^FoP1d<-$P zNSTBIbAg=cY2t3PV_xXNkS= zu!&@dR)c6ILk_kGJemf7Do87X-4$Dw60|XtfE{({mvd5~wE!tXWkmP#cngpE)vBX#Kd`MeYs!n`Y~`AHeu`-v4n z4}P`t7lCduXR?DGLlLo$FkEOxRr51aH(%HUHNZn1T6P}fPxb0AM2in?06Z{Qcas+U+5!qD1tef~0;zHWha=Cfuzgdn`M9j*jBz&};| zk^JHdFj0%E_7aHAFP$D-9*l zOb9T+iwBT|KcxkIs0#06T)hW<8jjd=JlonIUwF)QuT*9Nj^UP)GcBP78W2rU|2kIj zMuu1MJn((lfdmK85V6ue<+dt7ZUFAf&+Mg}Nd%%Lpolw!C{y<)9+38HLcoVIOF{^X zBHDE)R2@H4J?R|`q(a{gZjiWuvl_Jh4A@FWyN_HU#Vbq$-O|CA08s|o`Cz?+JQ~2s z<6$aceegsVPq_`1nw@{ir3Wj7@qa5lnF*8j6EFgcE`^>TkHw{LaFY%Vz9 zCf#)>IOuZMCryndPvPC)ebLs_y{+!|pQq6BoM>ngK#2gW14OJKPTZZq=M2PAv>UJjXK>E3WWp`_|75VeW>CNTWw1T}Yp zGNctJ`6c#)J1L_7Wiz8n&xL%@=?4NjpXR_!9lgSyD7+@|(Y}lGqs{SZvyMIr5j89m z$P?N}46e9&4hDiXAdxqEQ1!IOPe;+@)|_nEa&up9C5SaCXMqJ)mgsN?L zKmZmIqP711TpftVGtE3y2|5*XueZhFw4%>;wDGn8gr8v#{&-I5R=iO{rj)99+oJ4 zOy>?>+4Y{(Lwq8@D^U?#@bWzEf7Wqi6!@CG=$#oD<*fE}A{jkkGB9*AA5}LW`2y?$ zj)s82Vj%vUxq1bI{Kr-p*S~|G3-p&Q3}YwNj$V`<3vU*VCcn7q{)EQ~!hpw0ss@Uu z_bZlH*{Li$f)Y^6ZZC!3wOo5@6K6dPGuOXzEwwbNW-j(_MD*&)z?r zcutz8ARPy+2AY zrSJ!ujmBCWcn;MK*Ux(4r&$<$(5fy;v?bGC-uWWY?Q%HA6O{GT_7g#S=F^;j9(Af5 zbbi{jD3LNFDOkJwYM{Nsv{|e}Q_mJgv{vzc?N>;9MQqKw2=_yRY9S1iaF-&=b%?Lc zT#6)Xu%FL?>M)m{Qy}ZsahnRy?fKDXD}YnPm-G} z{A%>&^CMWEil(iLD)C$HzMpbtx-VMJ&H}gnufI+GOgVc(#dnIIpb5-CsG8r&=2b$} zY{KL{i$F!w@$LQ^ov0z zng4BnlU4yQpzQ2e;Lq1L3yL>JdGVk3ZwJQ&MTJT&%=w22-uHDS?1ec3T+*OnSP(YfNOfyES{&Yb-GxQA61PnJms?y6Qt&N#O{77ekmr>6vM(lz6Fkh2{&^JcN zxX&S5vtmrJsC}~Te3wP$PFxmaR`Tpuihul?f-jL-)MQPwtPf;-*s(mq7t8tbn9cT-VwMClk}4@Yf_&G~gVQ(7+=L3yv5EqehJzsyQS zZ%|M3z88uCbtDoHGe87z<2qDUS}>f6#o?y|W-}Y#br?a}LdI*e00h%#oDy7Z~bc2h5+e(f2XV^2pPQn-8|v zr@H{@2twdoZE*JlGmw=;mWWcqDfLv!^s%)!*CH*MnABAS zYA^eGb<@wNs(8C*T}iR&vBFc4eMUEPq%v*ti*OVv__w6#eN?!yMLfZN$F=w`ND#BX z*H70Vxz~(eXI<&m8-;wkHL|vz6XZ~;ThY;SgEsg(a_%mp4TAV(?Y!>JLMqJ4e8F&C zmzYWX+S70IyVLg|_*;x9oAA?bij-)wd9gL?a(X({)y!~(d~*PfE7j%9;7Ga;%Suk_ zblCgh5)Mbx;|xb0KHA)lHCz&tE2arh0`7Nl7v7LRON1?YRR(Jx9E+xh{XLUYpe4t5WMFBr8U0{X>H6HRK4v)s1yx=p^=2zcaZw|84e5ore?l%KWLZPZ4jtz!PkyIm3@dxpG=3~rOxe#((E%qh zF*Vgk?VhgP-5oj5Dc3stOf$@bxWkg@y5^J@9S7MWGt*gM0uY)$pLHabKtCB&S?FCv%5NmFiasoHO&xcu(Dsnzq zV<0pUTG6domQa}_y5r-Y@N7675ub8rrpa?=Qv!izIOdRh&X`2oW)}PWBPg(WlgaHwuYekoVT)s^K;?iwI@1PI%y=cx`Q()jY#;&}+nWM6@ z01m(#md73(U|Lf~9m@D-E$g8W>~BY<38!t5yI4vv-b`;X9$)r3ahR(vF_>?pN`btrw2u1TG zvn+=CH;uPOAq9AsRfhTE>r?V15%kjxZR37AzTna(2UVH{8FO4fW=JRB-dGHlazdx< zJy8pR(6yu)C7A<)Sk_e{mMhrs#^e!fgh<@sO)4?2W{g7knH{#BZM#bzP29tFoMu4T zhE1}0zhvzI1n$+5iem{u<|l?MPsUG8l$|*Kl!?FL#H=K^d~b+>qNTmeg6cI~Sz$s` z^pYPyKs1Hv+iANQ2s;g?AzCB^i4fP7$1=i8cJkk4=`x$44NVr75M57p%N!TT&T5vxsm3(B+E!)6Om#JPvM-S z0S*zM!UtZn1G;*fB@r^`D}zLMth8b0$oaQ#MALnUo51=bKvTLxi*Pa;LeRlHKV3si zC;OnlS5nnfhf#Dx<(9VcSNE9`I!WwblD<}Z{z2LdQ3fO_*dvN{{)#?!o!X4Nb zUdHQ4xB0M(V|!}cRFaTiTM)R$XtU4-A2d}NLSr}rjHGUOFX5TOpzvKrET$EXroc#o zI=?6P!a>prs~WNr=wE(*L|o)=YlXxP8i!tn0#>Z)T@z3Ml*ryWB%w`z{KV1&Y{cj3 zq$K0%;-*%5;mY=vl@(RdH9*gbKyZ!$?6 z`OQ~|%DXu)cmp_xram#~F412so+8IOFd&+n8hkW}+E6SZv5&D$$ooiS6G<~q#S*}X zCfdDxF9nbS90o&Eo=a+&nWD}HcX?YUp`_415vi4U;W3ze+@Y zO!7MXpg3ZvSk>7)oCe;>xWL-+u689tL9#*jLw-j1w2M})r!2=K6~-ij$s_PKl5=Aw zA6UJnwH7ndn7>cL!5MjRGcjCYYWQ0Tr~eGZB2c4S0)F~7Ap@h7K*ZjuF0tv*`;V$b ziCDR;1?)ulI(?1W9?Gftrgr77CqE_S%9G>T&`;};Zj2N(D~r;spOQ;=J(UA0j05`3 zWr`?0?2Q<(HWhZ9U3T}z7(VCZJg}&t>!o*&wfFrt!rapzEiQwk3MAKRPsid}BVr4m zf+tE=R<0^yPy(YiRqs$0XzISvHDCLF^VZ&IJOU0xYukfXnCrcDL$ug#=xZq96GHU% z!n=HSN6A{dx}0R`ybBR7=aIOo=!5X!SgWaXvknr{4E$sa*PO z#MBPe9?cTTMUG9wR{`BmMN*Ll5KJYT$CLY3;Gyv5Xe+!Ikhsfrr%Csrr5JvC@RZO< z_77^|edOt~A{J*e2jem)mzTsjm-dVWrRefr=$svnLXx|Fd;ZMq(JqzL|Bd5wTv=-w zc>BKiqY%tC30(onD|#Hn-Lkq*@yZapJXHd&<9O?25&L=mn>8N(uBh|qnEcB$rh7&D zC~nialppMG3gCL3TowD`s3gS_gOJL4{rSUIA(EuozFILAi@eg8Bt*9R^16@otzXvF z_##CYQlx52dsc08?@k(@4%IY7y(HTyOkFM@#q9r_x5B%6oT#IPaL=1^V%vC(#c`j# zmMfuc)A0;it~i;FRl3w~tY@IjPP^yQ%Q9yas#nazqMwar8E@y5mE7OS{VJ`{W-&V} zPF7BOB<||3*dNj^aV`puzvpfu`S~X0(cOj@BF}$~O)bMO3MY^iWJ=Bqlfr2UXU^5dDotTxkXi2_ygF{{5Rke?Ql*XVYwOqDYdPA{#q zZ(E>j`~55^>nlrs(5$!|^TSk8te&l@-GmOzdURz(==I<^Df@Ue;oD4WAVK5Iibm5r zk7&LY+SH`3*%y(oZGh`~NI-qo@qGEY?n=*I?k0ap7J}|T+RTIweP9(BlU3MpB^E;d z_hwCvrsL-H!nYgpFeCIG&#ZS!=mTN-msZ1rEeA!9U-djG9oTrGFp1vJX=Z2TCESye z5gYSE*_jxWeRnx|YdBkv+k^4mv-GSvCg@5I*AY5L5Ooc2@X-EA>pPjaKX_@-VDhYf zC!O7fwn?4V;K~cE{pvUtY7Q2G99f#A5%IO) zt(LnQ9%cfsWc86$9X3>q@Ha?=nRfUeO}E7`%IHX-TK|qzcyY~|x78jW6e$AwQnMw2 zyt<|aWxlX~fv&HppV4T5wjT8+f3e^1sjqk{kPT|WvzjVPX2=+R?Iv@%o+RG&fT>_e z{m1-|Pm?u=Z~L+!O8^#bLy5_V6K2N;7ZScRm~n8YsH?cg%)-Zj9*z|q`c*0*hk5lZ zOGPTg-WC_Z$z!+oYA0nl7uGT2(rt_Dgq-ZyYDYzFdYk!RxiPJnZq{B1ludBeu zThAb_J%z^rqQi4+OOqyAe5fP~5ny&|QggC{A>GA^xNFnY&0~or#CBvK6(m zpw`|T4-lx_|M{!m-z3V@vsT<6T_#68^_&jTEg^A3jS@k4ZS;*iNDPvLGIkdkEKUVS zLB1xAzY2pYYTCk^BKDuC@jyJ_v-A`0gIc|3J5(6`RL5zFiT*-#1-CTa%*)U)yHN;= ztEEdGV{Qw>;g^h4&KfL~<&iO9zMdyt9e*>AUVU+-E5c-SR**77QR(3&LE%wT!o?qu zJ3^XQ@XG8_To*-PaUSLoK$8JzMoS;uelMS?ko@q}v_iri_$^^fMGK1#Z{%3!blqDB z#-UtCKR$g?se|S?ga8iC@6f_8+iHN!e|RM^wE6WcY2Bnhpod&tQyN^DX9`9jGjVOV z3w-&t!7v-l+_S29Cb{3Eg8Wl%)db^a%cYtb(ymTPU8Y9(p(>mazRd?5^LuF#d@{R6 z+RSdN-Q$crb-S}E{x7xDyWO%~IAg}cNS!s~J24%M>TX}SM|qj<-68+D=n|~sq^Xz7 zJf~jCAIpdM*lc4XgMBf^#+Z17MiHwjwROXBx{4G8Wi5gh} zF%@wZlSZmc1PY^ZU*k;u~ z^()`mnYIRbs_H}aqzwL5sQlIGQq{(>&u3%m-0iO8jWnJXsdTs|-S@4t&tfYgQTO{S wV4fM>OxI52FrwS_Bmlh%{zvxT${qk*LDz_W7P*-L!T+MiDq6}_kIX~=2hjWw#sB~S From 3bd89564b5edfd41d42b0aad8b352756f2f5a5c0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 May 2019 15:16:02 -0400 Subject: [PATCH 043/119] Fix the underline on footer items (cherry picked from commit c60eee03aa1b8d3f147765825dc5feee431828c7) --- lms/static/sass/shared/_footer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index 284a3ccf71a3..3cc22c1b7620 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -138,7 +138,7 @@ display: inline-block; font-size: em(11); - &:not(:first-child) a::before { + &:not(:first-child)::before { margin-right: ($baseline/4); content: "-"; } From 0842a66da755879a76cb56a2c8c546713cc91d4e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 15 May 2019 07:02:20 -0400 Subject: [PATCH 044/119] Correct the trademark notices * edX should be lowercase everywhere * Studio is not a trademark * Our trademarks are all registered trademarks (cherry picked from commit 09690ce6676ba4d7f3c5e9d783ab4e0a1e6b810a) --- cms/templates/widgets/footer.html | 4 ++-- lms/djangoapps/branding/api.py | 8 ++++---- lms/djangoapps/branding/tests/test_api.py | 4 ++-- lms/djangoapps/branding/views.py | 2 +- .../templates/certificates/_accomplishment-footer.html | 8 ++++---- themes/edx.org/lms/templates/footer.html | 2 +- themes/red-theme/lms/templates/footer.html | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 53a96538f2f4..3a94777c1fcf 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -43,8 +43,8 @@ -
    \ No newline at end of file + diff --git a/themes/edx.org/lms/templates/footer.html b/themes/edx.org/lms/templates/footer.html index 4608d1336526..5b8e8effede8 100755 --- a/themes/edx.org/lms/templates/footer.html +++ b/themes/edx.org/lms/templates/footer.html @@ -134,7 +134,7 @@

    Connect


    ${_( - u"EdX, Open edX, and MicroMasters are registered trademarks of edX Inc. " + u"edX, Open edX, and MicroMasters are registered trademarks of edX Inc. " )} ${u" | {icp}".format(icp=getattr(settings,'ICP_LICENSE')) if getattr(settings,'ICP_LICENSE',False) else ""} diff --git a/themes/red-theme/lms/templates/footer.html b/themes/red-theme/lms/templates/footer.html index 320478a155e0..61f53d409164 100755 --- a/themes/red-theme/lms/templates/footer.html +++ b/themes/red-theme/lms/templates/footer.html @@ -67,8 +67,8 @@ ## Site operators: Please do not remove this paragraph! This attributes back to edX and makes your acknowledgement of edX's trademarks clear. diff --git a/themes/red-theme/lms/templates/footer.html b/themes/red-theme/lms/templates/footer.html index 61f53d409164..24ebd2483d91 100755 --- a/themes/red-theme/lms/templates/footer.html +++ b/themes/red-theme/lms/templates/footer.html @@ -1,4 +1,5 @@ ## mako +<%page expression_filter="h"/> <%namespace name='static' file='static_content.html'/> <%! from django.urls import reverse @@ -9,6 +10,7 @@ import pytz from openedx.core.djangoapps.lang_pref.api import footer_language_selector_is_enabled +from openedx.core.djangolib.markup import HTML, Text %> %endif + %if pending_upgrade_course_name: + + %endif From 590834ddee721414da60d66cb8d9813a42b0bdf3 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 2 Oct 2019 15:49:04 +0500 Subject: [PATCH 057/119] add caliper_app_code_for_ironwood --- lms/envs/production.py | 33 +++++++++++++++++++++++++++++++++ requirements/edx/base.txt | 1 + 2 files changed, 34 insertions(+) diff --git a/lms/envs/production.py b/lms/envs/production.py index b283b7f76841..f844249063bf 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1109,6 +1109,39 @@ ############## Settings for Writable Gradebook ######################### WRITABLE_GRADEBOOK_URL = ENV_TOKENS.get('WRITABLE_GRADEBOOK_URL', WRITABLE_GRADEBOOK_URL) +############### Settings for Caliper Tracking ##################### +# 'openedx_caliper_tracking' app allows us to transform Edx event-logs according to +# IMSGlobal Caliper Standards. +# https://www.imsglobal.org/sites/default/files/caliper/v1p1/caliper-spec-v1p1/caliper-spec-v1p1.html. + +# Following are the changes made in Django settings by openedx_caliper_tracking app. +# 1. EVENT_TRACKING_BACKENDS settings: +# It is responsible for logging events of 'eventtracking' app, our +# 'openedx_caliper_tracking' app appends its custom processor +# ('openedx_caliper_tracking.processor.CaliperProcessor') in the pipeline of +# 'processors' present in 'options' of 'tracking_logs' inside +# 'EVENT_TRACKING_BACKENDS'. + +# 2. TRACKING_BACKENDS settings: +# Logs generated by 'track' app use 'TRACKING_BACKENDS' settings. +# 'openedx_caliper_tracking' replaces the default logging backend with its +# own custom backend which eventually redirects the logs to +# openedx_caliper_tracking's processor. +# TRACKING_BACKENDS = { +# 'logger': { +# 'ENGINE': 'openedx_caliper_tracking.processor.CaliperProcessor', +# 'OPTIONS': { +# 'name': 'tracking' +# } + +if FEATURES.get('ENABLE_EVENT_CALIPERIZATION'): + INSTALLED_APPS.insert( + INSTALLED_APPS.index('eventtracking.django.apps.EventTrackingConfig'), + 'openedx_caliper_tracking' + ) +CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') +CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') + ############################### Plugin Settings ############################### # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 50d5434e3bec..a813e1661334 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,6 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger +openedx-caliper-tracking==0.11.1 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From 6375b5b96d63d13346daf76d31cbf36d26b67e5c Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 2 Oct 2019 18:16:00 +0500 Subject: [PATCH 058/119] add circle-ci test and zendesh replacement code --- .circleci/config.yml | 52 ++++++++++++++++ lms/envs/production.py | 3 + lms/envs/test.py | 2 + lms/templates/support/contact_us.html | 2 +- lms/urls.py | 5 ++ openedx/features/ucsd_features/__init__.py | 0 openedx/features/ucsd_features/tests/tests.py | 59 +++++++++++++++++++ openedx/features/ucsd_features/urls.py | 15 +++++ openedx/features/ucsd_features/views.py | 42 +++++++++++++ 9 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 .circleci/config.yml create mode 100644 openedx/features/ucsd_features/__init__.py create mode 100644 openedx/features/ucsd_features/tests/tests.py create mode 100644 openedx/features/ucsd_features/urls.py create mode 100644 openedx/features/ucsd_features/views.py diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000000..0dd9f758b521 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` + - image: circleci/python:2.7.15 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/edx-platform + + steps: + - checkout + + - run: + name: Prepare Environment + command: | + sudo apt-get -y install libxmlsec1-dev libxml2-dev libxslt-dev graphviz libgraphviz-dev libreadline7 libreadline-dev default-libmysqlclient-dev libsqlite3-dev build-essential libgeos-dev mongodb-server mongodb-clients + sudo pip install virtualenv + virtualenv -p python venv + source venv/bin/activate + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + nvm install v8.9.3 + npm install + pip install setuptools + pip install --exists-action w -r requirements/edx/testing.txt + pip install coveralls==1.0 + pip freeze + - run: + name: Run Tests + command: | + source venv/bin/activate + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + sudo mongod --config /etc/mongodb.conf & + paver test_system -t openedx/features/ucsd_features/tests/tests.py --fasttest + - store_artifacts: + path: test-reports + destination: test-reports + diff --git a/lms/envs/production.py b/lms/envs/production.py index f844249063bf..24d35978d07b 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1157,3 +1157,6 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +############### Settings for UCSD Support ##################### +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') diff --git a/lms/envs/test.py b/lms/envs/test.py index 17959b4e3ad5..047a555de370 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -619,3 +619,5 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +SUPPORT_DESK_EMAILS = ['servicedesk@ucsd.edu'] diff --git a/lms/templates/support/contact_us.html b/lms/templates/support/contact_us.html index dbdb93171fe5..8b3ab3a777c8 100644 --- a/lms/templates/support/contact_us.html +++ b/lms/templates/support/contact_us.html @@ -33,7 +33,7 @@ 'loginQuery': "${login_query() | n, js_escaped_string}", 'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}", 'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}", - 'submitFormUrl': "${reverse('zendesk_proxy_v1') | n, js_escaped_string}", + 'submitFormUrl': "${reverse('ucsd_support_email') | n, js_escaped_string}", 'customFields': ${custom_fields | n, dump_js_escaped_json}, 'tags': ${tags | n, dump_js_escaped_json}, 'supportEmail': "${support_email | n, js_escaped_string}", diff --git a/lms/urls.py b/lms/urls.py index ba2be98a95bf..f2ba9400a1c1 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -45,6 +45,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views from openedx.features.enterprise_support.api import enterprise_enabled +from openedx.features.ucsd_features import urls as ucsd_email_urls from ratelimitbackend import admin from static_template_view import views as static_template_view_views from staticbook import views as staticbook_views @@ -1047,4 +1048,8 @@ url(r'', include('csrf.urls')), ] +urlpatterns += [ + url(r'^ucsd_email/', include(ucsd_email_urls)) +] + urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS)) diff --git a/openedx/features/ucsd_features/__init__.py b/openedx/features/ucsd_features/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/tests/tests.py b/openedx/features/ucsd_features/tests/tests.py new file mode 100644 index 000000000000..e1c36a8f1f7b --- /dev/null +++ b/openedx/features/ucsd_features/tests/tests.py @@ -0,0 +1,59 @@ +import json +import mock + +from django.http import HttpResponse +from django.test import Client +from django.test import TestCase +from django.urls import reverse + +from rest_framework import status + + +class SupportEmailTestCase(TestCase): + """ + Tests email sending via contact/support form. + """ + + @classmethod + def setUpClass(cls): + super(SupportEmailTestCase, cls).setUpClass() + cls.client = Client() + cls.send_mail_url = reverse('ucsd_support_email') + cls.test_email = { + 'subject': 'Subject goes here', + 'comment': {'body': 'here goes the body/details'}, + 'tags': ['LMS'], + 'requester': { + 'email': 'edx@example.com', + 'name': 'edx' + }, + 'custom_fields': [{ + 'value': 'course-v1:edX+DemoX+Demo_Course' + }] + } + + def test_send_email_successful(self): + """ + Make sure email works successfully. + """ + response = self.client.post(self.send_mail_url, + data=json.dumps(self.test_email), + content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + @mock.patch( + 'django.test.Client.post', return_value=HttpResponse( + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ), autospec=True + ) + def test_send_email_fail(self, mock_func): + """ + Make sure email fails successfully. + """ + + response = self.client.post(self.send_mail_url, + data=json.dumps(self.test_email), + content_type='application/json') + + self.assertEqual(response.status_code, + status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/openedx/features/ucsd_features/urls.py b/openedx/features/ucsd_features/urls.py new file mode 100644 index 000000000000..9800736e3de7 --- /dev/null +++ b/openedx/features/ucsd_features/urls.py @@ -0,0 +1,15 @@ +""" +Map urls to the relevant view handlers +""" + +from django.conf.urls import url +from .views import email_support + + +urlpatterns = [ + url( + r'^ucsd_support_email$', + email_support, + name='ucsd_support_email' + ) +] diff --git a/openedx/features/ucsd_features/views.py b/openedx/features/ucsd_features/views.py new file mode 100644 index 000000000000..01f5c4961e01 --- /dev/null +++ b/openedx/features/ucsd_features/views.py @@ -0,0 +1,42 @@ +import json + +from django.core.mail import send_mail +from django.conf import settings +from django.views.decorators.http import require_http_methods +from django.http import HttpResponse +from rest_framework import status + +email_template = ''' + Course : {course} + Name: {name} + Email : {email} + + {body} + ''' + + +@require_http_methods(["POST"]) +def email_support(request): + """ + A View that will send user support (contact-form) emails to a specific + account. + """ + + body = json.loads(request.body) + subject = body['subject'] + + data = { + 'name': body['requester']['name'], + 'email': body['requester']['email'], + 'body': body['comment']['body'], + 'course': body['custom_fields'][0]['value'] + } + + content = email_template.format(**data) + response = send_mail(subject, content, settings.DEFAULT_FROM_EMAIL, + settings.SUPPORT_DESK_EMAILS, fail_silently=False) + + if response: + return HttpResponse(status=status.HTTP_201_CREATED) + else: + return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file From e7a920899beb93d70ac0d738c0ac402c75a06198 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Thu, 3 Oct 2019 16:35:04 +0500 Subject: [PATCH 059/119] add login logout events code and some other circleci changes and some bug fixes found during caliper app --- .../templates/static_content.html | 5 +--- lms/envs/production.py | 19 +++++++++++---- lms/envs/test.py | 2 +- lms/templates/pdf_viewer.html | 18 +++++++------- openedx/core/djangoapps/user_api/helpers.py | 13 ++++++++++ .../core/djangoapps/user_authn/views/login.py | 14 +++++++++++ scripts/circle-ci-tests.sh | 24 ++++++++++++------- 7 files changed, 67 insertions(+), 28 deletions(-) diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index da6868b8abbd..fa8a1595d6eb 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -31,10 +31,7 @@ %> <%def name='url(file, raw=False)'><% -try: - url = staticfiles_storage.url(file) -except: - url = file +url = staticfiles_storage.url(file) ## HTML-escaping must be handled by caller %>${url | n, decode.utf8}${"?raw" if raw else ""} diff --git a/lms/envs/production.py b/lms/envs/production.py index 24d35978d07b..3e48f9cba23e 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1109,30 +1109,42 @@ ############## Settings for Writable Gradebook ######################### WRITABLE_GRADEBOOK_URL = ENV_TOKENS.get('WRITABLE_GRADEBOOK_URL', WRITABLE_GRADEBOOK_URL) +############### Settings for UCSD Support ##################### +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') +INSTALLED_APPS.append('openedx.features.ucsd_features') + ############### Settings for Caliper Tracking ##################### # 'openedx_caliper_tracking' app allows us to transform Edx event-logs according to # IMSGlobal Caliper Standards. # https://www.imsglobal.org/sites/default/files/caliper/v1p1/caliper-spec-v1p1/caliper-spec-v1p1.html. - +# # Following are the changes made in Django settings by openedx_caliper_tracking app. + # 1. EVENT_TRACKING_BACKENDS settings: # It is responsible for logging events of 'eventtracking' app, our # 'openedx_caliper_tracking' app appends its custom processor # ('openedx_caliper_tracking.processor.CaliperProcessor') in the pipeline of # 'processors' present in 'options' of 'tracking_logs' inside # 'EVENT_TRACKING_BACKENDS'. - +# # 2. TRACKING_BACKENDS settings: # Logs generated by 'track' app use 'TRACKING_BACKENDS' settings. # 'openedx_caliper_tracking' replaces the default logging backend with its # own custom backend which eventually redirects the logs to # openedx_caliper_tracking's processor. +# # TRACKING_BACKENDS = { # 'logger': { # 'ENGINE': 'openedx_caliper_tracking.processor.CaliperProcessor', # 'OPTIONS': { # 'name': 'tracking' # } +# } +# } +# +# 3. This app must be placed in INSTALLED_APPS settings before +# 'eventtracking' app. The code bellow will handle this. +# if FEATURES.get('ENABLE_EVENT_CALIPERIZATION'): INSTALLED_APPS.insert( @@ -1157,6 +1169,3 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) - -############### Settings for UCSD Support ##################### -SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') diff --git a/lms/envs/test.py b/lms/envs/test.py index 047a555de370..d97b18a902d0 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -558,7 +558,7 @@ COMPREHENSIVE_THEME_DIRS = [REPO_ROOT / "themes", REPO_ROOT / "common/test"] COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ] -LMS_ROOT_URL = "http://localhost:8000" +LMS_ROOT_URL = "http://localhost:18000" # TODO (felipemontoya): This key is only needed during lettuce tests. # To be removed during https://openedx.atlassian.net/browse/DEPR-19 diff --git a/lms/templates/pdf_viewer.html b/lms/templates/pdf_viewer.html index 5702e030f41a..f977762cd312 100644 --- a/lms/templates/pdf_viewer.html +++ b/lms/templates/pdf_viewer.html @@ -32,23 +32,23 @@ ${current_chapter['title'] if current_chapter else ''} - + - + - + - + <%static:js group='main_vendor'/> <%static:js group='application'/> @@ -417,7 +417,7 @@ - - + + diff --git a/openedx/core/djangoapps/user_api/helpers.py b/openedx/core/djangoapps/user_api/helpers.py index 60784e66dbca..d6b2138feed2 100644 --- a/openedx/core/djangoapps/user_api/helpers.py +++ b/openedx/core/djangoapps/user_api/helpers.py @@ -6,6 +6,7 @@ import logging import traceback from collections import defaultdict +from eventtracking import tracker from functools import wraps from django import forms @@ -515,6 +516,18 @@ def _inner(request): # pylint: disable=missing-docstring else: response.content = msg + if response.status_code == 200: + event_name = 'edx.user.login' + event_data = { + 'email': request.POST.get('email'), + 'remember': request.POST.get('remember'), + 'username': request.user.username, + 'user_id': request.user.id + } + event_data.update(response_dict) + tracker.emit(event_name, event_data) + + # Return the response, preserving the original headers. # This is really important, since the student views set cookies # that are used elsewhere in the system (such as the marketing site). diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index 5bf0ab54e646..42e0cc421d1e 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -5,8 +5,10 @@ """ import logging +import json from django.conf import settings +from eventtracking import tracker from django.contrib.auth import authenticate, login as django_login from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User @@ -372,6 +374,18 @@ def login_user(request): # Ensure that the external marketing site can # detect that the user is logged in. + if response.status_code == 200: + event_name = 'edx.user.login' + event_data = { + 'email': request.POST.get('email'), + 'remember': request.POST.get('remember'), + 'username': request.user.username, + 'user_id': request.user.id + } + response_dict = json.loads(response.content) + event_data.update(response_dict) + tracker.emit(event_name, event_data) + return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: log.exception(error.get_response()) diff --git a/scripts/circle-ci-tests.sh b/scripts/circle-ci-tests.sh index 4888706534e0..83bcfa68c5e2 100755 --- a/scripts/circle-ci-tests.sh +++ b/scripts/circle-ci-tests.sh @@ -77,17 +77,23 @@ else exit $EXIT ;; - 1) # run all of the lms unit tests - paver test_system -s lms --cov-args="-p" - ;; + 1) # run caliper configuration tests + paver test_system -t openedx/features/caliper_tracking/tests/tests.py::CaliperTransformationTestCase::test_caliper_transformers --fasttest --cov-args="-p" - 2) # run all of the cms unit tests - paver test_system -s cms --cov-args="-p" - ;; + # The following jobs are commented out because at the moment we do not wish to run all the tests. + # We shall run the full test suite once Caliper transformation of logs is complete - 3) # run the commonlib unit tests - paver test_lib --cov-args="-p" - ;; + # 1) # run all of the lms unit tests + # paver test_system -s lms --cov-args="-p" + # ;; + + # 2) # run all of the cms unit tests + # paver test_system -s cms --cov-args="-p" + # ;; + + # 3) # run the commonlib unit tests + # paver test_lib --cov-args="-p" + # ;; *) echo "No tests were executed in this container." From b3373f060f42b07942a0d8d2c49a83ec6c77e236 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Fri, 25 Oct 2019 16:40:50 +0500 Subject: [PATCH 060/119] add kafka and caliper settings for cms production.py --- cms/envs/production.py | 13 +++++++++++++ requirements/edx/base.txt | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cms/envs/production.py b/cms/envs/production.py index 3f33bb57b7fb..41f798c644b6 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -605,6 +605,19 @@ ############## Settings for Course Enrollment Modes ###################### COURSE_ENROLLMENT_MODES = ENV_TOKENS.get('COURSE_ENROLLMENT_MODES', COURSE_ENROLLMENT_MODES) +############### Settings for UCSD Support ##################### +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') + +############### Settings for Edx Caliper Tracking ##################### +if FEATURES.get('ENABLE_EVENT_CALIPERIZATION'): + INSTALLED_APPS.insert( + INSTALLED_APPS.index('eventtracking.django.apps.EventTrackingConfig'), + 'openedx_caliper_tracking' + ) +CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') +CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') +CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') + ####################### Plugin Settings ########################## # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index a813e1661334..3d48dcb164b3 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,7 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger -openedx-caliper-tracking==0.11.1 +openedx-caliper-tracking==0.11.4 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From dc2230ac205dd000feb8c833d4f9e5d023662475 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 2 Oct 2019 15:49:04 +0500 Subject: [PATCH 061/119] add caliper_app_code_for_ironwood --- lms/envs/production.py | 33 +++++++++++++++++++++++++++++++++ requirements/edx/base.txt | 1 + 2 files changed, 34 insertions(+) diff --git a/lms/envs/production.py b/lms/envs/production.py index b283b7f76841..f844249063bf 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1109,6 +1109,39 @@ ############## Settings for Writable Gradebook ######################### WRITABLE_GRADEBOOK_URL = ENV_TOKENS.get('WRITABLE_GRADEBOOK_URL', WRITABLE_GRADEBOOK_URL) +############### Settings for Caliper Tracking ##################### +# 'openedx_caliper_tracking' app allows us to transform Edx event-logs according to +# IMSGlobal Caliper Standards. +# https://www.imsglobal.org/sites/default/files/caliper/v1p1/caliper-spec-v1p1/caliper-spec-v1p1.html. + +# Following are the changes made in Django settings by openedx_caliper_tracking app. +# 1. EVENT_TRACKING_BACKENDS settings: +# It is responsible for logging events of 'eventtracking' app, our +# 'openedx_caliper_tracking' app appends its custom processor +# ('openedx_caliper_tracking.processor.CaliperProcessor') in the pipeline of +# 'processors' present in 'options' of 'tracking_logs' inside +# 'EVENT_TRACKING_BACKENDS'. + +# 2. TRACKING_BACKENDS settings: +# Logs generated by 'track' app use 'TRACKING_BACKENDS' settings. +# 'openedx_caliper_tracking' replaces the default logging backend with its +# own custom backend which eventually redirects the logs to +# openedx_caliper_tracking's processor. +# TRACKING_BACKENDS = { +# 'logger': { +# 'ENGINE': 'openedx_caliper_tracking.processor.CaliperProcessor', +# 'OPTIONS': { +# 'name': 'tracking' +# } + +if FEATURES.get('ENABLE_EVENT_CALIPERIZATION'): + INSTALLED_APPS.insert( + INSTALLED_APPS.index('eventtracking.django.apps.EventTrackingConfig'), + 'openedx_caliper_tracking' + ) +CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') +CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') + ############################### Plugin Settings ############################### # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 50d5434e3bec..a813e1661334 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,6 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger +openedx-caliper-tracking==0.11.1 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From 772029ea9e1a7d93585fe7b748cea9ad959be6ae Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 2 Oct 2019 18:16:00 +0500 Subject: [PATCH 062/119] add circle-ci test and zendesh replacement code --- .circleci/config.yml | 52 ++++++++++++++++ lms/envs/production.py | 3 + lms/envs/test.py | 2 + lms/templates/support/contact_us.html | 2 +- lms/urls.py | 5 ++ openedx/features/ucsd_features/__init__.py | 0 openedx/features/ucsd_features/tests/tests.py | 59 +++++++++++++++++++ openedx/features/ucsd_features/urls.py | 15 +++++ openedx/features/ucsd_features/views.py | 42 +++++++++++++ 9 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 .circleci/config.yml create mode 100644 openedx/features/ucsd_features/__init__.py create mode 100644 openedx/features/ucsd_features/tests/tests.py create mode 100644 openedx/features/ucsd_features/urls.py create mode 100644 openedx/features/ucsd_features/views.py diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000000..0dd9f758b521 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` + - image: circleci/python:2.7.15 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/edx-platform + + steps: + - checkout + + - run: + name: Prepare Environment + command: | + sudo apt-get -y install libxmlsec1-dev libxml2-dev libxslt-dev graphviz libgraphviz-dev libreadline7 libreadline-dev default-libmysqlclient-dev libsqlite3-dev build-essential libgeos-dev mongodb-server mongodb-clients + sudo pip install virtualenv + virtualenv -p python venv + source venv/bin/activate + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + nvm install v8.9.3 + npm install + pip install setuptools + pip install --exists-action w -r requirements/edx/testing.txt + pip install coveralls==1.0 + pip freeze + - run: + name: Run Tests + command: | + source venv/bin/activate + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + sudo mongod --config /etc/mongodb.conf & + paver test_system -t openedx/features/ucsd_features/tests/tests.py --fasttest + - store_artifacts: + path: test-reports + destination: test-reports + diff --git a/lms/envs/production.py b/lms/envs/production.py index f844249063bf..24d35978d07b 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1157,3 +1157,6 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +############### Settings for UCSD Support ##################### +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') diff --git a/lms/envs/test.py b/lms/envs/test.py index 17959b4e3ad5..047a555de370 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -619,3 +619,5 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +SUPPORT_DESK_EMAILS = ['servicedesk@ucsd.edu'] diff --git a/lms/templates/support/contact_us.html b/lms/templates/support/contact_us.html index dbdb93171fe5..8b3ab3a777c8 100644 --- a/lms/templates/support/contact_us.html +++ b/lms/templates/support/contact_us.html @@ -33,7 +33,7 @@ 'loginQuery': "${login_query() | n, js_escaped_string}", 'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}", 'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}", - 'submitFormUrl': "${reverse('zendesk_proxy_v1') | n, js_escaped_string}", + 'submitFormUrl': "${reverse('ucsd_support_email') | n, js_escaped_string}", 'customFields': ${custom_fields | n, dump_js_escaped_json}, 'tags': ${tags | n, dump_js_escaped_json}, 'supportEmail': "${support_email | n, js_escaped_string}", diff --git a/lms/urls.py b/lms/urls.py index ba2be98a95bf..f2ba9400a1c1 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -45,6 +45,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views from openedx.features.enterprise_support.api import enterprise_enabled +from openedx.features.ucsd_features import urls as ucsd_email_urls from ratelimitbackend import admin from static_template_view import views as static_template_view_views from staticbook import views as staticbook_views @@ -1047,4 +1048,8 @@ url(r'', include('csrf.urls')), ] +urlpatterns += [ + url(r'^ucsd_email/', include(ucsd_email_urls)) +] + urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS)) diff --git a/openedx/features/ucsd_features/__init__.py b/openedx/features/ucsd_features/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/tests/tests.py b/openedx/features/ucsd_features/tests/tests.py new file mode 100644 index 000000000000..e1c36a8f1f7b --- /dev/null +++ b/openedx/features/ucsd_features/tests/tests.py @@ -0,0 +1,59 @@ +import json +import mock + +from django.http import HttpResponse +from django.test import Client +from django.test import TestCase +from django.urls import reverse + +from rest_framework import status + + +class SupportEmailTestCase(TestCase): + """ + Tests email sending via contact/support form. + """ + + @classmethod + def setUpClass(cls): + super(SupportEmailTestCase, cls).setUpClass() + cls.client = Client() + cls.send_mail_url = reverse('ucsd_support_email') + cls.test_email = { + 'subject': 'Subject goes here', + 'comment': {'body': 'here goes the body/details'}, + 'tags': ['LMS'], + 'requester': { + 'email': 'edx@example.com', + 'name': 'edx' + }, + 'custom_fields': [{ + 'value': 'course-v1:edX+DemoX+Demo_Course' + }] + } + + def test_send_email_successful(self): + """ + Make sure email works successfully. + """ + response = self.client.post(self.send_mail_url, + data=json.dumps(self.test_email), + content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + @mock.patch( + 'django.test.Client.post', return_value=HttpResponse( + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ), autospec=True + ) + def test_send_email_fail(self, mock_func): + """ + Make sure email fails successfully. + """ + + response = self.client.post(self.send_mail_url, + data=json.dumps(self.test_email), + content_type='application/json') + + self.assertEqual(response.status_code, + status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/openedx/features/ucsd_features/urls.py b/openedx/features/ucsd_features/urls.py new file mode 100644 index 000000000000..9800736e3de7 --- /dev/null +++ b/openedx/features/ucsd_features/urls.py @@ -0,0 +1,15 @@ +""" +Map urls to the relevant view handlers +""" + +from django.conf.urls import url +from .views import email_support + + +urlpatterns = [ + url( + r'^ucsd_support_email$', + email_support, + name='ucsd_support_email' + ) +] diff --git a/openedx/features/ucsd_features/views.py b/openedx/features/ucsd_features/views.py new file mode 100644 index 000000000000..01f5c4961e01 --- /dev/null +++ b/openedx/features/ucsd_features/views.py @@ -0,0 +1,42 @@ +import json + +from django.core.mail import send_mail +from django.conf import settings +from django.views.decorators.http import require_http_methods +from django.http import HttpResponse +from rest_framework import status + +email_template = ''' + Course : {course} + Name: {name} + Email : {email} + + {body} + ''' + + +@require_http_methods(["POST"]) +def email_support(request): + """ + A View that will send user support (contact-form) emails to a specific + account. + """ + + body = json.loads(request.body) + subject = body['subject'] + + data = { + 'name': body['requester']['name'], + 'email': body['requester']['email'], + 'body': body['comment']['body'], + 'course': body['custom_fields'][0]['value'] + } + + content = email_template.format(**data) + response = send_mail(subject, content, settings.DEFAULT_FROM_EMAIL, + settings.SUPPORT_DESK_EMAILS, fail_silently=False) + + if response: + return HttpResponse(status=status.HTTP_201_CREATED) + else: + return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file From b6f73f2c6d545444577d83b40adadf3da9702dfa Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Thu, 3 Oct 2019 16:35:04 +0500 Subject: [PATCH 063/119] add login logout events code and some other circleci changes and some bug fixes found during caliper app --- .../templates/static_content.html | 5 +--- lms/envs/production.py | 19 +++++++++++---- lms/envs/test.py | 2 +- lms/templates/pdf_viewer.html | 18 +++++++------- openedx/core/djangoapps/user_api/helpers.py | 13 ++++++++++ .../core/djangoapps/user_authn/views/login.py | 14 +++++++++++ scripts/circle-ci-tests.sh | 24 ++++++++++++------- 7 files changed, 67 insertions(+), 28 deletions(-) diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index da6868b8abbd..fa8a1595d6eb 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -31,10 +31,7 @@ %> <%def name='url(file, raw=False)'><% -try: - url = staticfiles_storage.url(file) -except: - url = file +url = staticfiles_storage.url(file) ## HTML-escaping must be handled by caller %>${url | n, decode.utf8}${"?raw" if raw else ""} diff --git a/lms/envs/production.py b/lms/envs/production.py index 24d35978d07b..3e48f9cba23e 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1109,30 +1109,42 @@ ############## Settings for Writable Gradebook ######################### WRITABLE_GRADEBOOK_URL = ENV_TOKENS.get('WRITABLE_GRADEBOOK_URL', WRITABLE_GRADEBOOK_URL) +############### Settings for UCSD Support ##################### +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') +INSTALLED_APPS.append('openedx.features.ucsd_features') + ############### Settings for Caliper Tracking ##################### # 'openedx_caliper_tracking' app allows us to transform Edx event-logs according to # IMSGlobal Caliper Standards. # https://www.imsglobal.org/sites/default/files/caliper/v1p1/caliper-spec-v1p1/caliper-spec-v1p1.html. - +# # Following are the changes made in Django settings by openedx_caliper_tracking app. + # 1. EVENT_TRACKING_BACKENDS settings: # It is responsible for logging events of 'eventtracking' app, our # 'openedx_caliper_tracking' app appends its custom processor # ('openedx_caliper_tracking.processor.CaliperProcessor') in the pipeline of # 'processors' present in 'options' of 'tracking_logs' inside # 'EVENT_TRACKING_BACKENDS'. - +# # 2. TRACKING_BACKENDS settings: # Logs generated by 'track' app use 'TRACKING_BACKENDS' settings. # 'openedx_caliper_tracking' replaces the default logging backend with its # own custom backend which eventually redirects the logs to # openedx_caliper_tracking's processor. +# # TRACKING_BACKENDS = { # 'logger': { # 'ENGINE': 'openedx_caliper_tracking.processor.CaliperProcessor', # 'OPTIONS': { # 'name': 'tracking' # } +# } +# } +# +# 3. This app must be placed in INSTALLED_APPS settings before +# 'eventtracking' app. The code bellow will handle this. +# if FEATURES.get('ENABLE_EVENT_CALIPERIZATION'): INSTALLED_APPS.insert( @@ -1157,6 +1169,3 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) - -############### Settings for UCSD Support ##################### -SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') diff --git a/lms/envs/test.py b/lms/envs/test.py index 047a555de370..d97b18a902d0 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -558,7 +558,7 @@ COMPREHENSIVE_THEME_DIRS = [REPO_ROOT / "themes", REPO_ROOT / "common/test"] COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ] -LMS_ROOT_URL = "http://localhost:8000" +LMS_ROOT_URL = "http://localhost:18000" # TODO (felipemontoya): This key is only needed during lettuce tests. # To be removed during https://openedx.atlassian.net/browse/DEPR-19 diff --git a/lms/templates/pdf_viewer.html b/lms/templates/pdf_viewer.html index 5702e030f41a..f977762cd312 100644 --- a/lms/templates/pdf_viewer.html +++ b/lms/templates/pdf_viewer.html @@ -32,23 +32,23 @@ ${current_chapter['title'] if current_chapter else ''} - + - + - + - + <%static:js group='main_vendor'/> <%static:js group='application'/> @@ -417,7 +417,7 @@ - - + + diff --git a/openedx/core/djangoapps/user_api/helpers.py b/openedx/core/djangoapps/user_api/helpers.py index 60784e66dbca..d6b2138feed2 100644 --- a/openedx/core/djangoapps/user_api/helpers.py +++ b/openedx/core/djangoapps/user_api/helpers.py @@ -6,6 +6,7 @@ import logging import traceback from collections import defaultdict +from eventtracking import tracker from functools import wraps from django import forms @@ -515,6 +516,18 @@ def _inner(request): # pylint: disable=missing-docstring else: response.content = msg + if response.status_code == 200: + event_name = 'edx.user.login' + event_data = { + 'email': request.POST.get('email'), + 'remember': request.POST.get('remember'), + 'username': request.user.username, + 'user_id': request.user.id + } + event_data.update(response_dict) + tracker.emit(event_name, event_data) + + # Return the response, preserving the original headers. # This is really important, since the student views set cookies # that are used elsewhere in the system (such as the marketing site). diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index 5bf0ab54e646..42e0cc421d1e 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -5,8 +5,10 @@ """ import logging +import json from django.conf import settings +from eventtracking import tracker from django.contrib.auth import authenticate, login as django_login from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User @@ -372,6 +374,18 @@ def login_user(request): # Ensure that the external marketing site can # detect that the user is logged in. + if response.status_code == 200: + event_name = 'edx.user.login' + event_data = { + 'email': request.POST.get('email'), + 'remember': request.POST.get('remember'), + 'username': request.user.username, + 'user_id': request.user.id + } + response_dict = json.loads(response.content) + event_data.update(response_dict) + tracker.emit(event_name, event_data) + return set_logged_in_cookies(request, response, possibly_authenticated_user) except AuthFailedError as error: log.exception(error.get_response()) diff --git a/scripts/circle-ci-tests.sh b/scripts/circle-ci-tests.sh index 4888706534e0..83bcfa68c5e2 100755 --- a/scripts/circle-ci-tests.sh +++ b/scripts/circle-ci-tests.sh @@ -77,17 +77,23 @@ else exit $EXIT ;; - 1) # run all of the lms unit tests - paver test_system -s lms --cov-args="-p" - ;; + 1) # run caliper configuration tests + paver test_system -t openedx/features/caliper_tracking/tests/tests.py::CaliperTransformationTestCase::test_caliper_transformers --fasttest --cov-args="-p" - 2) # run all of the cms unit tests - paver test_system -s cms --cov-args="-p" - ;; + # The following jobs are commented out because at the moment we do not wish to run all the tests. + # We shall run the full test suite once Caliper transformation of logs is complete - 3) # run the commonlib unit tests - paver test_lib --cov-args="-p" - ;; + # 1) # run all of the lms unit tests + # paver test_system -s lms --cov-args="-p" + # ;; + + # 2) # run all of the cms unit tests + # paver test_system -s cms --cov-args="-p" + # ;; + + # 3) # run the commonlib unit tests + # paver test_lib --cov-args="-p" + # ;; *) echo "No tests were executed in this container." From 24cae03d8bc8faed234c8444ca55e2884a7c5c32 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 16 Oct 2019 11:42:12 +0500 Subject: [PATCH 064/119] initial changes --- common/djangoapps/util/views.py | 39 ++++++++--- lms/djangoapps/commerce/utils.py | 17 ++++- lms/envs/production.py | 2 +- .../core/djangoapps/zendesk_proxy/utils.py | 15 ++++- openedx/features/ucsd_features/__init__.py | 1 + openedx/features/ucsd_features/apps.py | 5 ++ .../ucsd_features/templates/base_email.html | 6 ++ .../templates/logs_not_sent_email.html | 10 +++ .../templates/support_email.html | 21 ++++++ openedx/features/ucsd_features/utils.py | 66 +++++++++++++++++++ openedx/features/ucsd_features/views.py | 38 ++++------- 11 files changed, 179 insertions(+), 41 deletions(-) create mode 100644 openedx/features/ucsd_features/apps.py create mode 100644 openedx/features/ucsd_features/templates/base_email.html create mode 100644 openedx/features/ucsd_features/templates/logs_not_sent_email.html create mode 100644 openedx/features/ucsd_features/templates/support_email.html create mode 100644 openedx/features/ucsd_features/utils.py diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index f1993a790ca0..8a888c76e49a 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -22,6 +22,7 @@ from edxmako.shortcuts import render_to_response, render_to_string from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.features.enterprise_support import api as enterprise_api +from openedx.features.ucsd_features.utils import send_notification_email_to_support from student.models import CourseEnrollment from student.roles import GlobalStaff @@ -288,28 +289,48 @@ def _record_feedback_in_zendesk( custom_fields=None ): """ - Create a new user-requested Zendesk ticket. + Create a new user-requested Zendesk ticket or send an email to Support Team. - Once created, the ticket will be updated with a private comment containing - additional information from the browser and server, such as HTTP headers - and user state. Returns a boolean value indicating whether ticket creation - was successful, regardless of whether the private comment update succeeded. + Use ENABLE_EMAIL_INSTEAD_ZENDESK flag to switch between zendesk ticket or support email + + In case of Zendesk ticket, once created, the ticket will be updated with a private + comment containing additional information from the browser and server, such as HTTP headers + and user state. If `group_name` is provided, attaches the ticket to the matching Zendesk group. - If `require_update` is provided, returns False when the update does not - succeed. This allows using the private comment to add necessary information - which the user will not see in followup emails from support. + If `require_update` is provided, this allows using the private comment to add + necessary information which the user will not see in followup emails from support. If `custom_fields` is provided, submits data to those fields in Zendesk. + + Returns: a boolean value indicating if either email was sent successfully or ticket + was created (regardless of whether the private comment update succeeded or not). + """ - zendesk_api = _ZendeskApi() additional_info_string = ( u"Additional information:\n\n" + u"\n".join(u"%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None) ) + if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): + is_email_sent = send_notification_email_to_support( + subject=subject, + body=details, + name=realname, + email=email, + custom_fields=custom_fields, + additional_info=additional_info + ) + return is_email_sent + + zendesk_api = _ZendeskApi() + + if not (settings.ZENDESK_URL and settings.ZENDESK_USER and settings.ZENDESK_API_KEY): + log.error('Zendesk is not configured. Cannot create a ticket.') + return False + # Tag all issues with LMS to distinguish channel in Zendesk; requested by student support team zendesk_tags = list(tags.values()) + ["LMS"] diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 03251041105a..ec85dd8e4457 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -16,6 +16,7 @@ from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, is_commerce_service_configured from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming import helpers as theming_helpers +from openedx.features.ucsd_features.utils import send_notification_email_to_support from student.models import CourseEnrollment from .models import CommerceConfiguration @@ -358,11 +359,21 @@ def _generate_refund_notification_body(student, refund_ids): def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=None): """ - Create a Zendesk ticket via API. + Send email to support team or create a Zendesk ticket via API. + Use ENABLE_EMAIL_INSTEAD_ZENDESK flag to switch between zendesk ticket or support email - Returns: - bool: False if we are unable to create the ticket for any reason + Returns: a boolean value indicating if either email has been sent successfully or + ticket has been created. """ + if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): + is_email_sent = send_notification_email_to_support( + subject=subject, + body=body, + name=requester_name, + email=requester_email, + ) + return is_email_sent + if not (settings.ZENDESK_URL and settings.ZENDESK_USER and settings.ZENDESK_API_KEY): log.error('Zendesk is not configured. Cannot create a ticket.') return False diff --git a/lms/envs/production.py b/lms/envs/production.py index 3e48f9cba23e..369f301ea9da 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1153,7 +1153,7 @@ ) CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') - +CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') ############################### Plugin Settings ############################### # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/openedx/core/djangoapps/zendesk_proxy/utils.py b/openedx/core/djangoapps/zendesk_proxy/utils.py index 061a26963342..8c4a18389c2e 100644 --- a/openedx/core/djangoapps/zendesk_proxy/utils.py +++ b/openedx/core/djangoapps/zendesk_proxy/utils.py @@ -8,13 +8,14 @@ from django.conf import settings import requests from rest_framework import status +from openedx.features.ucsd_features.utils import send_notification_email_to_support log = logging.getLogger(__name__) - def create_zendesk_ticket(requester_name, requester_email, subject, body, custom_fields=None, uploads=None, tags=None): """ - Create a Zendesk ticket via API. + Create a Zendesk ticket via API or send an email to support team. + Use ENABLE_EMAIL_INSTEAD_ZENDESK flag to switch between zendesk ticket or support email Note that we do this differently in other locations (lms/djangoapps/commerce/signals.py and common/djangoapps/util/views.py). Both of those callers use basic auth, and should be switched over to this oauth @@ -24,6 +25,16 @@ def _std_error_message(details, payload): """Internal helper to standardize error message. This allows for simpler splunk alerts.""" return 'zendesk_proxy action required\n{}\nNo ticket created for payload {}'.format(details, payload) + if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): + is_email_sent = send_notification_email_to_support( + subject=subject, + body=body, + name=requester_name, + email=requester_email, + custom_fields=custom_fields + ) + return status.HTTP_201_CREATED if is_email_sent else status.HTTP_503_SERVICE_UNAVAILABLE + if tags: # Remove duplicates from tags list tags = list(set(tags)) diff --git a/openedx/features/ucsd_features/__init__.py b/openedx/features/ucsd_features/__init__.py index e69de29bb2d1..d7b242fefaaa 100644 --- a/openedx/features/ucsd_features/__init__.py +++ b/openedx/features/ucsd_features/__init__.py @@ -0,0 +1 @@ +default_app_config = 'openedx.features.ucsd_features.apps.UcsdFeatures' diff --git a/openedx/features/ucsd_features/apps.py b/openedx/features/ucsd_features/apps.py new file mode 100644 index 000000000000..604f2d78a9fe --- /dev/null +++ b/openedx/features/ucsd_features/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UcsdFeatures(AppConfig): + name = 'openedx.features.ucsd_features' diff --git a/openedx/features/ucsd_features/templates/base_email.html b/openedx/features/ucsd_features/templates/base_email.html new file mode 100644 index 000000000000..83c6201a46c7 --- /dev/null +++ b/openedx/features/ucsd_features/templates/base_email.html @@ -0,0 +1,6 @@ + + + {% block body %} + {% endblock body %} + + diff --git a/openedx/features/ucsd_features/templates/logs_not_sent_email.html b/openedx/features/ucsd_features/templates/logs_not_sent_email.html new file mode 100644 index 000000000000..541ecaeaf6e0 --- /dev/null +++ b/openedx/features/ucsd_features/templates/logs_not_sent_email.html @@ -0,0 +1,10 @@ +{% extends 'base_email.html' %} +{% block body %} +

    {{ body }}

    +
    + + {% for info_dict in additional_info.items %} +

    {{info_dict.0}}: {{info_dict.1}}

    +
    + {% endfor %} +{% endblock body %} diff --git a/openedx/features/ucsd_features/templates/support_email.html b/openedx/features/ucsd_features/templates/support_email.html new file mode 100644 index 000000000000..b37c33fb8cd5 --- /dev/null +++ b/openedx/features/ucsd_features/templates/support_email.html @@ -0,0 +1,21 @@ +{% extends 'base_email.html' %} +{% block body %} +

    Requester Name: {{name}}

    +

    Requester Email: {{email}}

    +

    {{ body }}

    +
    + {% if course %} + Course Name: {{course}} +
    + {% endif %} + {% if custom_fields or additional_info %} + {% for field_dict in custom_fields %} + {% for key, value in field_dict.items %} +

    {{key}}: {{value}}

    + {% endfor %} + {% endfor %} + {% for info_dict in additional_info.items %} +

    {{info_dict.0}}: {{info_dict.1}}

    + {% endfor %} + {% endif %} +{% endblock body %} diff --git a/openedx/features/ucsd_features/utils.py b/openedx/features/ucsd_features/utils.py new file mode 100644 index 000000000000..42e2e973f94b --- /dev/null +++ b/openedx/features/ucsd_features/utils.py @@ -0,0 +1,66 @@ +import json +import logging +from smtplib import SMTPException + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives, send_mail +from django.template.loader import get_template + +log = logging.getLogger(__name__) +TEMPLATE_PATH = '{key}_email.html' + + +def send_notification(key, data, subject, from_email, dest_emails): + """ + Send an email. + + params: + key - Email template will be selected on the basis of key + data - Dict containing context/data for the template + subject - Email subject + from_email - Email address to send email + dest_emails - List of destination emails + + return: a boolean variable indicating email response. + """ + content = json.dumps(data) + email_template_path = TEMPLATE_PATH.format(key=key) + html_content = get_template(email_template_path).render(data) + msg = EmailMultiAlternatives(subject, content, from_email, dest_emails) + msg.attach_alternative(html_content, "text/html") + try: + response = msg.send() + log.info( + 'Email has been sent from "%s" to "%s" for content %s.', + from_email, + dest_emails, + content + ) + return response + except SMTPException: + log.error( + 'Unable to send an email from "%s" to %s for content "%s".', + from_email, + dest_emails, + content + ) + return False + + +def send_notification_email_to_support(subject, body, name, email, custom_fields=None, additional_info=None, course=None): + """ + Sending a notification-email to the Support Team. + """ + key = "support" + dest_emails = settings.SUPPORT_DESK_EMAILS + from_address = settings.DEFAULT_FROM_EMAIL + data = { + 'name': name, + 'email': email, + 'body': body, + 'custom_fields': custom_fields, + 'additional_info': additional_info, + } + email_response = send_notification( + key, data, subject, from_address, dest_emails) + return email_response diff --git a/openedx/features/ucsd_features/views.py b/openedx/features/ucsd_features/views.py index 01f5c4961e01..d8a0b3ba1d4e 100644 --- a/openedx/features/ucsd_features/views.py +++ b/openedx/features/ucsd_features/views.py @@ -1,18 +1,10 @@ import json - -from django.core.mail import send_mail from django.conf import settings -from django.views.decorators.http import require_http_methods -from django.http import HttpResponse from rest_framework import status - -email_template = ''' - Course : {course} - Name: {name} - Email : {email} - - {body} - ''' +from django.http import HttpResponse +from django.core.mail import send_mail +from django.views.decorators.http import require_http_methods +from openedx.features.ucsd_features.utils import send_notification_email_to_support @require_http_methods(["POST"]) @@ -21,22 +13,16 @@ def email_support(request): A View that will send user support (contact-form) emails to a specific account. """ - body = json.loads(request.body) - subject = body['subject'] - - data = { - 'name': body['requester']['name'], - 'email': body['requester']['email'], - 'body': body['comment']['body'], - 'course': body['custom_fields'][0]['value'] - } - - content = email_template.format(**data) - response = send_mail(subject, content, settings.DEFAULT_FROM_EMAIL, - settings.SUPPORT_DESK_EMAILS, fail_silently=False) + response = send_notification_email_to_support( + subject=body['subject'], + body=body['comment']['body'], + name=body['requester']['name'], + email=body['requester']['email'], + course=body['custom_fields'][0]['value'] + ) if response: return HttpResponse(status=status.HTTP_201_CREATED) else: - return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file + return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) From c3be5a30ee5a4ada1b54cbff331242fc93bd50f9 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Wed, 30 Oct 2019 17:17:35 +0500 Subject: [PATCH 065/119] Refactor if conditions for caliper settings --- cms/envs/production.py | 8 +++++--- lms/envs/production.py | 9 ++++++--- .../ucsd_features/templates/logs_not_sent_email.html | 10 ---------- requirements/edx/base.txt | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 openedx/features/ucsd_features/templates/logs_not_sent_email.html diff --git a/cms/envs/production.py b/cms/envs/production.py index 41f798c644b6..ffc356c139c3 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -614,10 +614,12 @@ INSTALLED_APPS.index('eventtracking.django.apps.EventTrackingConfig'), 'openedx_caliper_tracking' ) -CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') -CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') -CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') + if FEATURES.get('ENABLE_CALIPER_EVENTS_DELIVERY'): + CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') + CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') + if FEATURES.get('ENABLE_KAFKA_FOR_CALIPER'): + CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') ####################### Plugin Settings ########################## # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/lms/envs/production.py b/lms/envs/production.py index 369f301ea9da..8539c5730086 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1151,9 +1151,12 @@ INSTALLED_APPS.index('eventtracking.django.apps.EventTrackingConfig'), 'openedx_caliper_tracking' ) -CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') -CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') -CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') + if FEATURES.get('ENABLE_CALIPER_EVENTS_DELIVERY'): + CALIPER_DELIVERY_ENDPOINT = ENV_TOKENS.get('CALIPER_DELIVERY_ENDPOINT') + CALIPER_DELIVERY_AUTH_TOKEN = AUTH_TOKENS.get('CALIPER_DELIVERY_AUTH_TOKEN') + + if FEATURES.get('ENABLE_KAFKA_FOR_CALIPER'): + CALIPER_KAFKA_SETTINGS = ENV_TOKENS.get('CALIPER_KAFKA_SETTINGS') ############################### Plugin Settings ############################### # This is at the bottom because it is going to load more settings after base settings are loaded diff --git a/openedx/features/ucsd_features/templates/logs_not_sent_email.html b/openedx/features/ucsd_features/templates/logs_not_sent_email.html deleted file mode 100644 index 541ecaeaf6e0..000000000000 --- a/openedx/features/ucsd_features/templates/logs_not_sent_email.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'base_email.html' %} -{% block body %} -

    {{ body }}

    -
    - - {% for info_dict in additional_info.items %} -

    {{info_dict.0}}: {{info_dict.1}}

    -
    - {% endfor %} -{% endblock body %} diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 3d48dcb164b3..9ca23811aea4 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,7 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger -openedx-caliper-tracking==0.11.4 +openedx-caliper-tracking==0.11.5 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From 4be34620b7130eeee0a9b1541f2094626110c05f Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Wed, 30 Oct 2019 20:06:33 +0500 Subject: [PATCH 066/119] initial changes --- .circleci/config.yml | 2 - common/djangoapps/util/views.py | 38 +++------ lms/templates/support/contact_us.html | 2 +- lms/urls.py | 5 -- .../features/ucsd_features/message_types.py | 8 ++ .../ucsd_features/templates/base_email.html | 6 -- .../supportnotification/email/body.html | 34 ++++++++ .../supportnotification/email/body.txt} | 11 ++- .../supportnotification/email/from_name.txt | 1 + .../supportnotification/email/head.html | 1 + .../supportnotification/email/subject.txt | 3 + openedx/features/ucsd_features/tests/tests.py | 59 -------------- openedx/features/ucsd_features/urls.py | 15 ---- openedx/features/ucsd_features/utils.py | 80 +++++++++++-------- openedx/features/ucsd_features/views.py | 28 ------- 15 files changed, 112 insertions(+), 181 deletions(-) create mode 100644 openedx/features/ucsd_features/message_types.py delete mode 100644 openedx/features/ucsd_features/templates/base_email.html create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html rename openedx/features/ucsd_features/templates/{support_email.html => ucsd_features/edx_ace/supportnotification/email/body.txt} (73%) create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt delete mode 100644 openedx/features/ucsd_features/tests/tests.py delete mode 100644 openedx/features/ucsd_features/urls.py delete mode 100644 openedx/features/ucsd_features/views.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 0dd9f758b521..25766827aa54 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,8 +45,6 @@ jobs: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" sudo mongod --config /etc/mongodb.conf & - paver test_system -t openedx/features/ucsd_features/tests/tests.py --fasttest - store_artifacts: path: test-reports destination: test-reports - diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index 8a888c76e49a..930e5bbd889c 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -289,48 +289,28 @@ def _record_feedback_in_zendesk( custom_fields=None ): """ - Create a new user-requested Zendesk ticket or send an email to Support Team. + Create a new user-requested Zendesk ticket. - Use ENABLE_EMAIL_INSTEAD_ZENDESK flag to switch between zendesk ticket or support email - - In case of Zendesk ticket, once created, the ticket will be updated with a private - comment containing additional information from the browser and server, such as HTTP headers - and user state. + Once created, the ticket will be updated with a private comment containing + additional information from the browser and server, such as HTTP headers + and user state. Returns a boolean value indicating whether ticket creation + was successful, regardless of whether the private comment update succeeded. If `group_name` is provided, attaches the ticket to the matching Zendesk group. - If `require_update` is provided, this allows using the private comment to add - necessary information which the user will not see in followup emails from support. + If `require_update` is provided, returns False when the update does not + succeed. This allows using the private comment to add necessary information + which the user will not see in followup emails from support. If `custom_fields` is provided, submits data to those fields in Zendesk. - - Returns: a boolean value indicating if either email was sent successfully or ticket - was created (regardless of whether the private comment update succeeded or not). - """ + zendesk_api = _ZendeskApi() additional_info_string = ( u"Additional information:\n\n" + u"\n".join(u"%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None) ) - if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): - is_email_sent = send_notification_email_to_support( - subject=subject, - body=details, - name=realname, - email=email, - custom_fields=custom_fields, - additional_info=additional_info - ) - return is_email_sent - - zendesk_api = _ZendeskApi() - - if not (settings.ZENDESK_URL and settings.ZENDESK_USER and settings.ZENDESK_API_KEY): - log.error('Zendesk is not configured. Cannot create a ticket.') - return False - # Tag all issues with LMS to distinguish channel in Zendesk; requested by student support team zendesk_tags = list(tags.values()) + ["LMS"] diff --git a/lms/templates/support/contact_us.html b/lms/templates/support/contact_us.html index 8b3ab3a777c8..dbdb93171fe5 100644 --- a/lms/templates/support/contact_us.html +++ b/lms/templates/support/contact_us.html @@ -33,7 +33,7 @@ 'loginQuery': "${login_query() | n, js_escaped_string}", 'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}", 'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}", - 'submitFormUrl': "${reverse('ucsd_support_email') | n, js_escaped_string}", + 'submitFormUrl': "${reverse('zendesk_proxy_v1') | n, js_escaped_string}", 'customFields': ${custom_fields | n, dump_js_escaped_json}, 'tags': ${tags | n, dump_js_escaped_json}, 'supportEmail': "${support_email | n, js_escaped_string}", diff --git a/lms/urls.py b/lms/urls.py index f2ba9400a1c1..ba2be98a95bf 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -45,7 +45,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views from openedx.features.enterprise_support.api import enterprise_enabled -from openedx.features.ucsd_features import urls as ucsd_email_urls from ratelimitbackend import admin from static_template_view import views as static_template_view_views from staticbook import views as staticbook_views @@ -1048,8 +1047,4 @@ url(r'', include('csrf.urls')), ] -urlpatterns += [ - url(r'^ucsd_email/', include(ucsd_email_urls)) -] - urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS)) diff --git a/openedx/features/ucsd_features/message_types.py b/openedx/features/ucsd_features/message_types.py new file mode 100644 index 000000000000..c74ff451a21f --- /dev/null +++ b/openedx/features/ucsd_features/message_types.py @@ -0,0 +1,8 @@ +from openedx.core.djangoapps.ace_common.message import BaseMessageType + + +class SupportNotification(BaseMessageType): + """ + A message for notifying support. + """ + pass diff --git a/openedx/features/ucsd_features/templates/base_email.html b/openedx/features/ucsd_features/templates/base_email.html deleted file mode 100644 index 83c6201a46c7..000000000000 --- a/openedx/features/ucsd_features/templates/base_email.html +++ /dev/null @@ -1,6 +0,0 @@ - - - {% block body %} - {% endblock body %} - - diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html new file mode 100644 index 000000000000..da368e521bd2 --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html @@ -0,0 +1,34 @@ +{% extends 'ace_common/edx_ace/common/base_body.html' %} + +{% load i18n %} +{% load static %} +{% block content %} + + + + +
    +

    Requester Name: {{name}}

    +

    Requester Email: {{email}}

    +

    {{ body }}

    +
    + {% if course %} + Course Name: {{course}} +
    + {% endif %} + {% if custom_fields or additional_info %} + {% for field_dict in custom_fields %} + {% for key, value in field_dict.items %} +

    {{key}}: {{value}}

    + {% endfor %} + {% endfor %} + {% for info_dict in additional_info.items %} +

    {{info_dict.0}}: {{info_dict.1}}

    + {% endfor %} + {% endif %} + + {% block google_analytics_pixel %} + + {% endblock %} +
    +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/support_email.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt similarity index 73% rename from openedx/features/ucsd_features/templates/support_email.html rename to openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt index b37c33fb8cd5..c656d4d04f75 100644 --- a/openedx/features/ucsd_features/templates/support_email.html +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt @@ -1,5 +1,6 @@ -{% extends 'base_email.html' %} -{% block body %} +{% load i18n %} + +{% block content %}

    Requester Name: {{name}}

    Requester Email: {{email}}

    {{ body }}

    @@ -18,4 +19,8 @@

    {{info_dict.0}}: {{info_dict.1}}

    {% endfor %} {% endif %} -{% endblock body %} +{% endblock %} + +{% block google_analytics_pixel %} + +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt new file mode 100644 index 000000000000..dcbc23c00480 --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt @@ -0,0 +1 @@ +{{ platform_name }} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html new file mode 100644 index 000000000000..366ada7ad92e --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html @@ -0,0 +1 @@ +{% extends 'ace_common/edx_ace/common/base_head.html' %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt new file mode 100644 index 000000000000..b26a0f47382a --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt @@ -0,0 +1,3 @@ +{% load i18n %} + +{% blocktrans %}{{ subject }}{% endblocktrans %} diff --git a/openedx/features/ucsd_features/tests/tests.py b/openedx/features/ucsd_features/tests/tests.py deleted file mode 100644 index e1c36a8f1f7b..000000000000 --- a/openedx/features/ucsd_features/tests/tests.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -import mock - -from django.http import HttpResponse -from django.test import Client -from django.test import TestCase -from django.urls import reverse - -from rest_framework import status - - -class SupportEmailTestCase(TestCase): - """ - Tests email sending via contact/support form. - """ - - @classmethod - def setUpClass(cls): - super(SupportEmailTestCase, cls).setUpClass() - cls.client = Client() - cls.send_mail_url = reverse('ucsd_support_email') - cls.test_email = { - 'subject': 'Subject goes here', - 'comment': {'body': 'here goes the body/details'}, - 'tags': ['LMS'], - 'requester': { - 'email': 'edx@example.com', - 'name': 'edx' - }, - 'custom_fields': [{ - 'value': 'course-v1:edX+DemoX+Demo_Course' - }] - } - - def test_send_email_successful(self): - """ - Make sure email works successfully. - """ - response = self.client.post(self.send_mail_url, - data=json.dumps(self.test_email), - content_type='application/json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - @mock.patch( - 'django.test.Client.post', return_value=HttpResponse( - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ), autospec=True - ) - def test_send_email_fail(self, mock_func): - """ - Make sure email fails successfully. - """ - - response = self.client.post(self.send_mail_url, - data=json.dumps(self.test_email), - content_type='application/json') - - self.assertEqual(response.status_code, - status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/openedx/features/ucsd_features/urls.py b/openedx/features/ucsd_features/urls.py deleted file mode 100644 index 9800736e3de7..000000000000 --- a/openedx/features/ucsd_features/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Map urls to the relevant view handlers -""" - -from django.conf.urls import url -from .views import email_support - - -urlpatterns = [ - url( - r'^ucsd_support_email$', - email_support, - name='ucsd_support_email' - ) -] diff --git a/openedx/features/ucsd_features/utils.py b/openedx/features/ucsd_features/utils.py index 42e2e973f94b..558bf08a6250 100644 --- a/openedx/features/ucsd_features/utils.py +++ b/openedx/features/ucsd_features/utils.py @@ -1,50 +1,66 @@ import json import logging -from smtplib import SMTPException from django.conf import settings -from django.core.mail import EmailMultiAlternatives, send_mail -from django.template.loader import get_template +from edx_ace import ace +from edx_ace.recipient import Recipient +from openedx.core.djangoapps.ace_common.template_context import get_base_template_context +from openedx.core.djangoapps.theming.helpers import get_current_site +from openedx.features.ucsd_features.message_types import SupportNotification log = logging.getLogger(__name__) TEMPLATE_PATH = '{key}_email.html' -def send_notification(key, data, subject, from_email, dest_emails): +def send_notification(message_type, data, subject, dest_emails): """ - Send an email. + Send an email - params: - key - Email template will be selected on the basis of key + Arguments: + message_type - string value to select ace message object data - Dict containing context/data for the template subject - Email subject - from_email - Email address to send email dest_emails - List of destination emails - return: a boolean variable indicating email response. + Returns: + a boolean variable indicating email response. """ + message_types = { + 'support': SupportNotification, + } + current_site = get_current_site() content = json.dumps(data) - email_template_path = TEMPLATE_PATH.format(key=key) - html_content = get_template(email_template_path).render(data) - msg = EmailMultiAlternatives(subject, content, from_email, dest_emails) - msg.attach_alternative(html_content, "text/html") - try: - response = msg.send() - log.info( - 'Email has been sent from "%s" to "%s" for content %s.', - from_email, - dest_emails, - content - ) - return response - except SMTPException: - log.error( - 'Unable to send an email from "%s" to %s for content "%s".', - from_email, - dest_emails, - content - ) - return False + data.update( + { + 'subject': subject, + 'site': current_site + } + ) + message_context = get_base_template_context(current_site) + message_context.update(data) + message_class = message_types[message_type] + return_value = False + for email in dest_emails: + try: + message = message_class().personalize( + recipient=Recipient(username='', email_address=email), + language='en', + user_context=message_context, + ) + ace.send(message) + log.info( + 'Email has been sent to "%s" for content %s.', + email, + content + ) + return_value = True + except Exception: + log.error( + 'Unable to send an email to %s for content "%s".', + email, + content + ) + return return_value def send_notification_email_to_support(subject, body, name, email, custom_fields=None, additional_info=None, course=None): @@ -53,7 +69,6 @@ def send_notification_email_to_support(subject, body, name, email, custom_fields """ key = "support" dest_emails = settings.SUPPORT_DESK_EMAILS - from_address = settings.DEFAULT_FROM_EMAIL data = { 'name': name, 'email': email, @@ -61,6 +76,5 @@ def send_notification_email_to_support(subject, body, name, email, custom_fields 'custom_fields': custom_fields, 'additional_info': additional_info, } - email_response = send_notification( - key, data, subject, from_address, dest_emails) + email_response = send_notification(key, data, subject, dest_emails) return email_response diff --git a/openedx/features/ucsd_features/views.py b/openedx/features/ucsd_features/views.py deleted file mode 100644 index d8a0b3ba1d4e..000000000000 --- a/openedx/features/ucsd_features/views.py +++ /dev/null @@ -1,28 +0,0 @@ -import json -from django.conf import settings -from rest_framework import status -from django.http import HttpResponse -from django.core.mail import send_mail -from django.views.decorators.http import require_http_methods -from openedx.features.ucsd_features.utils import send_notification_email_to_support - - -@require_http_methods(["POST"]) -def email_support(request): - """ - A View that will send user support (contact-form) emails to a specific - account. - """ - body = json.loads(request.body) - - response = send_notification_email_to_support( - subject=body['subject'], - body=body['comment']['body'], - name=body['requester']['name'], - email=body['requester']['email'], - course=body['custom_fields'][0]['value'] - ) - if response: - return HttpResponse(status=status.HTTP_201_CREATED) - else: - return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) From 1dc14f918207d58552b2c5c28db348a6d72e6e45 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Wed, 6 Nov 2019 16:18:19 +0500 Subject: [PATCH 067/119] Update caliper app version --- requirements/edx/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9ca23811aea4..50754d39a552 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,7 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger -openedx-caliper-tracking==0.11.5 +openedx-caliper-tracking==0.11.6 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From f1f88be2e2a178c0a7f30663e9e42dba1ae115c8 Mon Sep 17 00:00:00 2001 From: iamhassantariq <23108499+imhassantariq@users.noreply.github.com@users.noreply.github.com> Date: Wed, 6 Nov 2019 16:46:20 +0500 Subject: [PATCH 068/119] Added Email Content for Support --- lms/djangoapps/commerce/utils.py | 1 + .../core/djangoapps/zendesk_proxy/utils.py | 1 + .../features/ucsd_features/message_types.py | 9 ++++- .../email/body.html | 29 +++++++++++++++ .../email/body.txt | 21 +++++++++++ .../email/from_name.txt | 0 .../email/head.html | 0 .../email/subject.txt | 3 ++ .../email/body.html | 27 ++++++++++++++ .../contactsupportnotification/email/body.txt | 14 ++++++++ .../email/from_name.txt | 1 + .../email/head.html | 1 + .../email/subject.txt | 3 ++ .../supportnotification/email/body.html | 34 ------------------ .../supportnotification/email/body.txt | 26 -------------- .../supportnotification/email/subject.txt | 3 -- openedx/features/ucsd_features/utils.py | 36 +++++++++++++------ 17 files changed, 134 insertions(+), 75 deletions(-) create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt rename openedx/features/ucsd_features/templates/ucsd_features/edx_ace/{supportnotification => commercesupportnotification}/email/from_name.txt (100%) rename openedx/features/ucsd_features/templates/ucsd_features/edx_ace/{supportnotification => commercesupportnotification}/email/head.html (100%) create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/from_name.txt create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/head.html create mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt delete mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html delete mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt delete mode 100644 openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index ec85dd8e4457..c6da352cbb04 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -367,6 +367,7 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N """ if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): is_email_sent = send_notification_email_to_support( + message_type='commerce_support', subject=subject, body=body, name=requester_name, diff --git a/openedx/core/djangoapps/zendesk_proxy/utils.py b/openedx/core/djangoapps/zendesk_proxy/utils.py index 8c4a18389c2e..d7c2aa80fce5 100644 --- a/openedx/core/djangoapps/zendesk_proxy/utils.py +++ b/openedx/core/djangoapps/zendesk_proxy/utils.py @@ -27,6 +27,7 @@ def _std_error_message(details, payload): if settings.FEATURES.get("ENABLE_EMAIL_INSTEAD_ZENDESK", True): is_email_sent = send_notification_email_to_support( + message_type='contact_support', subject=subject, body=body, name=requester_name, diff --git a/openedx/features/ucsd_features/message_types.py b/openedx/features/ucsd_features/message_types.py index c74ff451a21f..ffb9e40edebc 100644 --- a/openedx/features/ucsd_features/message_types.py +++ b/openedx/features/ucsd_features/message_types.py @@ -1,7 +1,14 @@ from openedx.core.djangoapps.ace_common.message import BaseMessageType -class SupportNotification(BaseMessageType): +class ContactSupportNotification(BaseMessageType): + """ + A message for notifying support. + """ + pass + + +class CommerceSupportNotification(BaseMessageType): """ A message for notifying support. """ diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html new file mode 100644 index 000000000000..9f7b525f7b8a --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html @@ -0,0 +1,29 @@ +{% extends 'ace_common/edx_ace/common/base_body.html' %} + +{% load i18n %} +{% load static %} +{% block content %} + + + + +
    +

    Hello,

    +

    LearnX was not able to process a user request, and we need to take action.

    +

    Here are the details:

    +

    Learner: {{name}} ({{email}})

    +

    + {% if course %} + Course: {{course}} +
    + {% endif %} +

    +

    --------------------Request Details--------------------

    + +

    {{ body }}

    + + {% block google_analytics_pixel %} + + {% endblock %} +
    +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt new file mode 100644 index 000000000000..4f264522028d --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt @@ -0,0 +1,21 @@ +{% load i18n %} + +{% block content %} + Hello, + LearnX was not able to process a user request, and we need to take action. + Here are the details: + Learner: {{name}} ({{email}}) + + {% if course %} + Course: {{course}} + {% endif %} + + --------------------Request Details-------------------- + + {{ body }} + +{% endblock %} + +{% block google_analytics_pixel %} + +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/from_name.txt similarity index 100% rename from openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/from_name.txt rename to openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/from_name.txt diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/head.html similarity index 100% rename from openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/head.html rename to openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/head.html diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt new file mode 100644 index 000000000000..1031e9f24aa5 --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt @@ -0,0 +1,3 @@ +{% load i18n %} + +{% blocktrans %}[LearnX] Administrative Action Needed {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html new file mode 100644 index 000000000000..f5dcda67096f --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html @@ -0,0 +1,27 @@ +{% extends 'ace_common/edx_ace/common/base_body.html' %} + +{% load i18n %} +{% load static %} +{% block content %} + + + + +
    +

    Hello,

    +

    We were contacted by a user on LearnX @ UC San Diego. Please take a moment to review and respond.

    +

    Learner: {{name}} ({{email}})

    +

    + {% if course %} + Course: {{course}} +
    + {% endif %} +

    +

    --------------------Request Details--------------------

    +

    Subject: {{subject}}

    +

    {{ body }}

    + {% block google_analytics_pixel %} + + {% endblock %} +
    +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt new file mode 100644 index 000000000000..e511d032a1da --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt @@ -0,0 +1,14 @@ +{% load i18n %} + +{% block content %} + Hello, + We were contacted by a user on LearnX @ UC San Diego. Please take a moment to review and respond. + Learner: {{name}} ({{email}}) + {% if course %} + Course: {{course}} + {% endif %} + + --------------------Request Details-------------------- + Subject: {{subject}} + {{ body }} +{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/from_name.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/from_name.txt new file mode 100644 index 000000000000..dcbc23c00480 --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/from_name.txt @@ -0,0 +1 @@ +{{ platform_name }} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/head.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/head.html new file mode 100644 index 000000000000..366ada7ad92e --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/head.html @@ -0,0 +1 @@ +{% extends 'ace_common/edx_ace/common/base_head.html' %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt new file mode 100644 index 000000000000..8406c1f2146e --- /dev/null +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt @@ -0,0 +1,3 @@ +{% load i18n %} + +{% blocktrans %}[LearnX] Support / Information Request {{name}}{% endblocktrans %} {% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html deleted file mode 100644 index da368e521bd2..000000000000 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends 'ace_common/edx_ace/common/base_body.html' %} - -{% load i18n %} -{% load static %} -{% block content %} - - - - -
    -

    Requester Name: {{name}}

    -

    Requester Email: {{email}}

    -

    {{ body }}

    -
    - {% if course %} - Course Name: {{course}} -
    - {% endif %} - {% if custom_fields or additional_info %} - {% for field_dict in custom_fields %} - {% for key, value in field_dict.items %} -

    {{key}}: {{value}}

    - {% endfor %} - {% endfor %} - {% for info_dict in additional_info.items %} -

    {{info_dict.0}}: {{info_dict.1}}

    - {% endfor %} - {% endif %} - - {% block google_analytics_pixel %} - - {% endblock %} -
    -{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt deleted file mode 100644 index c656d4d04f75..000000000000 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/body.txt +++ /dev/null @@ -1,26 +0,0 @@ -{% load i18n %} - -{% block content %} -

    Requester Name: {{name}}

    -

    Requester Email: {{email}}

    -

    {{ body }}

    -
    - {% if course %} - Course Name: {{course}} -
    - {% endif %} - {% if custom_fields or additional_info %} - {% for field_dict in custom_fields %} - {% for key, value in field_dict.items %} -

    {{key}}: {{value}}

    - {% endfor %} - {% endfor %} - {% for info_dict in additional_info.items %} -

    {{info_dict.0}}: {{info_dict.1}}

    - {% endfor %} - {% endif %} -{% endblock %} - -{% block google_analytics_pixel %} - -{% endblock %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt deleted file mode 100644 index b26a0f47382a..000000000000 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/supportnotification/email/subject.txt +++ /dev/null @@ -1,3 +0,0 @@ -{% load i18n %} - -{% blocktrans %}{{ subject }}{% endblocktrans %} diff --git a/openedx/features/ucsd_features/utils.py b/openedx/features/ucsd_features/utils.py index 558bf08a6250..5cc336b89b11 100644 --- a/openedx/features/ucsd_features/utils.py +++ b/openedx/features/ucsd_features/utils.py @@ -2,37 +2,39 @@ import logging from django.conf import settings + from edx_ace import ace from edx_ace.recipient import Recipient +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.ace_common.template_context import get_base_template_context from openedx.core.djangoapps.theming.helpers import get_current_site -from openedx.features.ucsd_features.message_types import SupportNotification +from openedx.features.ucsd_features.message_types import CommerceSupportNotification, ContactSupportNotification log = logging.getLogger(__name__) TEMPLATE_PATH = '{key}_email.html' -def send_notification(message_type, data, subject, dest_emails): +def send_notification(message_type, data, dest_emails): """ Send an email Arguments: message_type - string value to select ace message object data - Dict containing context/data for the template - subject - Email subject dest_emails - List of destination emails Returns: a boolean variable indicating email response. """ message_types = { - 'support': SupportNotification, + 'contact_support': ContactSupportNotification, + 'commerce_support': CommerceSupportNotification } current_site = get_current_site() content = json.dumps(data) data.update( { - 'subject': subject, 'site': current_site } ) @@ -55,7 +57,7 @@ def send_notification(message_type, data, subject, dest_emails): ) return_value = True except Exception: - log.error( + log.exception( 'Unable to send an email to %s for content "%s".', email, content @@ -63,18 +65,30 @@ def send_notification(message_type, data, subject, dest_emails): return return_value -def send_notification_email_to_support(subject, body, name, email, custom_fields=None, additional_info=None, course=None): +def send_notification_email_to_support(subject, body, name, email, message_type, custom_fields=None): """ Sending a notification-email to the Support Team. """ - key = "support" + course = None + if message_type == 'contact_support': + course = get_course_name(custom_fields) dest_emails = settings.SUPPORT_DESK_EMAILS data = { + 'subject': subject, 'name': name, 'email': email, 'body': body, - 'custom_fields': custom_fields, - 'additional_info': additional_info, + 'course': course, + 'custom_fields': custom_fields } - email_response = send_notification(key, data, subject, dest_emails) + email_response = send_notification(message_type, data, dest_emails) return email_response + + +def get_course_name(custom_fields): + course_key = custom_fields[0].get('value') + try: + course_name = CourseKey.from_string(course_key).course + except InvalidKeyError: + return None + return course_name From f1a90ff874e0b5e3e0cb2a5509b5e9faaddccb82 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Tue, 12 Nov 2019 18:42:53 +0500 Subject: [PATCH 069/119] We need to remove enable site configuration check to send an email on unenroll (refund failure) --- lms/djangoapps/commerce/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index ec85dd8e4457..572c0b14a2fc 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -327,9 +327,13 @@ def _send_refund_notification(user, refund_ids): tags = ['auto_refund'] - if theming_helpers.is_request_in_themed_site(): - # this is not presently supported with the external service. - raise NotImplementedError("Unable to send refund processing emails to support teams.") + # [UCSD_CUSTOM] These changes are only for UCSD to send refund failure notification on unenroll + # We need to remove this check as by default, edX does not create zendesk ticket + # if site_configurations are enabled. + + # if theming_helpers.is_request_in_themed_site(): + # # this is not presently supported with the external service. + # raise NotImplementedError("Unable to send refund processing emails to support teams.") # Build the information for the ZenDesk ticket student = user From 6dcd25776d14e810f4d649ea7bb5c171ea9be51e Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Thu, 14 Nov 2019 15:03:35 +0500 Subject: [PATCH 070/119] replace ECOMMERCE_COOKIE_DOMAIN with SESSION_COOKIE_DOMAIN --- common/djangoapps/student/views/dashboard.py | 2 +- lms/envs/production.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 82b2f4ffe247..85d9c534fc84 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -898,5 +898,5 @@ def student_dashboard(request): response = render_to_response('dashboard.html', context) set_logged_in_cookies(request, response, user) response.delete_cookie( - ECOMMERCE_TRANSACTION_COOKIE_NAME, domain=settings.ECOMMERCE_COOKIE_DOMAIN) + ECOMMERCE_TRANSACTION_COOKIE_NAME, domain=settings.SESSION_COOKIE_DOMAIN) return response diff --git a/lms/envs/production.py b/lms/envs/production.py index 8539c5730086..f395514cc0cf 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -108,11 +108,6 @@ ENV_TOKENS = json.load(env_file) -# Authorizenet payment processor set a cookie for dashboard to show pending course purchased dashoard -# notification. This cookie domain will be used to set and delete that cookie. -ECOMMERCE_COOKIE_DOMAIN = ENV_TOKENS.get('ECOMMERCE_COOKIE_DOMAIN', None) - - # STATIC_ROOT specifies the directory where static files are # collected STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) From 6c0872c3b29f0892113a401989f5b6bbec67ea24 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Tue, 19 Nov 2019 14:43:36 +0500 Subject: [PATCH 071/119] FIX: Double event logs are emitted for login operations BUG: Event was being fired from an outer (wrapper) method as well as inner login method (view) --- openedx/core/djangoapps/user_api/helpers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openedx/core/djangoapps/user_api/helpers.py b/openedx/core/djangoapps/user_api/helpers.py index d6b2138feed2..2ca438eda9d6 100644 --- a/openedx/core/djangoapps/user_api/helpers.py +++ b/openedx/core/djangoapps/user_api/helpers.py @@ -516,17 +516,6 @@ def _inner(request): # pylint: disable=missing-docstring else: response.content = msg - if response.status_code == 200: - event_name = 'edx.user.login' - event_data = { - 'email': request.POST.get('email'), - 'remember': request.POST.get('remember'), - 'username': request.user.username, - 'user_id': request.user.id - } - event_data.update(response_dict) - tracker.emit(event_name, event_data) - # Return the response, preserving the original headers. # This is really important, since the student views set cookies From 562907d45ed5171696ec2279afbf945364e590c2 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Fri, 29 Nov 2019 15:28:37 +0500 Subject: [PATCH 072/119] Update openedx-caliper-tracking app version to 0.11.7 --- requirements/edx/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 50754d39a552..de6fa0554b25 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,7 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger -openedx-caliper-tracking==0.11.6 +openedx-caliper-tracking==0.11.7 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From 725b1baafe2748acbb1b504569150055d44bdae8 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Fri, 29 Nov 2019 16:53:03 +0500 Subject: [PATCH 073/119] Use "/tos" instead of "/honor" as "Terms of service and honor" url --- openedx/core/djangoapps/user_api/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_api/api.py b/openedx/core/djangoapps/user_api/api.py index 541826aac9f9..29bcd8d0ea43 100644 --- a/openedx/core/djangoapps/user_api/api.py +++ b/openedx/core/djangoapps/user_api/api.py @@ -845,7 +845,9 @@ def _add_honor_code_field(self, form_desc, required=True): # Translators: This is a legal document users must agree to # in order to register a new account. terms_label = _(u"Terms of Service and Honor Code") - terms_link = marketing_link("HONOR") + # [UCSD_CUSTOM] we want to use "tos" page as "HONOR" page + # so we will use "/tos" url instead of "/honor" + terms_link = marketing_link("TOS") # Translators: "Terms of Service" is a legal document users must agree to # in order to register a new account. From 2c0a874373e85d1736f3e24ab4e76728efc9eb2a Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Mon, 9 Dec 2019 12:29:30 +0500 Subject: [PATCH 074/119] Add pr template --- .github/PULL_REQUEST_TEMPLATE.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..f9b00a9a529c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +#### Story Link +[Text to be displayed](Ticket link) + +#### PR Description + +Please include a summary of the change/issue and include relevant information. List any dependencies that are required for this change. + +#### Type of change + +Please select the options that are relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation Change + +#### How to test? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configurations. + +- [ ] Test A +- [ ] Test B + +#### Checklist before merging: + +- [ ] Squased +- [ ] Reviewd From 9b171438d9760d6a8809faebab95f7155d44f923 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Thu, 12 Dec 2019 12:33:21 +0500 Subject: [PATCH 075/119] Add tests for ucsd_features and modify existing tests accordingly --- cms/envs/test.py | 6 ++ lms/djangoapps/commerce/tests/test_utils.py | 31 ++++++++ lms/envs/test.py | 5 ++ .../zendesk_proxy/tests/test_utils.py | 59 +++++++++++++- .../features/ucsd_features/tests/__init__.py | 0 .../ucsd_features/tests/test_utils.py | 77 +++++++++++++++++++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 openedx/features/ucsd_features/tests/__init__.py create mode 100644 openedx/features/ucsd_features/tests/test_utils.py diff --git a/cms/envs/test.py b/cms/envs/test.py index 849130059c52..2d21e6ca6e5d 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -355,3 +355,9 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + + +################## UCSD Features ###################################### + +INSTALLED_APPS.append('openedx.features.ucsd_features') +FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 3137d71d3d95..0d7d0909bc4b 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -269,6 +269,37 @@ def test_ecommerce_refund_failed_process_notification_sent(self, mock_send_notif assert call_args[0] == (course_entitlement.user, [1]) assert refund_success + @httpretty.activate + @patch('lms.djangoapps.commerce.utils.send_notification_email_to_support', return_value=True) + def test_ecommerce_refund_failed_process_notification_email_sent(self, mock_send_notification): + """ + Test that email is sent for notification instead of creating a ticket on zendesk when + `ENABLE_EMAIL_INSTEAD_ZENDESK` feature flag is set to `True` + """ + features = settings.FEATURES.copy() + features['ENABLE_EMAIL_INSTEAD_ZENDESK'] = True + with override_settings(FEATURES=features): + httpretty.register_uri( + httpretty.POST, + settings.ECOMMERCE_API_URL + 'refunds/', + status=201, + body='[1]', + content_type='application/json' + ) + httpretty.register_uri( + httpretty.PUT, + settings.ECOMMERCE_API_URL + 'refunds/1/process/', + status=400, + body='{}', + content_type='application/json' + ) + course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED) + refund_success = refund_entitlement(course_entitlement) + assert mock_send_notification.called + assert refund_success + call_args = list(mock_send_notification.call_args) + assert call_args[1]['message_type'] == 'commerce_support' + @httpretty.activate @patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True) def test_ecommerce_refund_not_verified_notification_for_entitlement(self, mock_send_notification): diff --git a/lms/envs/test.py b/lms/envs/test.py index d97b18a902d0..c534cb28169c 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -620,4 +620,9 @@ derive_settings(__name__) +################## UCSD Features ###################################### + SUPPORT_DESK_EMAILS = ['servicedesk@ucsd.edu'] + +INSTALLED_APPS.append('openedx.features.ucsd_features') +FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False diff --git a/openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py b/openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py index b02e5685f937..e213c8a6cb61 100644 --- a/openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py +++ b/openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py @@ -1,7 +1,10 @@ import ddt -from django.test.utils import override_settings from mock import MagicMock, patch +from django.conf import settings +from django.test.utils import override_settings +from rest_framework import status + from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket from openedx.core.lib.api.test_utils import ApiTestCase @@ -56,3 +59,57 @@ def test_unexpected_error_pinging_zendesk(self): body=self.request_data['body'], ) self.assertEqual(status_code, 500) + + @patch('openedx.core.djangoapps.zendesk_proxy.utils.send_notification_email_to_support', return_value=True) + def test_send_email_instead_zendesk_ticket_successfully(self, mocked_send_notification): + """ + Test that email is sent for notification instead of creating a ticket on zendesk when + `ENABLE_EMAIL_INSTEAD_ZENDESK` feature flag is set to `True`. If email is send successfully + return 201 status code. + """ + notification_message_type = 'contact_support' + + features = settings.FEATURES.copy() + features['ENABLE_EMAIL_INSTEAD_ZENDESK'] = True + + with override_settings(FEATURES=features): + return_value = create_zendesk_ticket( + requester_name=self.request_data['name'], + requester_email=self.request_data['email'], + subject=self.request_data['subject'], + body=self.request_data['body'], + ) + + self.assertTrue(mocked_send_notification.called) + self.assertEqual( + mocked_send_notification.call_args[1]['message_type'], + notification_message_type + ) + self.assertEqual(return_value, status.HTTP_201_CREATED) + + @patch('openedx.core.djangoapps.zendesk_proxy.utils.send_notification_email_to_support', return_value=False) + def test_send_email_instead_zendesk_ticket_failed(self, mocked_send_notification): + """ + Test that email is sent for notification instead of creating a ticket on zendesk when + `ENABLE_EMAIL_INSTEAD_ZENDESK` feature flag is set to `True`. If email is not sent successfully, + return 503 status code. + """ + notification_message_type = 'contact_support' + + features = settings.FEATURES.copy() + features['ENABLE_EMAIL_INSTEAD_ZENDESK'] = True + + with override_settings(FEATURES=features): + return_value = create_zendesk_ticket( + requester_name=self.request_data['name'], + requester_email=self.request_data['email'], + subject=self.request_data['subject'], + body=self.request_data['body'], + ) + + self.assertTrue(mocked_send_notification.called) + self.assertEqual( + mocked_send_notification.call_args[1]['message_type'], + notification_message_type + ) + self.assertEqual(return_value, status.HTTP_503_SERVICE_UNAVAILABLE) diff --git a/openedx/features/ucsd_features/tests/__init__.py b/openedx/features/ucsd_features/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/tests/test_utils.py b/openedx/features/ucsd_features/tests/test_utils.py new file mode 100644 index 000000000000..51a9b8581777 --- /dev/null +++ b/openedx/features/ucsd_features/tests/test_utils.py @@ -0,0 +1,77 @@ +import ddt + +from django.conf import settings +from django.test import TestCase +from mock import patch, ANY + +from edx_ace.recipient import Recipient +from opaque_keys.edx.keys import CourseKey +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from openedx.features.ucsd_features.message_types import ContactSupportNotification, CommerceSupportNotification +from openedx.features.ucsd_features.utils import send_notification, send_notification_email_to_support + + +MESSAGE_TYPES = { + 'contact_support': ContactSupportNotification, + 'commerce_support': CommerceSupportNotification +} + + +@ddt.ddt +class UCSDFeaturesUtilsTests(ModuleStoreTestCase): + """ Tests for the utils used by ucsd-specific customizations """ + + def setUp(self): + super(UCSDFeaturesUtilsTests, self).setUp() + self.test_emails = ['test@demo.com', ] + self.course = CourseFactory.create() + + @ddt.data(*MESSAGE_TYPES) + @patch('openedx.features.ucsd_features.utils.ace.send') + def test_send_notification_method_success(self, message_type, mocked_send): + """ + Test the send_notification method success flow + """ + with patch.object(MESSAGE_TYPES[message_type], 'personalize') as mocked_personalize: + test_recipient = Recipient(username='', email_address=self.test_emails[0]) + returned_value = send_notification(message_type, {}, self.test_emails) + + mocked_send.assertCalled() + self.assertTrue(returned_value) + mocked_personalize.assert_called_with(user_context=ANY, recipient=test_recipient, language='en') + + @ddt.data(*MESSAGE_TYPES) + @patch('openedx.features.ucsd_features.utils.ace.send', side_effect=Exception) + def test_send_notification_method_failure(self, message_type, mocked_send): + """ + Test the send_notification method failure flow. In case of any exception, + that exception is handled and `False` is returned. + """ + returned_value = send_notification(message_type, {}, self.test_emails) + + mocked_send.assertCalled() + self.assertFalse(returned_value) + + @patch('openedx.features.ucsd_features.utils.send_notification', return_value=True) + def test_send_notification_email_to_support_method(self, mocked_send_notification): + """ + Test the send_notification_email_to_support method and verify that `send_notification` method + is called with correct parameters. + """ + test_course_key = unicode(self.course.id) + dest_emails = settings.SUPPORT_DESK_EMAILS + test_data = { + 'subject': 'test_subject', + 'name': 'test_name', + 'email': 'test_email', + 'body': 'test_body', + 'custom_fields': [ + {'value': test_course_key} + ] + } + + response = send_notification_email_to_support(message_type='contact_support', **test_data) + mocked_send_notification.assert_called_with('contact_support', ANY, dest_emails) + self.assertTrue(response) From 94be6b84f2ffe9f931f56f0212018a2500b44e84 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Tue, 17 Dec 2019 14:38:13 +0500 Subject: [PATCH 076/119] Automatically verify all users and add a management command --- lms/djangoapps/verify_student/models.py | 16 ++++++ lms/envs/common.py | 5 ++ lms/envs/production.py | 9 ++-- lms/envs/test.py | 1 + openedx/features/ucsd_features/apps.py | 4 ++ .../ucsd_features/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/verify_all_users.py | 44 +++++++++++++++++ .../management/tests/__init__.py | 0 .../management/tests/test_verify_all_users.py | 49 +++++++++++++++++++ openedx/features/ucsd_features/signals.py | 31 ++++++++++++ .../ucsd_features/tests/test_signals.py | 38 ++++++++++++++ 12 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 openedx/features/ucsd_features/management/__init__.py create mode 100644 openedx/features/ucsd_features/management/commands/__init__.py create mode 100644 openedx/features/ucsd_features/management/commands/verify_all_users.py create mode 100644 openedx/features/ucsd_features/management/tests/__init__.py create mode 100644 openedx/features/ucsd_features/management/tests/test_verify_all_users.py create mode 100644 openedx/features/ucsd_features/signals.py create mode 100644 openedx/features/ucsd_features/tests/test_signals.py diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index f333b018886a..dd34ad551ac2 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -167,6 +167,22 @@ def should_display_status_to_user(self): """ return False + @property + def expiration_datetime(self): + """ + [UCSD_CUSTOM] Datetime that the verification will expire. + if `AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION` flag is set to True, + User's verification should never expire. + """ + if settings.FEATURES.get('AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'): + # We have to add timezone info here because if we don't do that, + # datetime.max itself is not timezone aware and in the method: + # edx-platform/lms/djangoapps/verify_student/utils.py:is_verification_expiring_soon + # when this value is subtracted from datetime.datetime.now(pytz.UTC)) (timezone aware) + # an exception is thrown. + return datetime.max.replace(tzinfo=pytz.UTC) + return super(ManualVerification, self).expiration_datetime + class SSOVerification(IDVerificationAttempt): """ diff --git a/lms/envs/common.py b/lms/envs/common.py index 217c5be52595..c89742b6a178 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3484,3 +3484,8 @@ def _make_locale_paths(settings): from openedx.core.djangoapps.plugins import plugin_apps, plugin_settings, constants as plugin_constants INSTALLED_APPS.extend(plugin_apps.get_apps(plugin_constants.ProjectType.LMS)) plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.COMMON) + + +############## UCSD Features ######################### + +FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True diff --git a/lms/envs/production.py b/lms/envs/production.py index f395514cc0cf..c5848a54d6b9 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1104,10 +1104,6 @@ ############## Settings for Writable Gradebook ######################### WRITABLE_GRADEBOOK_URL = ENV_TOKENS.get('WRITABLE_GRADEBOOK_URL', WRITABLE_GRADEBOOK_URL) -############### Settings for UCSD Support ##################### -SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') -INSTALLED_APPS.append('openedx.features.ucsd_features') - ############### Settings for Caliper Tracking ##################### # 'openedx_caliper_tracking' app allows us to transform Edx event-logs according to # IMSGlobal Caliper Standards. @@ -1167,3 +1163,8 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) + +############### UCSD Features ##################### + +SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') +INSTALLED_APPS.append('openedx.features.ucsd_features') diff --git a/lms/envs/test.py b/lms/envs/test.py index c534cb28169c..6173937f780c 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -626,3 +626,4 @@ INSTALLED_APPS.append('openedx.features.ucsd_features') FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False +FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = False diff --git a/openedx/features/ucsd_features/apps.py b/openedx/features/ucsd_features/apps.py index 604f2d78a9fe..78213ffd662f 100644 --- a/openedx/features/ucsd_features/apps.py +++ b/openedx/features/ucsd_features/apps.py @@ -3,3 +3,7 @@ class UcsdFeatures(AppConfig): name = 'openedx.features.ucsd_features' + + def ready(self): + super(UcsdFeatures, self).ready() + from .signals import * diff --git a/openedx/features/ucsd_features/management/__init__.py b/openedx/features/ucsd_features/management/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/management/commands/__init__.py b/openedx/features/ucsd_features/management/commands/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/management/commands/verify_all_users.py b/openedx/features/ucsd_features/management/commands/verify_all_users.py new file mode 100644 index 000000000000..73150410390d --- /dev/null +++ b/openedx/features/ucsd_features/management/commands/verify_all_users.py @@ -0,0 +1,44 @@ +""" +Django admin command to manually verify the users +""" +from logging import getLogger + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from lms.djangoapps.verify_student.models import ManualVerification + + +logger = getLogger(__name__) + + +class Command(BaseCommand): + """ + This command attempts to manually verify users. + + Example usage: + $ ./manage.py lms verify_all_users + """ + help = 'Command to mark all existing users as Verified' + + def handle(self, *args, **options): + users = User.objects.all() + new_users_count = 0 + for user in users: + try: + logger.info('Generating ManualVerification for user: {}'.format(user.email)) + user, is_created = ManualVerification.objects.get_or_create( + user=user, + status='approved', + defaults={ + 'name': user.profile.name, + 'reason': 'SKIP_IDENTITY_VERIFICATION', + } + ) + if is_created: + new_users_count += 1 + + except Exception: # pylint: disable=broad-except + logger.error('Error while generating ManualVerification for user: %s', user.email, exc_info=True) + + logger.info('{} new user(s) have been verified'.format(new_users_count)) diff --git a/openedx/features/ucsd_features/management/tests/__init__.py b/openedx/features/ucsd_features/management/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/management/tests/test_verify_all_users.py b/openedx/features/ucsd_features/management/tests/test_verify_all_users.py new file mode 100644 index 000000000000..2633e41fbdbe --- /dev/null +++ b/openedx/features/ucsd_features/management/tests/test_verify_all_users.py @@ -0,0 +1,49 @@ +import pytz +from datetime import datetime + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.core.management import call_command +from django.test import TestCase + +from django.test.utils import override_settings + +from lms.djangoapps.verify_student.models import ManualVerification +from student.tests.factories import UserFactory + + +class VerifyAllUsersCommandTest(TestCase): + def test_users_are_verified(self): + """ + Test that all existing users are verified when the command is run + """ + users = [UserFactory() for _ in range(3)] + + pre_command_verifications_count = ManualVerification.objects.all().count() + self.assertEqual(pre_command_verifications_count, 0) + + call_command('verify_all_users') + + post_command_verifications_count = ManualVerification.objects.all().count() + self.assertEqual(post_command_verifications_count, 3) + + def test_already_verified_users_are_not_verified_again(self): + """ + Test that already verified users are not verified again when the command is run + """ + + users = [UserFactory() for _ in range(3)] + for user in users: + ManualVerification.objects.create( + user=user, + reason='SKIP_IDENTITY_VERIFICATION_FOR_TEST', + status='approved' + ) + + pre_command_verifications_count = ManualVerification.objects.all().count() + self.assertEqual(pre_command_verifications_count, 3) + + call_command('verify_all_users') + + post_command_verifications_count = ManualVerification.objects.all().count() + self.assertEqual(post_command_verifications_count, 3) diff --git a/openedx/features/ucsd_features/signals.py b/openedx/features/ucsd_features/signals.py new file mode 100644 index 000000000000..a3c81bda1f83 --- /dev/null +++ b/openedx/features/ucsd_features/signals.py @@ -0,0 +1,31 @@ +from logging import getLogger + +from django.conf import settings +from django.dispatch import receiver +from django.db.models.signals import post_save + +from lms.djangoapps.verify_student.models import ManualVerification +from student.models import UserProfile + + +logger = getLogger(__name__) + + +@receiver(post_save, sender=UserProfile) +def generate_manual_verification_for_user(sender, instance, **kwargs): + """ + Generate ManualVerification for the User (whose UserProfile instance has been created). + """ + if not settings.FEATURES.get('AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'): + return + + logger.info('Generating ManualVerification for user: {}'.format(instance.user.email)) + try: + ManualVerification.objects.create( + user=instance.user, + status='approved', + reason='SKIP_IDENTITY_VERIFICATION', + name=instance.name + ) + except Exception: # pylint: disable=broad-except + logger.error('Error while generating ManualVerification for user: %s', instance.user.email, exc_info=True) diff --git a/openedx/features/ucsd_features/tests/test_signals.py b/openedx/features/ucsd_features/tests/test_signals.py new file mode 100644 index 000000000000..d41e81590753 --- /dev/null +++ b/openedx/features/ucsd_features/tests/test_signals.py @@ -0,0 +1,38 @@ +import pytz +from datetime import datetime + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.test import TestCase +from django.test.utils import override_settings + +from lms.djangoapps.verify_student.models import ManualVerification +from student.tests.factories import UserFactory + + +class UCSDFeaturesSignalsTests(TestCase): + def test_user_is_verified_after_creation_when_flag_is_set(self): + features = settings.FEATURES.copy() + features['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True + with override_settings(FEATURES=features): + user = UserFactory() + verification_attempt = ManualVerification.objects.get(user=user) + self.assertTrue(verification_attempt) + self.assertEqual(verification_attempt.status, 'approved') + + def test_user_is_not_verified_after_creation_when_flag_is_unset(self): + features = settings.FEATURES.copy() + features['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = False + with override_settings(FEATURES=features): + user = UserFactory() + with self.assertRaises(ObjectDoesNotExist): + verification_attempt = ManualVerification.objects.get(user=user) + + def test_verification_attempt_expiration_datetime(self): + features = settings.FEATURES.copy() + features['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True + with override_settings(FEATURES=features): + user = UserFactory() + verification_attempt = ManualVerification.objects.get(user=user) + expected_expiration_datetime = datetime.max.replace(tzinfo=pytz.UTC) + self.assertEqual(verification_attempt.expiration_datetime, expected_expiration_datetime) From 9b65dce662dc74b21f0a2d19e67c95f79ad89d2f Mon Sep 17 00:00:00 2001 From: danialmalik Date: Mon, 23 Dec 2019 11:28:40 +0500 Subject: [PATCH 077/119] Fix multiple verification upon registration and profile update --- openedx/features/ucsd_features/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/features/ucsd_features/signals.py b/openedx/features/ucsd_features/signals.py index a3c81bda1f83..0fc5083db574 100644 --- a/openedx/features/ucsd_features/signals.py +++ b/openedx/features/ucsd_features/signals.py @@ -12,11 +12,11 @@ @receiver(post_save, sender=UserProfile) -def generate_manual_verification_for_user(sender, instance, **kwargs): +def generate_manual_verification_for_user(sender, instance, created, **kwargs): """ Generate ManualVerification for the User (whose UserProfile instance has been created). """ - if not settings.FEATURES.get('AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'): + if not (settings.FEATURES.get('AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION') and created): return logger.info('Generating ManualVerification for user: {}'.format(instance.user.email)) From 69d2db4bd3dc6bdac1cc28d579a2f36bab97c0b8 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Thu, 26 Dec 2019 16:15:04 +0500 Subject: [PATCH 078/119] Disable refund failure emails --- cms/envs/common.py | 4 ++++ cms/envs/test.py | 1 + lms/djangoapps/commerce/utils.py | 5 +++++ lms/envs/common.py | 1 + lms/envs/test.py | 1 + 5 files changed, 12 insertions(+) diff --git a/cms/envs/common.py b/cms/envs/common.py index a76fcb89e807..a010442be325 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1568,3 +1568,7 @@ # setting for the FileWrapper class used to iterate over the export file data. # See: https://docs.python.org/2/library/wsgiref.html#wsgiref.util.FileWrapper COURSE_EXPORT_DOWNLOAD_CHUNK_SIZE = 8192 + +############## UCSD Features ######################### + +FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = True diff --git a/cms/envs/test.py b/cms/envs/test.py index 2d21e6ca6e5d..224e7474af11 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -361,3 +361,4 @@ INSTALLED_APPS.append('openedx.features.ucsd_features') FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False +FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = False diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 237ea2c718c0..a44a6684e3be 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -308,6 +308,11 @@ def _process_refund(refund_ids, api_client, mode, user, always_notify=False): ) else: try: + # [UCSD_CUSTOM] If this feature flag is True, then it means that we are sending + # the "Refund Failure" emails from ecommerce and thus no need to send that notification + # from here. + if settings.FEATURES.get('DISABLE_REFUND_FAILURE_EMAIL'): + return True return _send_refund_notification(user, refunds_requiring_approval) except: # pylint: disable=bare-except # Unable to send notification to Support, do not break as this method is used by Signals diff --git a/lms/envs/common.py b/lms/envs/common.py index c89742b6a178..1d19cbed4202 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3489,3 +3489,4 @@ def _make_locale_paths(settings): ############## UCSD Features ######################### FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True +FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index 6173937f780c..619c807f78e1 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -626,4 +626,5 @@ INSTALLED_APPS.append('openedx.features.ucsd_features') FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False +FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = False FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = False From 25b8e6d4ddab63f3b8b8dcd96947f92811f80f46 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Wed, 1 Jan 2020 15:21:15 +0500 Subject: [PATCH 079/119] Add test case and logging --- cms/envs/common.py | 2 +- cms/envs/test.py | 2 +- lms/djangoapps/commerce/tests/test_utils.py | 33 +++++++++++++++++++++ lms/djangoapps/commerce/utils.py | 3 +- lms/envs/common.py | 2 +- lms/envs/test.py | 2 +- 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index a010442be325..98c6a48c884d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1571,4 +1571,4 @@ ############## UCSD Features ######################### -FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = True +FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = True diff --git a/cms/envs/test.py b/cms/envs/test.py index 224e7474af11..c0013dc2fe40 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -361,4 +361,4 @@ INSTALLED_APPS.append('openedx.features.ucsd_features') FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False -FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = False +FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = False diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 0d7d0909bc4b..f0a250403fda 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -408,3 +408,36 @@ def test_mode_change_after_refund_seat(self, course_modes, new_mode): assert refund_success self.assertEqual(enrollment.mode, new_mode) + + @httpretty.activate + @patch('slumber.Resource.put', side_effect=Exception()) + @patch('lms.djangoapps.commerce.utils.log.info') + def test_refund_failure_notification_is_not_sent_if_disabled(self, mocked_log_info, mocked_put): + """ + Test that refund failure notification is not sent if feature flag DISABLE_REFUND_FAILURE_NOTIFICATION + is set to True. + """ + features = settings.FEATURES + features['DISABLE_REFUND_FAILURE_NOTIFICATION'] = True + with override_settings(FEATURES=features): + course_id = CourseLocator('test_org', 'test_course_number', 'test_run') + CourseMode.objects.all().delete() + course_mode = 'verified' + CourseModeFactory.create( + course_id=course_id, + mode_slug=course_mode, + mode_display_name=course_mode, + ) + httpretty.register_uri( + httpretty.POST, + settings.ECOMMERCE_API_URL + 'refunds/', + status=201, + body='[1]', + content_type='application/json' + ) + + enrollment = CourseEnrollment.enroll(self.user, course_id, mode=course_mode) + refund_success = refund_seat(enrollment, True) + + mocked_log_info.assert_called_with('Skipping refund failure notification to support') + assert refund_success diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index a44a6684e3be..232d6878a62a 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -311,7 +311,8 @@ def _process_refund(refund_ids, api_client, mode, user, always_notify=False): # [UCSD_CUSTOM] If this feature flag is True, then it means that we are sending # the "Refund Failure" emails from ecommerce and thus no need to send that notification # from here. - if settings.FEATURES.get('DISABLE_REFUND_FAILURE_EMAIL'): + if settings.FEATURES.get('DISABLE_REFUND_FAILURE_NOTIFICATION'): + log.info('Skipping refund failure support notification') return True return _send_refund_notification(user, refunds_requiring_approval) except: # pylint: disable=bare-except diff --git a/lms/envs/common.py b/lms/envs/common.py index 1d19cbed4202..19b7433fb4b7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3489,4 +3489,4 @@ def _make_locale_paths(settings): ############## UCSD Features ######################### FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True -FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = True +FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index 619c807f78e1..36308f5551d7 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -626,5 +626,5 @@ INSTALLED_APPS.append('openedx.features.ucsd_features') FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False -FEATURES['DISABLE_REFUND_FAILURE_EMAIL'] = False +FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = False FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = False From 5a8d1d57602f6fa7670288b4747a03966e7a8852 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Fri, 10 Jan 2020 16:11:58 +0500 Subject: [PATCH 080/119] change learnx to ucsd online --- .../edx_ace/commercesupportnotification/email/body.html | 2 +- .../edx_ace/commercesupportnotification/email/body.txt | 2 +- .../edx_ace/commercesupportnotification/email/subject.txt | 2 +- .../edx_ace/contactsupportnotification/email/body.html | 2 +- .../edx_ace/contactsupportnotification/email/body.txt | 2 +- .../edx_ace/contactsupportnotification/email/subject.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html index 9f7b525f7b8a..ff55b620b96a 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.html @@ -7,7 +7,7 @@

    Hello,

    -

    LearnX was not able to process a user request, and we need to take action.

    +

    UC San Diego Online was not able to process a user request, and we need to take action.

    Here are the details:

    Learner: {{name}} ({{email}})

    diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt index 4f264522028d..331bb35784f7 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/body.txt @@ -2,7 +2,7 @@ {% block content %} Hello, - LearnX was not able to process a user request, and we need to take action. + UC San Diego Online was not able to process a user request, and we need to take action. Here are the details: Learner: {{name}} ({{email}}) diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt index 1031e9f24aa5..36d7b9e3168b 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt @@ -1,3 +1,3 @@ {% load i18n %} -{% blocktrans %}[LearnX] Administrative Action Needed {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} +{% blocktrans %}[UC San Diego Online] Administrative Action Needed {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html index f5dcda67096f..3aa1b67bc38c 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.html @@ -7,7 +7,7 @@

    Hello,

    -

    We were contacted by a user on LearnX @ UC San Diego. Please take a moment to review and respond.

    +

    We were contacted by a user on UC San Diego Online. Please take a moment to review and respond.

    Learner: {{name}} ({{email}})

    ${_("Course Tools")}

    % endif % if upgrade_url and upgrade_price:
    -

    ${_("Pursue a verified certificate")}

    +

    ${_("Pursue a Statement of Accomplishment")}

    From 9b286deec5e6e08712a7376b7ba8692bde439569 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Fri, 7 Feb 2020 16:11:16 +0500 Subject: [PATCH 092/119] increase jenkins test worker timeout --- scripts/xdist/pytest_worker_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/xdist/pytest_worker_manager.py b/scripts/xdist/pytest_worker_manager.py index ca432e04cbd1..4383d55316db 100644 --- a/scripts/xdist/pytest_worker_manager.py +++ b/scripts/xdist/pytest_worker_manager.py @@ -84,7 +84,7 @@ def spin_up_workers(self, number_of_workers, ami, instance_type, subnet, securit not_running = worker_instance_ids[:] ip_addresses = [] all_running = False - for attempt in range(0, self.WORKER_BOOTUP_TIMEOUT_MINUTES * 12): + for attempt in range(0, self.WORKER_BOOTUP_TIMEOUT_MINUTES * 30): try: list_workers_response = self.ec2.describe_instances(InstanceIds=not_running) except: From a1743c3ad8a9ce0ddefb7bb43b6980cd669bd9f0 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Mon, 10 Feb 2020 18:57:24 +0500 Subject: [PATCH 093/119] Update user deletion snippet and its modal --- .../student_account/components/StudentAccountDeletion.jsx | 8 ++++---- .../components/StudentAccountDeletionModal.jsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lms/static/js/student_account/components/StudentAccountDeletion.jsx b/lms/static/js/student_account/components/StudentAccountDeletion.jsx index 556d0a8b59d9..ed501fdc6d40 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletion.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletion.jsx @@ -39,7 +39,7 @@ export class StudentAccountDeletion extends React.Component { render() { const { deletionModalOpen, socialAuthConnected, isActive } = this.state; const loseAccessText = StringUtils.interpolate( - gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), + gettext('You may also lose access to Statements of Accomplishments for courses and programs. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), { htmlStart: '', htmlEnd: '', @@ -73,7 +73,7 @@ export class StudentAccountDeletion extends React.Component { ); const acctDeletionWarningText = StringUtils.interpolate( - gettext('{strongStart}Warning: Account deletion is permanent.{strongEnd} Please read the above carefully before proceeding. This is an irreversible action, and {strongStart}you will no longer be able to use the same email on edX.{strongEnd}'), + gettext('{strongStart}Warning: Account deletion is permanent.{strongEnd} Please read the above carefully before proceeding. This is an irreversible action, and {strongStart}you will no longer be able to use the same email on UC San Diego Online.{strongEnd}'), { strongStart: '', strongEnd: '', @@ -83,8 +83,8 @@ export class StudentAccountDeletion extends React.Component { return (

    { gettext('We’re sorry to see you go!') }

    -

    { gettext('Please note: Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }

    -

    { gettext('Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }

    +

    { gettext('Please note: Deletion of your account and personal data is permanent and cannot be undone. UC San Diego Online will not be able to recover your account or the data that is deleted.') }

    +

    { gettext('Once your account is deleted, you cannot use it to access or take courses through UC San Diego Online.') }

    ', htmlEnd: '', @@ -137,8 +137,8 @@ class StudentAccountDeletionConfirmationModal extends React.Component {

    -

    { gettext('You have selected “Delete my account.” Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }

    -

    { gettext('If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }

    +

    { gettext('You have selected “Delete my account.” Deletion of your account and personal data is permanent and cannot be undone. UC San Diego Online will not be able to recover your account or the data that is deleted.') }

    +

    { gettext('If you proceed, you will be unable to use this account to access or take courses through UC San Diego Online.') }

    From c54e5b543419ef187db1f3d0c3d66b463a0bf936 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Tue, 11 Feb 2020 17:40:25 +0500 Subject: [PATCH 094/119] Update caliper version to 0.11.18 --- requirements/edx/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index de6fa0554b25..0c992897bfb6 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -176,7 +176,7 @@ numpy==1.6.2 oauth2==1.9.0.post1 oauthlib==2.1.0 openapi-codec==1.3.2 # via django-rest-swagger -openedx-caliper-tracking==0.11.7 +openedx-caliper-tracking==0.11.8 path.py==8.2.1 pathtools==0.1.2 paver==1.3.4 From cf0e21ad1c635b4176776954df9b886ae82ad8c3 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 11 Feb 2020 11:25:57 -0800 Subject: [PATCH 095/119] Update api.py Update language per meeting discussion. --- openedx/core/djangoapps/user_api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_api/api.py b/openedx/core/djangoapps/user_api/api.py index 29bcd8d0ea43..e6a983a039c5 100644 --- a/openedx/core/djangoapps/user_api/api.py +++ b/openedx/core/djangoapps/user_api/api.py @@ -878,7 +878,7 @@ def _add_honor_code_field(self, form_desc, required=True): u"By creating an account with {platform_name}, you agree \ to abide by our {platform_name} \ {terms_of_service_link_start}{terms_of_service}{terms_of_service_link_end} \ - and agree to our {privacy_policy_link_start}Privacy Policy{privacy_policy_link_end}." + and read, acknowledge, and understand our {privacy_policy_link_start}Privacy Policy{privacy_policy_link_end}." )).format( platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), terms_of_service=terms_label, From 4831c8705d759b8181275f819c405010507f023a Mon Sep 17 00:00:00 2001 From: imhassantariq Date: Tue, 11 Feb 2020 14:37:49 +0500 Subject: [PATCH 096/119] Fixed the course images tests --- .../courseware/tests/test_courses.py | 2 +- .../courseware/tests/test_module_render.py | 2 +- .../tests/test_course_overviews.py | 33 ++++++++----------- openedx/core/lib/courses.py | 15 +++++---- openedx/core/lib/tests/test_courses.py | 3 +- openedx/features/ucsd_features/signals.py | 8 ----- 6 files changed, 27 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py index c54d98e4e204..6c4ef4bd2ce9 100644 --- a/lms/djangoapps/courseware/tests/test_courses.py +++ b/lms/djangoapps/courseware/tests/test_courses.py @@ -226,7 +226,7 @@ class MongoCourseImageTestCase(ModuleStoreTestCase): def test_get_image_url(self): """Test image URL formatting.""" course = CourseFactory.create(org='edX', course='999') - self.assertEquals(course_image_url(course), '/c4x/edX/999/asset/{0}'.format(course.course_image)) + self.assertEquals(course_image_url(course), '/static/' + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL) def test_non_ascii_image_name(self): # Verify that non-ascii image names are cleaned diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 5f2225623bb3..69f7517e2550 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1595,7 +1595,7 @@ def test_static_asset_path_use(self): def test_course_image(self): url = course_image_url(self.course) - self.assertTrue(url.startswith('/c4x/')) + self.assertTrue(url.startswith('/static/')) self.course.static_asset_path = "toy_course_dir" url = course_image_url(self.course) diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py index ea131edd846f..621f31260786 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py @@ -347,7 +347,7 @@ def test_malformed_grading_policy(self): course_overview = CourseOverview._create_or_update(course) # pylint: disable=protected-access self.assertEqual(course_overview.lowest_passing_grade, None) - @ddt.data((ModuleStoreEnum.Type.mongo, 4, 4), (ModuleStoreEnum.Type.split, 3, 4)) + @ddt.data((ModuleStoreEnum.Type.mongo, 4, 5), (ModuleStoreEnum.Type.split, 3, 4)) @ddt.unpack def test_versioning(self, modulestore_type, min_mongo_calls, max_mongo_calls): """ @@ -702,31 +702,26 @@ def test_disabled_with_prior_data(self, modulestore_type): } ) - @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) - def test_cdn(self, modulestore_type): + def test_cdn(self): """ Test that we return CDN prefixed URLs if it is enabled. """ - with self.store.default_store(modulestore_type): - course = CourseFactory.create(default_store=modulestore_type) - overview = CourseOverview.get_from_id(course.id) + course = CourseFactory.create() + overview = CourseOverview.get_from_id(course.id) - # First the behavior when there's no CDN enabled... - AssetBaseUrlConfig.objects.all().delete() - if modulestore_type == ModuleStoreEnum.Type.mongo: - expected_path_start = "/c4x/" - elif modulestore_type == ModuleStoreEnum.Type.split: - expected_path_start = "/asset-v1:" + # First the behavior when there's no CDN enabled... + AssetBaseUrlConfig.objects.all().delete() + expected_path_start = "/static" - for url in overview.image_urls.values(): - self.assertTrue(url.startswith(expected_path_start)) + for url in overview.image_urls.values(): + self.assertTrue(url.startswith(expected_path_start)) - # Now enable the CDN... - AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org') - expected_cdn_url = "//fakecdn.edx.org" + expected_path_start + # Now enable the CDN... + AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org') + expected_cdn_url = "//fakecdn.edx.org" + expected_path_start - for url in overview.image_urls.values(): - self.assertTrue(url.startswith(expected_cdn_url)) + for url in overview.image_urls.values(): + self.assertTrue(url.startswith(expected_cdn_url)) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_cdn_with_external_image(self, modulestore_type): diff --git a/openedx/core/lib/courses.py b/openedx/core/lib/courses.py index c0f59e780a30..a74afafd1e02 100644 --- a/openedx/core/lib/courses.py +++ b/openedx/core/lib/courses.py @@ -33,12 +33,15 @@ def course_image_url(course, image_key='course_image'): url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL else: loc = StaticContent.compute_location(course.id, getattr(course, image_key)) - try: - AssetManager.find(loc) - except NotFoundError: - url = '/static/' + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL - else: - url = StaticContent.serialize_asset_key_with_slash(loc) + if getattr(course, image_key).endswith('images_course_image.jpg'): + try: + AssetManager.find(loc) + except NotFoundError: + url = '/static/' + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL + return url + + 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 b3f06936a2d0..10433a7e9978 100644 --- a/openedx/core/lib/tests/test_courses.py +++ b/openedx/core/lib/tests/test_courses.py @@ -3,6 +3,7 @@ """ import ddt +from django.conf import settings from django.test.utils import override_settings from xmodule.modulestore import ModuleStoreEnum @@ -29,7 +30,7 @@ def test_get_image_url(self): """Test image URL formatting.""" course = CourseFactory.create() self.verify_url( - unicode(course.id.make_asset_key('asset', course.course_image)), + '/static/' + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL, course_image_url(course) ) diff --git a/openedx/features/ucsd_features/signals.py b/openedx/features/ucsd_features/signals.py index 16739b29c04b..ca24517e2ca0 100644 --- a/openedx/features/ucsd_features/signals.py +++ b/openedx/features/ucsd_features/signals.py @@ -31,11 +31,3 @@ def generate_manual_verification_for_user(sender, instance, created, **kwargs): except Exception: # pylint: disable=broad-except logger.error('Error while generating ManualVerification for user: %s', instance.user.email, exc_info=True) - -@receiver(pre_save, sender=CourseOverview) -def course_image_change(sender, instance, **kwargs): - """ - Change the default course image whenever new course is created - """ - if instance.course_image_url.endswith('images_course_image.jpg'): - instance.course_image_url = "/static/" + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL From 37c2a7221f98aa9614a8136abe982f33cc002b23 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Wed, 12 Feb 2020 18:47:36 +0500 Subject: [PATCH 097/119] changed accomplishments to accomplishment on user deletion --- .../js/student_account/components/StudentAccountDeletion.jsx | 2 +- .../student_account/components/StudentAccountDeletionModal.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/static/js/student_account/components/StudentAccountDeletion.jsx b/lms/static/js/student_account/components/StudentAccountDeletion.jsx index ed501fdc6d40..867acc123efa 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletion.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletion.jsx @@ -39,7 +39,7 @@ export class StudentAccountDeletion extends React.Component { render() { const { deletionModalOpen, socialAuthConnected, isActive } = this.state; const loseAccessText = StringUtils.interpolate( - gettext('You may also lose access to Statements of Accomplishments for courses and programs. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), + gettext('You may also lose access to Statements of Accomplishment for courses and programs. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), { htmlStart: '', htmlEnd: '', diff --git a/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx b/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx index b6ae05d190ee..a4663ed19fa5 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx @@ -94,7 +94,7 @@ class StudentAccountDeletionConfirmationModal extends React.Component { } = this.state; const { onClose } = this.props; const loseAccessText = StringUtils.interpolate( - gettext('You may also lose access to Statements of Accomplishments for courses and programs. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), + gettext('You may also lose access to Statements of Accomplishment for courses and programs. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), { htmlStart: '', htmlEnd: '', From fd4baef2b79c1d05b6e72f588a4aa4c977d84d2f Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Fri, 14 Feb 2020 12:15:31 +0500 Subject: [PATCH 098/119] Update cert name veriables in configuration file named common.py --- lms/envs/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 19b7433fb4b7..e1a943561944 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2559,8 +2559,8 @@ def _make_locale_paths(settings): REGISTRATION_EMAIL_PATTERNS_ALLOWED = None ########################## CERTIFICATE NAME ######################## -CERT_NAME_SHORT = "Certificate" -CERT_NAME_LONG = "Certificate of Achievement" +CERT_NAME_SHORT = "Statement of Accomplishment" +CERT_NAME_LONG = "Statement of Accomplishment" #################### OpenBadges Settings ####################### From 261c3aa044555bd2c1a9093b4d675d97b7838c6f Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Fri, 14 Feb 2020 16:49:25 +0500 Subject: [PATCH 099/119] Add url for what is soa --- lms/djangoapps/static_template_view/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/static_template_view/urls.py b/lms/djangoapps/static_template_view/urls.py index 0371b97a561d..461af2b1c81b 100644 --- a/lms/djangoapps/static_template_view/urls.py +++ b/lms/djangoapps/static_template_view/urls.py @@ -18,6 +18,7 @@ url(r'^contact$', views.render, {'template': 'contact.html'}, name="contact"), url(r'^donate$', views.render, {'template': 'donate.html'}, name="donate"), url(r'^faq$', views.render, {'template': 'faq.html'}, name="faq"), + url(r'^what_is_soa$', views.render, {'template': 'what_is_soa.html'}, name="what_is_soa"), url(r'^help$', views.render, {'template': 'help.html'}, name="help_edx"), url(r'^jobs$', views.render, {'template': 'jobs.html'}, name="jobs"), url(r'^news$', views.render, {'template': 'news.html'}, name="news"), From d2c451c4e64b6a1b4b418431e8743039f539aefe Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Fri, 14 Feb 2020 18:12:27 +0500 Subject: [PATCH 100/119] fixing RefundUtilMethodTests's test_refund_failure_notification_is_not_sent_if_disabled test --- lms/djangoapps/commerce/tests/test_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index f0a250403fda..02eede185d6a 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -439,5 +439,6 @@ def test_refund_failure_notification_is_not_sent_if_disabled(self, mocked_log_in enrollment = CourseEnrollment.enroll(self.user, course_id, mode=course_mode) refund_success = refund_seat(enrollment, True) - mocked_log_info.assert_called_with('Skipping refund failure notification to support') + mocked_log_info.assert_called_with('Skipping support notification for refund failure from edx-platform.' + 'The email will be sent from the ecommerce service') assert refund_success From 8ba85612b7bf75366ed25a94135c59e0a98526c5 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 12:03:27 +0500 Subject: [PATCH 101/119] blocking TestRefundSignal's test_notification_themed_site test due to ucsd changes --- lms/djangoapps/commerce/tests/test_signals.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/commerce/tests/test_signals.py b/lms/djangoapps/commerce/tests/test_signals.py index 152c2a1ccbdd..505c4504a003 100644 --- a/lms/djangoapps/commerce/tests/test_signals.py +++ b/lms/djangoapps/commerce/tests/test_signals.py @@ -229,14 +229,17 @@ def test_notification_error(self, mock_log_warning, mock_send_notification): self.assertTrue(mock_send_notification.called) self.assertTrue(mock_log_warning.called) - @mock.patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site', return_value=True) - def test_notification_themed_site(self, mock_is_request_in_themed_site): # pylint: disable=unused-argument - """ - Ensure the notification function raises an Exception if used in the - context of themed site. - """ - with self.assertRaises(NotImplementedError): - _send_refund_notification(self.course_enrollment.user, [1, 2, 3]) + # [UCSD_CUSTOM] These changes are only for UCSD to send refund failure notification on unenroll + # The check required for this test has been disabled for UCSD + + # @mock.patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site', return_value=True) + # def test_notification_themed_site(self, mock_is_request_in_themed_site): # pylint: disable=unused-argument + # """ + # Ensure the notification function raises an Exception if used in the + # context of themed site. + # """ + # with self.assertRaises(NotImplementedError): + # _send_refund_notification(self.course_enrollment.user, [1, 2, 3]) @ddt.data('email@example.com', 'üñîcode.email@example.com') @mock.patch('lms.djangoapps.commerce.utils.create_zendesk_ticket') From 0529f420f5f981ff876abbe205c7b032b9394cf3 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 12:15:48 +0500 Subject: [PATCH 102/119] FIX: RegistrationViewTest's python tests --- openedx/core/djangoapps/user_api/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 3fe8a4291048..522352fbdcc0 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -1663,7 +1663,7 @@ def test_registration_form_confirm_email(self): ) @override_settings( - MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"}, + MKTG_URLS={"ROOT": "https://www.test.com/", "TOS": "honor"}, ) @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True}) def test_registration_honor_code_mktg_site_enabled(self): @@ -1696,7 +1696,7 @@ def test_registration_honor_code_mktg_site_enabled(self): } ) - @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"}) + @override_settings(MKTG_URL_LINK_MAP={"TOS": "honor", "PRIVACY": "privacy"}) @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False}) def test_registration_honor_code_mktg_site_disabled(self): link_template = "{link_label}" From 55bec9e471fa8a99138c654f283a83fd38e4a0a8 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 12:20:42 +0500 Subject: [PATCH 103/119] FIX: TestCourseEmailContext tests --- lms/djangoapps/bulk_email/tests/test_email.py | 5 +---- lms/djangoapps/commerce/tests/test_signals.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index 98bdd98bf0de..063e4fff84a9 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -662,10 +662,7 @@ def verify_email_context(self, email_context, scheme): self.assertEquals(email_context['platform_name'], settings.PLATFORM_NAME) self.assertEquals(email_context['course_title'], self.course_title) self.assertEquals(email_context['course_url'], - '{}://edx.org/courses/{}/{}/{}/'.format(scheme, - self.course_org, - self.course_number, - self.course_run)) + '{}://edx.org/static/{}'.format(scheme, settings.DEFAULT_COURSE_ABOUT_IMAGE_URL)) self.assertEquals(email_context['course_image_url'], '{}://edx.org/c4x/{}/{}/asset/images_course_image.jpg'.format(scheme, self.course_org, diff --git a/lms/djangoapps/commerce/tests/test_signals.py b/lms/djangoapps/commerce/tests/test_signals.py index 505c4504a003..34339c183b11 100644 --- a/lms/djangoapps/commerce/tests/test_signals.py +++ b/lms/djangoapps/commerce/tests/test_signals.py @@ -231,7 +231,7 @@ def test_notification_error(self, mock_log_warning, mock_send_notification): # [UCSD_CUSTOM] These changes are only for UCSD to send refund failure notification on unenroll # The check required for this test has been disabled for UCSD - + # @mock.patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site', return_value=True) # def test_notification_themed_site(self, mock_is_request_in_themed_site): # pylint: disable=unused-argument # """ From 01f9010b26be41f08649c1fa6083597fc7824ce7 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 18:44:20 +0500 Subject: [PATCH 104/119] FIX: TestCourseEmailContext tests --- lms/djangoapps/bulk_email/tests/test_email.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index 063e4fff84a9..a5d010576e0c 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -662,11 +662,12 @@ def verify_email_context(self, email_context, scheme): self.assertEquals(email_context['platform_name'], settings.PLATFORM_NAME) self.assertEquals(email_context['course_title'], self.course_title) self.assertEquals(email_context['course_url'], - '{}://edx.org/static/{}'.format(scheme, settings.DEFAULT_COURSE_ABOUT_IMAGE_URL)) + '{}://edx.org/courses/{}/{}/{}/'.format(scheme, + self.course_org, + self.course_number, + self.course_run)) self.assertEquals(email_context['course_image_url'], - '{}://edx.org/c4x/{}/{}/asset/images_course_image.jpg'.format(scheme, - self.course_org, - self.course_number)) + '{}://edx.org/static/{}'.format(scheme, settings.DEFAULT_COURSE_ABOUT_IMAGE_URL)) self.assertEquals(email_context['email_settings_url'], '{}://edx.org/dashboard'.format(scheme)) self.assertEquals(email_context['account_settings_url'], '{}://edx.org/account/settings'.format(scheme)) From 7ce48d3b034038eff4d8843e430fbd1d8483df5d Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 18:45:41 +0500 Subject: [PATCH 105/119] FIX: changed content for RegistrationViewTest tests --- openedx/core/djangoapps/user_api/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 522352fbdcc0..53cd3beae38b 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -1677,7 +1677,7 @@ def test_registration_honor_code_mktg_site_enabled(self): "label": (u"By creating an account with {platform_name}, you agree {spacing}" u"to abide by our {platform_name} {spacing}" u"{link_label} {spacing}" - u"and agree to our {link_label2}.").format( + u"and read, acknowledge, and understand our {link_label2}.").format( platform_name=settings.PLATFORM_NAME, link_label=link_template.format(link_label=link_label), link_label2=link_template2.format(link_label=link_label2), @@ -1708,7 +1708,7 @@ def test_registration_honor_code_mktg_site_disabled(self): "label": (u"By creating an account with {platform_name}, you agree {spacing}" u"to abide by our {platform_name} {spacing}" u"{link_label} {spacing}" - u"and agree to our {link_label2}.").format( + u"and read, acknowledge, and understand our {link_label2}.").format( platform_name=settings.PLATFORM_NAME, link_label=self.link_template.format(link_label=link_label), link_label2=link_template.format(link_label=link_label2), From 788baa573d72df49779e3a4135a0564a8d97a2b5 Mon Sep 17 00:00:00 2001 From: HamzaIbnFarooq Date: Mon, 17 Feb 2020 18:58:16 +0500 Subject: [PATCH 106/119] FIX: content change for TestScheduleOverrides tests --- lms/djangoapps/courseware/tests/test_date_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 2c69ab2ded62..29645059edf5 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -596,8 +596,8 @@ def _check_text(self, upgrade_date_summary): self.assertEqual(upgrade_date_summary.title, 'Update Enrollment to Earn Statement of Accomplishment') self.assertEqual( upgrade_date_summary.description, - 'Don\'t miss the opportunity to highlight your new knowledge and skills by earning a verified' - ' certificate.' + 'Don\'t miss the opportunity to highlight your new knowledge and skills by earning a' + ' Statement of Accomplishment' ) self.assertEqual(upgrade_date_summary.relative_datestring, 'by {date}') From 59e5ca09fa0f1e443e089c2988d2041b5dd61ab0 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 18 Feb 2020 11:02:22 -0800 Subject: [PATCH 107/119] Update learner_profile_fields.js Update language per Paul J https://its-pro.ucsd.edu/browse/EDX-399 --- .../static/learner_profile/js/views/learner_profile_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js index 28c24ab37bc2..bf7e5f37ba33 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js +++ b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js @@ -41,7 +41,7 @@ if (this.profileIsPrivate) { this._super( HtmlUtils.interpolateHtml( - gettext('You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}'), // eslint-disable-line max-len + gettext('To share your profile with other UC San Diego Online learners, you must confirm that you are over the age of 13. To confirm, set your Year of Birth on the {account_settings_page_link}'), // eslint-disable-line max-len {account_settings_page_link: accountSettingsLink} ) ); From 17be73bc9b38ccc667029882bc4f262f6f996b0c Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 18 Feb 2020 17:21:27 -0800 Subject: [PATCH 108/119] Update account_settings_factory.js Remove social media account reference --- lms/static/js/student_account/views/account_settings_factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index d8231d9f2ae3..1d967abcabdb 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -354,7 +354,7 @@ { title: gettext('Linked Accounts'), subtitle: StringUtils.interpolate( - gettext('You can link your social media accounts to simplify signing in to {platform_name}.'), + gettext('You can link your UC San Diego Single Sign On account to simplify signing in to {platform_name}.'), {platform_name: platformName} ), fields: _.map(authData.providers, function(provider) { From 9f5b836aedc083b3dd320bca9595a94142902ef7 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Fri, 21 Feb 2020 13:32:37 -0800 Subject: [PATCH 109/119] EDX-395_row 1 of Google doc Row 1 link reference on google doc https://its-pro.ucsd.edu/browse/EDX-395 --- common/djangoapps/student/tests/test_activate_account.py | 2 +- common/djangoapps/student/views/dashboard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/tests/test_activate_account.py b/common/djangoapps/student/tests/test_activate_account.py index 8ea34465ea41..9be046c085de 100644 --- a/common/djangoapps/student/tests/test_activate_account.py +++ b/common/djangoapps/student/tests/test_activate_account.py @@ -122,7 +122,7 @@ def test_account_activation_message(self): self.login() expected_message = ( u"Check your {email_start}{email}{email_end} inbox for an account activation link from " - u"{platform_name}. If you need help, contact {link_start}{platform_name} Support{link_end}." + u"{platform_name}. If you need help, contact {platform_name} Support." ).format( platform_name=self.platform_name, email_start="", diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 265035b03ab2..5339522c782f 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -622,7 +622,7 @@ def student_dashboard(request): if not user.is_active: activate_account_message = Text(_( "Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " - "If you need help, contact {link_start}{platform_name} Support{link_end}." + "If you need help, contact {platform_name} Support." )).format( platform_name=platform_name, email_start=HTML(""), From 1a24068b7e0995bff2da4fe299b77c08fa2a64bb Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Fri, 21 Feb 2020 14:40:29 -0800 Subject: [PATCH 110/119] Second Row of Google Doc target file to edit in bundle ./common/static/bundles/StudentAccountDeletionInitializer.js --- .../js/student_account/components/StudentAccountDeletion.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/js/student_account/components/StudentAccountDeletion.jsx b/lms/static/js/student_account/components/StudentAccountDeletion.jsx index 867acc123efa..1f553a774262 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletion.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletion.jsx @@ -67,7 +67,7 @@ export class StudentAccountDeletion extends React.Component { const changeAcctInfoText = StringUtils.interpolate( gettext('{htmlStart}Want to change your email, name, or password instead?{htmlEnd}'), { - htmlStart: '', + htmlStart: '', htmlEnd: '', }, ); From 799748699d36e08f39708a197218160b85618aa9 Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Fri, 21 Feb 2020 19:36:18 +0500 Subject: [PATCH 111/119] Replace certificate language in lms and core --- common/djangoapps/student/views/management.py | 2 +- common/lib/xmodule/xmodule/course_module.py | 24 +++++------ .../PortfolioExperimentUpsellModal.jsx | 2 +- lms/djangoapps/certificates/views/webview.py | 41 +++++++++---------- lms/djangoapps/courseware/date_summary.py | 20 ++++----- lms/djangoapps/courseware/views/views.py | 28 ++++++------- lms/djangoapps/instructor/views/api.py | 10 ++--- .../core/djangoapps/schedules/docs/README.rst | 11 +++-- .../edx_ace/upgradereminder/email/body.html | 20 ++++----- .../edx_ace/upgradereminder/email/body.txt | 8 ++-- .../edx_ace/upgradereminder/email/subject.txt | 4 +- .../course-sock-fragment.html | 20 ++++----- .../templates/share_modal.underscore | 4 +- 13 files changed, 96 insertions(+), 98 deletions(-) diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 28bed49746c8..2fbfb2ea2b68 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -425,7 +425,7 @@ def change_enrollment(request, check_access=True): certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: - return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course")) + return HttpResponseBadRequest(_("Your statement of accomplishment prevents you from unenrolling from this course")) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 6d4f81c0ea7b..19a1a77681be 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -580,34 +580,34 @@ class CourseFields(object): ## Course level Certificate Name overrides. cert_name_short = String( help=_( - 'Use this setting only when generating PDF certificates. ' - 'Between quotation marks, enter the short name of the type of certificate that ' - 'students receive when they complete the course. For instance, "Certificate".' + 'Use this setting only when generating PDF statements of accomplishment. ' + 'Between quotation marks, enter the short name of the type of statement of accomplishment that ' + 'students receive when they complete the course. For instance, "Statement of Accomplishment".' ), - display_name=_("Certificate Name (Short)"), + display_name=_("Statement of Accomplishment Name (Short)"), scope=Scope.settings, default="" ) cert_name_long = String( help=_( - 'Use this setting only when generating PDF certificates. ' - 'Between quotation marks, enter the long name of the type of certificate that students ' - 'receive when they complete the course. For instance, "Certificate of Achievement".' + 'Use this setting only when generating PDF statements of accomplishment. ' + 'Between quotation marks, enter the long name of the type of statement of accomplishment that students ' + 'receive when they complete the course. For instance, "Statement of Accomplishment".' ), - display_name=_("Certificate Name (Long)"), + display_name=_("Statement of Accomplishment Name (Long)"), scope=Scope.settings, default="" ) cert_html_view_enabled = Boolean( - display_name=_("Certificate Web/HTML View Enabled"), - help=_("If true, certificate Web/HTML views are enabled for the course."), + display_name=_("Statement of Accomplishment Web/HTML View Enabled"), + help=_("If true, statement of accomplishment Web/HTML views are enabled for the course."), scope=Scope.settings, default=True, deprecated=True ) cert_html_view_overrides = Dict( # Translators: This field is the container for course-specific certificate configuration values - display_name=_("Certificate Web/HTML View Overrides"), + display_name=_("Statement of Accomplishment Web/HTML View Overrides"), # Translators: These overrides allow for an alternative configuration of the certificate web view help=_("Enter course-specific overrides for the Web/HTML template parameters here (JSON format)"), scope=Scope.settings, @@ -616,7 +616,7 @@ class CourseFields(object): # Specific certificate information managed via Studio (should eventually fold other cert settings into this) certificates = Dict( # Translators: This field is the container for course-specific certificate configuration values - display_name=_("Certificate Configuration"), + display_name=_("Statement of Accomplishment Configuration"), # Translators: These overrides allow for an alternative configuration of the certificate web view help=_("Enter course-specific configuration information here (JSON format)"), scope=Scope.settings, diff --git a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx b/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx index 2b47d8fe393b..2d94903d6207 100644 --- a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx +++ b/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx @@ -44,7 +44,7 @@ export class PortfolioExperimentUpsellModal extends React.Component { Sample verified certificate
    ); diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 581b57d2e21f..a1481fcf178d 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -72,13 +72,14 @@ def get_certificate_description(mode, certificate_type, platform_name): elif mode == 'verified': # Translators: This text describes the 'ID Verified' course certificate type, which is a higher level of # verification offered by edX. This type of verification is useful for professional education/certifications - certificate_type_description = _("A {cert_type} certificate signifies that a " + # [UCSD_CUSTOM] EDS-104 Here I am removing {cert_type} as ucsd is only using verified certificates so we + # can replace it with statement of accomplishment + certificate_type_description = _("A statement of accomplishment signifies that a " "learner has agreed to abide by the honor code established by {platform_name} " "and has completed all of the required tasks for this course under its " - "guidelines. A {cert_type} certificate also indicates that the " + "guidelines. A statement of accomplishment also indicates that the " "identity of the learner has been checked and " - "is valid.").format(cert_type=certificate_type, - platform_name=platform_name) + "is valid.").format(platform_name=platform_name) elif mode == 'xseries': # Translators: This text describes the 'XSeries' course certificate type. An XSeries is a collection of # courses related to each other in a meaningful way, such as a specific topic or theme, or even an organization @@ -113,7 +114,7 @@ def _update_certificate_context(context, course, user_certificate, platform_name ) # Translators: This text represents the verification of the certificate - context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, ' + context['document_meta_description'] = _('This is a valid {platform_name} statement of accomplishment for {user_name}, ' 'who participated in {partner_short_name} {course_number}').format( platform_name=platform_name, user_name=context['accomplishment_copy_name'], @@ -122,7 +123,7 @@ def _update_certificate_context(context, course, user_certificate, platform_name ) # Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar - context['document_title'] = _("{partner_short_name} {course_number} Certificate | {platform_name}").format( + context['document_title'] = _("{partner_short_name} {course_number} Statement of Accomplishment | {platform_name}").format( partner_short_name=context['organization_short_name'], course_number=context['course_number'], platform_name=platform_name @@ -131,10 +132,8 @@ def _update_certificate_context(context, course, user_certificate, platform_name # Translators: This text fragment appears after the student's name (displayed in a large font) on the certificate # screen. The text describes the accomplishment represented by the certificate information displayed to the user context['accomplishment_copy_description_full'] = _("successfully completed, received a passing grade, and was " - "awarded this {platform_name} {certificate_type} " - "Certificate of Completion in ").format( - platform_name=platform_name, - certificate_type=context.get("certificate_type")) + "awarded this {platform_name} " + "Statement of Accomplishment in ").format(platform_name=platform_name) certificate_type_description = get_certificate_description(user_certificate.mode, certificate_type, platform_name) if certificate_type_description: @@ -142,7 +141,7 @@ def _update_certificate_context(context, course, user_certificate, platform_name # Translators: This text describes the purpose (and therefore, value) of a course certificate context['certificate_info_description'] = _("{platform_name} acknowledges achievements through " - "certificates, which are awarded for course activities " + "statements of accomplishment, which are awarded for course activities " "that {platform_name} students complete.").format( platform_name=platform_name, tos_url=context.get('company_tos_url'), @@ -170,7 +169,7 @@ def _update_context_with_basic_info(context, course_id, platform_name, configura # Translators: This text is bound to the HTML 'title' element of the page and appears # in the browser title bar when a requested certificate is not found or recognized - context['document_title'] = _("Invalid Certificate") + context['document_title'] = _("Invalid Statement of Accomplishment") context['company_tos_urltext'] = _("Terms of Service & Honor Code") @@ -178,7 +177,7 @@ def _update_context_with_basic_info(context, course_id, platform_name, configura context['company_privacy_urltext'] = _("Privacy Policy") # Translators: This line appears as a byline to a header image and describes the purpose of the page - context['logo_subtitle'] = _("Certificate Validation") + context['logo_subtitle'] = _("Statement of Accomplishment Validation") # Translators: Accomplishments describe the awards/certifications obtained by students on this platform context['accomplishment_copy_about'] = _('About {platform_name} Accomplishments').format( @@ -189,24 +188,24 @@ def _update_context_with_basic_info(context, course_id, platform_name, configura context['certificate_date_issued_title'] = _("Issued On:") # Translators: The Certificate ID Number is an alphanumeric value unique to each individual certificate - context['certificate_id_number_title'] = _('Certificate ID Number') + context['certificate_id_number_title'] = _('Statement of Accomplishment ID Number') - context['certificate_info_title'] = _('About {platform_name} Certificates').format( + context['certificate_info_title'] = _('About {platform_name} Statements of Accomplishment').format( platform_name=platform_name ) - context['certificate_verify_title'] = _("How {platform_name} Validates Student Certificates").format( + context['certificate_verify_title'] = _("How {platform_name} Validates Student Statements of Accomplishment").format( platform_name=platform_name ) # Translators: This text describes the validation mechanism for a certificate file (known as GPG security) - context['certificate_verify_description'] = _('Certificates issued by {platform_name} are signed by a gpg key so ' + context['certificate_verify_description'] = _('Statements of Accomplishment issued by {platform_name} are signed by a gpg key so ' 'that they can be validated independently by anyone with the ' '{platform_name} public key. For independent verification, ' '{platform_name} uses what is called a ' '"detached signature""".').format(platform_name=platform_name) - context['certificate_verify_urltext'] = _("Validate this certificate for yourself") + context['certificate_verify_urltext'] = _("Validate this statement of accomplishment for yourself") # Translators: This text describes (at a high level) the mission and charter the edX platform and organization context['company_about_description'] = _("{platform_name} offers interactive online classes and MOOCs.").format( @@ -271,7 +270,7 @@ def _update_social_context(request, context, course, user, user_certificate, pla context['twitter_share_enabled'] = share_settings.get('CERTIFICATE_TWITTER', False) context['twitter_share_text'] = share_settings.get( 'CERTIFICATE_TWITTER_TEXT', - _("I completed a course at {platform_name}. Take a look at my certificate.").format( + _("I completed a course at {platform_name}. Take a look at my statement of accomplishment.").format( platform_name=platform_name ) ) @@ -311,11 +310,11 @@ def _update_context_with_user_info(context, user, user_certificate): context['accomplishment_copy_name'] = user_fullname context['accomplishment_copy_username'] = user.username - context['accomplishment_more_title'] = _("More Information About {user_name}'s Certificate:").format( + context['accomplishment_more_title'] = _("More Information About {user_name}'s Statement of Accomplishment:").format( user_name=user_fullname ) # Translators: This line is displayed to a user who has completed a course and achieved a certification - context['accomplishment_banner_opening'] = _("{fullname}, you earned a certificate!").format( + context['accomplishment_banner_opening'] = _("{fullname}, you earned a statement of accomplishment!").format( fullname=user_fullname ) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 7af08377630e..4d7d7629f0d1 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -278,7 +278,7 @@ def description(self): if self.current_time <= self.date: mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) if is_active and CourseMode.is_eligible_for_certificate(mode): - return _('To earn a certificate, you must complete all requirements before this date.') + return _('To earn a statement of accomplishment, you must complete all requirements before this date.') else: return _('After this date, course content will be archived.') return _('This course is archived, which means you can review course content but it is no longer active.') @@ -321,7 +321,7 @@ class CertificateAvailableDate(DateSummary): Displays the certificate available date of the course. """ css_class = 'certificate-available-date' - title = ugettext_lazy('Certificate Available') + title = ugettext_lazy('Statement of Accomplishment Available') @property def active_certificates(self): @@ -342,7 +342,7 @@ def is_enabled(self): @property def description(self): - return _('Day certificates will become available for passing verified learners.') + return _('Day statements of accomplishment will become available for passing verified learners.') @property def date(self): @@ -367,8 +367,8 @@ def register_alerts(self, request, course): CourseHomeMessages.register_info_message( request, Text(_( - 'If you have earned a certificate, you will be able to access it {time_remaining_string}' - ' from now. You will also be able to view your certificates on your {learner_profile_link}.' + 'If you have earned a statement of accomplishment, you will be able to access it {time_remaining_string}' + ' from now. You will also be able to view your statements of accomplishment on your {learner_profile_link}.' )).format( time_remaining_string=self.time_remaining_string, learner_profile_link=HTML( @@ -378,7 +378,7 @@ def register_alerts(self, request, course): learner_profile_name=_('Learner Profile'), ), ), - title=Text(_('We are working on generating course certificates.')) + title=Text(_('We are working on generating course statements of accomplishment.')) ) @@ -515,16 +515,16 @@ def register_alerts(self, request, course): days_left_to_upgrade = (self.date - self.current_time).days if self.date > self.current_time and days_left_to_upgrade <= settings.COURSE_MESSAGE_ALERT_DURATION_IN_DAYS: upgrade_message = _( - "Don't forget, you have {time_remaining_string} left to upgrade to a Verified Certificate." + "Don't forget, you have {time_remaining_string} left to upgrade to a Statement of Accomplishment." ).format(time_remaining_string=self.time_remaining_string) if self._dynamic_deadline() is not None: upgrade_message = _( - "Don't forget to upgrade to a verified certificate by {localized_date}." + "Don't forget to upgrade to a statement of accomplishment by {localized_date}." ).format(localized_date=date_format(self.date)) CourseHomeMessages.register_info_message( request, Text(_( - 'In order to qualify for a certificate, you must meet all course grading ' + 'In order to qualify for a statement of accomplishment, you must meet all course grading ' 'requirements, upgrade before the course deadline, and successfully verify ' 'your identity on {platform_name} if you have not done so already.{button_panel}' )).format( @@ -595,7 +595,7 @@ def description(self): ) return _( "You must successfully complete verification before" - " this date to qualify for a Verified Certificate." + " this date to qualify for a Statement of Accomplishment." ) @lazy diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 923f180d959a..f1730cd06876 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -152,7 +152,7 @@ CertificateStatuses.generating, _("We're working on it..."), _( - "We're creating your certificate. You can keep working in your courses and a link " + "We're creating your statement of accomplishment. You can keep working in your courses and a link " "to it will appear here and on your Dashboard when it is ready." ), download_url=None, @@ -161,7 +161,7 @@ INVALID_CERT_DATA = CertData( CertificateStatuses.invalidated, - _('Your certificate has been invalidated'), + _('Your statement of accomplishment has been invalidated'), _('Please contact your course team if you have any questions.'), download_url=None, cert_web_view_url=None @@ -169,17 +169,17 @@ REQUESTING_CERT_DATA = CertData( CertificateStatuses.requesting, - _('Congratulations, you qualified for a certificate!'), - _("You've earned a certificate for this course."), + _('Congratulations, you qualified for a statement of accomplishment!'), + _("You've earned a statement of accomplishment for this course."), download_url=None, cert_web_view_url=None ) UNVERIFIED_CERT_DATA = CertData( CertificateStatuses.unverified, - _('Certificate unavailable'), + _('Statement of accomplishment unavailable'), _( - 'You have not received a certificate because you do not have a current {platform_name} ' + 'You have not received a statement of accomplishment because you do not have a current {platform_name} ' 'verified identity.' ).format(platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)), download_url=None, @@ -190,8 +190,8 @@ def _downloadable_cert_data(download_url=None, cert_web_view_url=None): return CertData( CertificateStatuses.downloadable, - _('Your certificate is available'), - _("You've earned a certificate for this course."), + _('Your statement of accomplishment is available'), + _("You've earned a statement of accomplishment for this course."), download_url=download_url, cert_web_view_url=cert_web_view_url ) @@ -1420,7 +1420,7 @@ def generate_user_cert(request, course_id): if not is_course_passed(student, course): log.info(u"User %s has not passed the course: %s", student.username, course_id) - return HttpResponseBadRequest(_("Your certificate will be available when you pass the course.")) + return HttpResponseBadRequest(_("Your statement of accomplishment will be available when you pass the course.")) certificate_status = certs_api.certificate_downloadable_status(student, course.id) @@ -1433,9 +1433,9 @@ def generate_user_cert(request, course_id): ) if certificate_status["is_downloadable"]: - return HttpResponseBadRequest(_("Certificate has already been created.")) + return HttpResponseBadRequest(_("Statement of accomplishment has already been created.")) elif certificate_status["is_generating"]: - return HttpResponseBadRequest(_("Certificate is being created.")) + return HttpResponseBadRequest(_("Statement of accomplishment is being created.")) else: # If the certificate is not already in-process or completed, # then create a new certificate generation task. @@ -1545,12 +1545,12 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'Tell us about your current financial situation. Why do you need assistance?' ) FA_GOALS_LABEL = _( - 'Tell us about your learning or professional goals. How will a Verified Certificate in' + 'Tell us about your learning or professional goals. How will a Statement of Accomplishment in' ' this course help you achieve these goals?' ) FA_EFFORT_LABEL = _( 'Tell us about your plans for this course. What steps will you take to help you complete' - ' the course work and receive a certificate?' + ' the course work and receive a statement of accomplishment?' ) FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 250 and 500 words or so in your response.') @@ -1665,7 +1665,7 @@ def financial_assistance_form(request): 'required': True, 'options': enrolled_courses, 'instructions': _( - 'Select the course for which you want to earn a verified certificate. If' + 'Select the course for which you want to earn a statement of accomplishment. If' ' the course does not appear in the list, make sure that you have enrolled' ' in the audit track for the course.' ) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 0f561da25f1f..384d6dc71e27 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1212,8 +1212,8 @@ def get_issued_certificates(request, course_id): query_features = ['course_id', 'mode', 'total_issued_certificate', 'report_run_date'] query_features_names = [ ('course_id', _('CourseID')), - ('mode', _('Certificate Type')), - ('total_issued_certificate', _('Total Certificates Issued')), + ('mode', _('Statement of Accomplishment Type')), + ('total_issued_certificate', _('Total Statements of Accomplishment Issued')), ('report_run_date', _('Date Report Run')) ] certificates_data = instructor_analytics.basic.issued_certificates(course_key, query_features) @@ -3021,7 +3021,7 @@ def start_certificate_generation(request, course_id): """ course_key = CourseKey.from_string(course_id) task = lms.djangoapps.instructor_task.api.generate_certificates_for_students(request, course_key) - message = _('Certificate generation task for all students of this course has been started. ' + message = _('Statement of accomplishment generation task for all students of this course has been started. ' 'You can view the status of the generation task in the "Pending Tasks" section.') response_payload = { 'message': message, @@ -3046,7 +3046,7 @@ def start_certificate_regeneration(request, course_id): certificates_statuses = request.POST.getlist('certificate_statuses', []) if not certificates_statuses: return JsonResponse( - {'message': _('Please select one or more certificate statuses that require certificate regeneration.')}, + {'message': _('Please select one or more statement of accomplishment statuses that require statement of accomplishment regeneration.')}, status=400 ) @@ -3066,7 +3066,7 @@ def start_certificate_regeneration(request, course_id): lms.djangoapps.instructor_task.api.regenerate_certificates(request, course_key, certificates_statuses) response_payload = { - 'message': _('Certificate regeneration task has been started. ' + 'message': _('Statement of accomplishment regeneration task has been started. ' 'You can view the status of the generation task in the "Pending Tasks" section.'), 'success': True } diff --git a/openedx/core/djangoapps/schedules/docs/README.rst b/openedx/core/djangoapps/schedules/docs/README.rst index 853ca0af7bda..5dc4a8e6bb2a 100644 --- a/openedx/core/djangoapps/schedules/docs/README.rst +++ b/openedx/core/djangoapps/schedules/docs/README.rst @@ -20,8 +20,7 @@ Recurring Nudges encourage learners to return to self-paced courses at regular intervals. The app sends nudges three days and ten days after a learner enrolls in a course. -Upgrade Reminders ask learners to purchase their course’s Verified -certificate. The reminders are sent two days before their course’s upgrade +Upgrade Reminders ask learners to purchase their course’s Statement of Accomplishment. The reminders are sent two days before their course’s upgrade deadline, or two days before the course’s end date (whichever date occurs sooner). @@ -53,7 +52,7 @@ Glossary - “Course Updates” - **Upgrade Deadline**: The date before which a learner is encouraged to - purchase a verified certificate. By default, a Schedule imposes a "soft" + purchase a statement of accomplishment. By default, a Schedule imposes a "soft" upgrade deadline (meaning, a suggested, but not final, date) 21 days from when a learner enrolled in a course. A self-paced course imposes a "hard" upgrade deadline that is the course-wide expiration date for upgrading on the @@ -463,10 +462,10 @@ To begin using Litmus, follow these steps: 1. Make sure that ACE is configured to use Sailthru (see instructions above). 2. Go to the `Litmus checklist page `__ and start a new checklist. -3. The checklist will provide you with an email address to which you will send +3. The checklist will provide you with an email address to which you will send a test email. -4. Send an email. Use one of the management commands with the - `--override-recipient-email` flag. Use the Litmus email you got in step 3 +4. Send an email. Use one of the management commands with the + `--override-recipient-email` flag. Use the Litmus email you got in step 3 as the flag value. :: diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html index 2b42a50de129..13d7e1b4df03 100644 --- a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html @@ -6,16 +6,16 @@ {% block preview_text %} {% if course_ids|length > 1 %} {% blocktrans trimmed %} - We hope you are enjoying learning with us so far on {{ platform_name }}! A verified certificate allows you to - highlight your new knowledge and skills. An {{ platform_name }} certificate is official and easily + We hope you are enjoying learning with us so far on {{ platform_name }}! A statement of accomplishment allows you to + highlight your new knowledge and skills. An {{ platform_name }} statement of accomplishment is official and easily shareable. Upgrade by {{ user_schedule_upgrade_deadline_time }}. {% endblocktrans %} {% else %} {% blocktrans trimmed %} - We hope you are enjoying learning with us so far in {{ first_course_name }}! A verified certificate allows - you to highlight your new knowledge and skills. An {{ platform_name }} certificate is official and easily + We hope you are enjoying learning with us so far in {{ first_course_name }}! A statement of accomplishment allows + you to highlight your new knowledge and skills. An {{ platform_name }} statement of accomplishment is official and easily shareable. Upgrade by {{ user_schedule_upgrade_deadline_time }}. @@ -33,14 +33,14 @@

    {% trans "Upgrade now" %}

    {% if course_ids|length > 1 %} {% blocktrans trimmed %} We hope you are enjoying learning with us so far on {{ platform_name }}! A - verified certificate allows you to highlight your new knowledge and skills. An - {{ platform_name }} certificate is official and easily shareable. + statement of accomplishment allows you to highlight your new knowledge and skills. An + {{ platform_name }} statement of accomplishment is official and easily shareable. {% endblocktrans %} {% else %} {% blocktrans trimmed %} We hope you are enjoying learning with us so far in {{ first_course_name }}! A - verified certificate allows you to highlight your new knowledge and skills. An - {{ platform_name }} certificate is official and easily shareable. + statement of accomplishment allows you to highlight your new knowledge and skills. An + {{ platform_name }} statement of accomplishment is official and easily shareable. {% endblocktrans %} {% endif %}

    @@ -65,7 +65,7 @@

    {% trans "Upgrade now" %}

    {% trans 'Example of a verified certificate' %} border-bottom: 3px solid lightgray; border-right: 3px solid lightgray; border-left: 1px solid lightgray; - " + " />

    diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt index 7c8b275a14ee..6d5006e09b05 100644 --- a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt @@ -3,8 +3,8 @@ {% load ace %} {% if course_ids|length > 1 %} {% blocktrans trimmed %} -We hope you are enjoying learning with us so far on {{ platform_name }}! A verified certificate -allows you to highlight your new knowledge and skills. An {{ platform_name }} certificate is +We hope you are enjoying learning with us so far on {{ platform_name }}! A statement of accomplishment +allows you to highlight your new knowledge and skills. An {{ platform_name }} statement of accomplishment is official and easily shareable. Upgrade by {{ user_schedule_upgrade_deadline_time }}. @@ -19,8 +19,8 @@ Upgrade by {{ user_schedule_upgrade_deadline_time }}. {% trans "Upgrade now at" %} <{% with_link_tracking dashboard_url %}> {% else %} {% blocktrans trimmed %} -We hope you are enjoying learning with us so far in {{ first_course_name }}! A verified certificate -allows you to highlight your new knowledge and skills. An {{ platform_name }} certificate is +We hope you are enjoying learning with us so far in {{ first_course_name }}! A statement of accomplishment +allows you to highlight your new knowledge and skills. An {{ platform_name }} statement of accomplishment is official and easily shareable. Upgrade by {{ user_schedule_upgrade_deadline_time }}. diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/subject.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/subject.txt index 215eda56ee9e..0b7b2762247a 100644 --- a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/subject.txt +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/subject.txt @@ -2,8 +2,8 @@ {% load i18n %} {% if course_ids|length > 1 %} -{% blocktrans %}Upgrade to earn a verified certificate on {{ platform_name }}{% endblocktrans %} +{% blocktrans %}Upgrade to earn a statement of accomplishment on {{ platform_name }}{% endblocktrans %} {% else %} -{% blocktrans %}Upgrade to earn a verified certificate in {{ first_course_name }}{% endblocktrans %} +{% blocktrans %}Upgrade to earn a statement of accomplishment in {{ first_course_name }}{% endblocktrans %} {% endif %} {% endautoescape %} diff --git a/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html b/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html index 932c703dba6f..6a88d56d46e8 100644 --- a/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html @@ -17,32 +17,32 @@ %endif >

    -

    ${_('{platform_name} Verified Certificate').format(platform_name=settings.PLATFORM_NAME)}

    +

    ${_('{platform_name} Statement of Accomplishment').format(platform_name=settings.PLATFORM_NAME)}

    ${_('Why upgrade?')}

    • ${_('Official proof of completion')}
    • -
    • ${_('Easily shareable certificate')}
    • +
    • ${_('Easily shareable statement of accomplishment')}
    • ${_('Proven motivator to complete the course')}
    • -
    • ${_('Certificate purchases help {platform_name} continue to offer free courses').format(platform_name=settings.PLATFORM_NAME)}
    • +
    • ${_('Statement of accomplishment purchases help {platform_name} continue to offer free courses').format(platform_name=settings.PLATFORM_NAME)}

    ${_('How it works')}

      -
    • ${_('Pay the Verified Certificate upgrade fee')}
    • +
    • ${_('Pay the Statement of Accomplishment upgrade fee')}
    • ${_('Verify your identity with a webcam and government-issued ID')}
    • ${_('Study hard and pass the course')}
    • -
    • ${_('Share your certificate with friends, employers, and others')}
    • +
    • ${_('Share your statement of accomplishment with friends, employers, and others')}
    % if settings.PLATFORM_NAME == 'edX':

    ${_('edX Learner Stories')}

    Student Image
    - ${_('My certificate has helped me showcase my knowledge on my \ - resume - I feel like this certificate could really help me land \ + ${_('My statement of accomplishment has helped me showcase my knowledge on my \ + resume - I feel like this statement of accomplishment could really help me land \ my dream job!')} - ${_('{learner_name}, edX Learner').format(learner_name='Christina Fong')}
    @@ -50,14 +50,14 @@

    ${_('edX Learner Stories')}

    Student Image
    - ${_('I wanted to include a verified certificate on my resume and my profile to \ + ${_('I wanted to include a statement of accomplishment on my resume and my profile to \ illustrate that I am working towards this goal I have and that I have \ achieved something while I was unemployed.')}
    - ${_('{learner_name}, edX Learner').format(learner_name='Cheryl Troell')}
    % endif - Example Certificate Image + Example Statement of Accomplishment Image
    ${Text(_('Upgrade ({course_price})')).format(course_price=HTML(course_price))} diff --git a/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore b/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore index 6034e3e8bf71..eef094867785 100644 --- a/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore +++ b/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore @@ -2,7 +2,7 @@

    <%- gettext("Share on Mozilla Backpack") %>

    -

    <%- gettext("To share your certificate on Mozilla Backpack, you must first have a Backpack account. Complete the following steps to add your certificate to Backpack.") %> +

    <%- gettext("To share your statement of accomplishment on Mozilla Backpack, you must first have a Backpack account. Complete the following steps to add your statement of accomplishment to Backpack.") %>

    @@ -27,4 +27,4 @@
    -
    \ No newline at end of file +
    From e459f3cd82cdb0ee927277a63b8b5b976d011444 Mon Sep 17 00:00:00 2001 From: tehreem-sadat Date: Fri, 21 Feb 2020 17:09:32 +0500 Subject: [PATCH 112/119] fix SuspeciousFileOperation error --- lms/djangoapps/certificates/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index e54501b2e0e7..62c58953b51c 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -469,6 +469,15 @@ def get_active_web_certificate(course, is_preview_mode=None): configurations = certificates.get('certificates', []) for config in configurations: if config.get('is_active') or is_preview_mode: + # [UCSD_CUSTOM] remove '/' from signatory image path + # "Preview certificate" error fix + signatories = config.get('signatories') + for signatory in signatories: + signatory_image_path = signatory.get('signature_image_path') + if signatory_image_path and signatory_image_path.startswith('/'): + signatory.update({ + 'signature_image_path': signatory_image_path[1:] + }) return config return None From 3e2a60823cd965c22c2dd8338c03e08760ae9be5 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 25 Feb 2020 08:39:55 -0800 Subject: [PATCH 113/119] Address errors update quotation marks --- common/djangoapps/student/tests/test_activate_account.py | 2 +- common/djangoapps/student/views/dashboard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/tests/test_activate_account.py b/common/djangoapps/student/tests/test_activate_account.py index 9be046c085de..4285039a0785 100644 --- a/common/djangoapps/student/tests/test_activate_account.py +++ b/common/djangoapps/student/tests/test_activate_account.py @@ -122,7 +122,7 @@ def test_account_activation_message(self): self.login() expected_message = ( u"Check your {email_start}{email}{email_end} inbox for an account activation link from " - u"{platform_name}. If you need help, contact
    {platform_name} Support." + u"{platform_name}. If you need help, contact {platform_name} Support." ).format( platform_name=self.platform_name, email_start="", diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 5339522c782f..97e2d5ed0234 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -622,7 +622,7 @@ def student_dashboard(request): if not user.is_active: activate_account_message = Text(_( "Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " - "If you need help, contact {platform_name} Support." + "If you need help, contact {platform_name} Support." )).format( platform_name=platform_name, email_start=HTML(""), From c5c6b89bd5eff629b855690a8ca91dbab28eb3a4 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Mon, 10 Feb 2020 17:57:27 +0500 Subject: [PATCH 114/119] Add geography discount on course enrollments --- common/djangoapps/course_modes/views.py | 6 + common/djangoapps/student/views/management.py | 44 ++++- lms/envs/common.py | 159 ++++++++++++++++++ lms/envs/production.py | 3 + lms/envs/test.py | 1 + .../ecommerce/EcommerceClient.py | 44 +++++ .../ucsd_features/ecommerce/__init__.py | 0 .../ucsd_features/ecommerce/constants.py | 1 + .../features/ucsd_features/ecommerce/tasks.py | 36 ++++ .../ucsd_features/ecommerce/tests/__init__.py | 0 .../ecommerce/tests/test_ecommerce_client.py | 58 +++++++ .../ecommerce/tests/test_tasks.py | 67 ++++++++ .../ecommerce/tests/test_utils.py | 30 ++++ .../ucsd_features/ecommerce/tests/utils.py | 12 ++ .../features/ucsd_features/ecommerce/utils.py | 25 +++ 15 files changed, 482 insertions(+), 4 deletions(-) create mode 100644 openedx/features/ucsd_features/ecommerce/EcommerceClient.py create mode 100644 openedx/features/ucsd_features/ecommerce/__init__.py create mode 100644 openedx/features/ucsd_features/ecommerce/constants.py create mode 100644 openedx/features/ucsd_features/ecommerce/tasks.py create mode 100644 openedx/features/ucsd_features/ecommerce/tests/__init__.py create mode 100644 openedx/features/ucsd_features/ecommerce/tests/test_ecommerce_client.py create mode 100644 openedx/features/ucsd_features/ecommerce/tests/test_tasks.py create mode 100644 openedx/features/ucsd_features/ecommerce/tests/test_utils.py create mode 100644 openedx/features/ucsd_features/ecommerce/tests/utils.py create mode 100644 openedx/features/ucsd_features/ecommerce/utils.py diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 655f1378d2ed..a8d0d8c7bf9d 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -8,6 +8,7 @@ import waffle from babel.dates import format_datetime +from edx_rest_api_client import exceptions from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import HttpResponse, HttpResponseBadRequest @@ -32,6 +33,9 @@ from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig +from openedx.features.ucsd_features.ecommerce.utils import is_user_eligible_for_discount +from openedx.features.ucsd_features.ecommerce.EcommerceClient import EcommerceRestAPIClient +from openedx.features.ucsd_features.ecommerce.constants import IS_DISCOUNT_AVAILABLE_QUERY_PARAM from student.models import CourseEnrollment from util.db import outer_atomic from xmodule.modulestore.django import modulestore @@ -228,6 +232,8 @@ def get(self, request, course_id, error=None): context["ecommerce_payment_page"] = ecommerce_service.payment_page_url() context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku + if request.GET.get(IS_DISCOUNT_AVAILABLE_QUERY_PARAM): + context['is_request_for_voucher_sent'] = True context['currency_data'] = [] if waffle.switch_is_active('local_currency'): diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 2fbfb2ea2b68..3bcea5d290f5 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -62,6 +62,10 @@ from openedx.core.djangoapps.user_api.errors import UserNotFound, UserAPIInternalError from openedx.core.djangoapps.user_api.models import UserRetirementRequest from openedx.core.djangoapps.user_api.preferences import api as preferences_api +from openedx.features.ucsd_features.ecommerce.utils import is_user_eligible_for_discount +from openedx.features.ucsd_features.ecommerce.EcommerceClient import EcommerceRestAPIClient +from openedx.features.ucsd_features.ecommerce.tasks import assign_course_voucher_to_user +from openedx.features.ucsd_features.ecommerce.constants import IS_DISCOUNT_AVAILABLE_QUERY_PARAM from openedx.core.djangolib.markup import HTML, Text from openedx.features.journals.api import get_journals_context @@ -122,7 +126,7 @@ def csrf_token(context): if token == 'NOTPROVIDED': return '' return (HTML(u'
    ').format(token)) + ' name="csrfmiddlewaretoken" value="{}" />
    ').format(token)) # NOTE: This view is not linked to directly--it is called from @@ -412,9 +416,41 @@ def change_enrollment(request, check_access=True): # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes): - return HttpResponse( - reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)}) - ) + # [UCSD_CUSTOM] Enable geographic country based discounts on course enrollments + redirect_url = reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)}) + ecommerce_client = EcommerceRestAPIClient(user=request.user) + course_key = str(course_id) + if is_user_eligible_for_discount(request, course_key): + log.info('user {username} is eligible for geographic discount on the course {course_key}.' + 'Starting the task to send a request to ecommerce to email user about coupon ' + 'codes.'.format( + username=user.username, + course_key=course_id + )) + redirect_url = '{}?{}=True'.format(redirect_url, IS_DISCOUNT_AVAILABLE_QUERY_PARAM) + + try: + course_sku = CourseMode.objects.get(course=course_id, mode_slug='verified').sku + except (AttributeError, CourseMode.DoesNotExist): + course_sku = None + + try: + assign_course_voucher_to_user.delay(request.user.email, course_key, course_sku) + log.info('Successfully scheduled a task to assign a voucher to ' + 'user {username} for the course {course_key}.'.format( + username=user.username, + course_key=course_key + )) + except Exception as ex: # pylint: disable=broad-except + log.exception('Failed to schedule a task to assign a voucher to ' + 'user {username} for the course {course_key}.' + '\nError message: {error}'.format( + username=user.username, + course_key=course_key, + error=ex.message + )) + + return HttpResponse(redirect_url) # Otherwise, there is only one mode available (the default) return HttpResponse() diff --git a/lms/envs/common.py b/lms/envs/common.py index e1a943561944..1373b21243cc 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3490,3 +3490,162 @@ def _make_locale_paths(settings): FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = True FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = True +FEATURES['ENABLE_GEOGRAPHIC_DISCOUNTS'] = True + +COUNTRIES_ELIGIBLE_FOR_DISCOUNTS = { + 'AF': 'Afghanistan', + 'AL': 'Albania', + 'DZ': 'Algeria', + 'AO': 'Angola', + 'AG': 'Antigua and Barbuda', + 'AR': 'Argentina', + 'AM': 'Armenia', + 'AW': 'Aruba', + 'AZ': 'Azerbaijan', + 'BS': 'Bahamaz', + 'BH': 'Bahrain', + 'BD': 'Bangladesh', + 'BB': 'Barbados', + 'BY': 'Belarus', + 'BZ': 'Belize', + 'BJ': 'Benin', + 'BT': 'Bhutan', + 'BO': 'Bolivia', + 'BA': 'Bosnia and Herzegovina', + 'BW': 'Botswana', + 'BR': 'Brazil', + 'BN': 'Brunei Darussalam', + 'BG': 'Bulgaria', + 'BF': 'Burkina Faso', + 'BI': 'Burundi', + 'CV': 'Cabo Verde', + 'KH': 'Cambodia', + 'CM': 'Cameroon', + 'CF': 'Central African Republic', + 'TD': 'Chad', + 'CL': 'Chile', + 'CN': 'China', + 'CO': 'Colombia', + 'KM': 'Comoros', + 'CD': 'Democratic Republic of the Congo', + 'CD': 'Republic of Congo', + 'CR': 'Costa Rica', + 'CI': 'Côte d\'Ivoire', + 'HR': 'Croatia', + 'DJ': 'Djibouti', + 'DM': 'Dominica', + 'DO': 'Dominican Republic', + 'EC': 'Ecuador', + 'EG': 'Egypt', + 'SV': 'El Salvador', + 'GQ': 'Equatorial Guinea', + 'ER': 'Eritrea', + 'SZ': 'Eswatini', + 'ET': 'Ethiopia', + 'FJ': 'Fiji', + 'GA': 'Gabon', + 'GM': 'Gambia', + 'GE': 'Georgia', + 'GH': 'Ghana', + 'GD': 'Grenada', + 'GT': 'Guatemala', + 'GN': 'Guinea', + 'GW': 'Guinea-Bissau', + 'GY': 'Guyana', + 'HT': 'Haiti', + 'HN': 'Honduras', + 'HU': 'Hungary', + 'IN': 'India', + 'ID': 'Indonesia', + 'IR': 'Iran', + 'IQ': 'Iraq', + 'JM': 'Jamaica', + 'JO': 'Jordan', + 'KZ': 'Kazakhstan', + 'KE': 'Kenya', + 'KI': 'Kiribati', + 'RS': 'Kosovo', + 'KW': 'Kuwait', + 'KG': 'Kyrgyzstan', + 'LA': 'Lao People\'s Democratic Republic', + 'LB': 'Lebanon', + 'LS': 'Lesotho', + 'LR': 'Liberia', + 'LY': 'Libya', + 'MK': 'Macedonia', + 'MG': 'Madagascar', + 'MW': 'Malawi', + 'MY': 'Malaysia', + 'MV': 'Maldives', + 'ML': 'Mali', + 'MH': 'Marshall Islands', + 'MR': 'Mauritania', + 'MU': 'Mauritius', + 'MX': 'Mexico', + 'FM': 'Micronesia', + 'MD': 'Moldova', + 'MN': 'Mongolia', + 'ME': 'Montenegro', + 'MA': 'Morocco', + 'MZ': 'Mozambique', + 'MM': 'Myanmar', + 'NA': 'Namibia', + 'NR': 'Nauru', + 'NP': 'Nepal', + 'NI': 'Nicaragua', + 'NE': 'Niger', + 'NG': 'Nigeria', + 'OM': 'Oman', + 'PK': 'Pakistan', + 'PW': 'Palau', + 'PA': 'Panama', + 'PG': 'Papua New Guinea', + 'PY': 'Paraguay', + 'PE': 'Peru', + 'PH': 'Philippines', + 'PL': 'Poland', + 'QA': 'Qatar', + 'RO': 'Romania', + 'RU': 'Russian Federation', + 'RW': 'Rwanda', + 'WS': 'Samoa', + 'ST': 'São Tomé and Príncipe', + 'SA': 'Saudi Arabia', + 'SN': 'Senegal', + 'RS': 'Serbia', + 'SC': 'Seychelles', + 'SL': 'Sierra Leone', + 'SB': 'Solomon Islands', + 'SO': 'Somalia', + 'ZA': 'South Africa', + 'SS': 'South Sudan', + 'LK': 'Sri Lanka', + 'KN': 'Saint Kitts and Nevis', + 'LC': 'Saint Lucia', + 'VC': 'Saint Vincent and the Grenadines', + 'SD': 'Sudan', + 'SR': 'Suriname', + 'SY': 'Syrian Arab Republic', + 'TJ': 'Tajikistan', + 'TZ': 'Tanzania', + 'TH': 'Thailand', + 'TL': 'Timor-Leste', + 'TG': 'Togo', + 'TO': 'Tonga', + 'TT': 'Trinidad and Tobago', + 'TN': 'Tunisia', + 'TR': 'Turkey', + 'TM': 'Turkmenistan', + 'TV': 'Tuvalu', + 'UG': 'Uganda', + 'UA': 'Ukraine', + 'AE': 'United Arab Emirates', + 'UY': 'Uruguay', + 'UZ': 'Uzbekistan', + 'VU': 'Vanuatu', + 'VE': 'Venezuela', + 'VN': 'Viet nam', + 'YE': 'Yemen', + 'ZM': 'Zambia', + 'ZW': 'Zimbabwe' +} diff --git a/lms/envs/production.py b/lms/envs/production.py index c5848a54d6b9..fb72e34ea146 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1168,3 +1168,6 @@ SUPPORT_DESK_EMAILS = ENV_TOKENS.get('SUPPORT_DESK_EMAILS') INSTALLED_APPS.append('openedx.features.ucsd_features') + +if ENV_TOKENS.get('COUNTRIES_ELIGIBLE_FOR_DISCOUNTS'): + COUNTRIES_ELIGIBLE_FOR_DISCOUNTS = ENV_TOKENS.get('COUNTRIES_ELIGIBLE_FOR_DISCOUNTS') diff --git a/lms/envs/test.py b/lms/envs/test.py index 36308f5551d7..4127dff22749 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -628,3 +628,4 @@ FEATURES['ENABLE_EMAIL_INSTEAD_ZENDESK'] = False FEATURES['DISABLE_REFUND_FAILURE_NOTIFICATION'] = False FEATURES['AUTOMATIC_PERMANENT_ACCOUNT_VERIFICATION'] = False +FEATURES['ENABLE_GEOGRAPHIC_DISCOUNTS'] = False diff --git a/openedx/features/ucsd_features/ecommerce/EcommerceClient.py b/openedx/features/ucsd_features/ecommerce/EcommerceClient.py new file mode 100644 index 000000000000..e3346eea6733 --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/EcommerceClient.py @@ -0,0 +1,44 @@ +""" +Custom client to communicate with ecommerce service +""" +import logging + +from openedx.core.djangoapps.commerce.utils import ecommerce_api_client + + +logger = logging.getLogger(__name__) + + +class EcommerceRestAPIClient: + def __init__(self, user, session=None): + self.client = ecommerce_api_client(user, session) + + def assign_voucher_to_user(self, user, course_key, course_sku=None): + try: + self.client.resource('/ucsd/api/v1/assign_voucher').post( + { + 'username': user.username, + 'user_email': user.email, + 'course_key': str(course_key), + 'course_sku': course_sku + }) + return True, '' + except Exception as ex: + logger.exception('Got failure response from ecommerce while ' + 'trying to assign a voucher to user.\n' + 'Details:{}'.format(ex.message)) + return False, ex.message + + def check_coupon_availability_for_course(self, course_key): + try: + self.client.resource('/ucsd/api/v1/check_course_coupon').post( + { + 'course_key': course_key + } + ) + return True + except Exception as ex: + logger.exception('Got failure response from ecommerce while ' + 'trying to check coupon availability for the course.\n' + 'Details:{}'.format(ex.message)) + return False diff --git a/openedx/features/ucsd_features/ecommerce/__init__.py b/openedx/features/ucsd_features/ecommerce/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/ecommerce/constants.py b/openedx/features/ucsd_features/ecommerce/constants.py new file mode 100644 index 000000000000..9043d3363867 --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/constants.py @@ -0,0 +1 @@ +IS_DISCOUNT_AVAILABLE_QUERY_PARAM = 'is_discount_available' diff --git a/openedx/features/ucsd_features/ecommerce/tasks.py b/openedx/features/ucsd_features/ecommerce/tasks.py new file mode 100644 index 000000000000..7766c8c76612 --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/tasks.py @@ -0,0 +1,36 @@ +from celery.task import task +from celery.utils.log import get_task_logger +from django.contrib.auth.models import User + +from openedx.features.ucsd_features.ecommerce.EcommerceClient import EcommerceRestAPIClient + + +logger = get_task_logger(__name__) + + +@task() +def assign_course_voucher_to_user(user_email, course_key, course_sku): + try: + user = User.objects.get(email=user_email) + except User.DoesNotExist: + logger.error('User with email: {} not found. Cannot assign a voucher.'.format(user_email)) + return + + ecommerce_client = EcommerceRestAPIClient(user=user) + is_voucher_assigned, message = ecommerce_client.assign_voucher_to_user( + user=user, course_key=course_key, course_sku=course_sku + ) + if is_voucher_assigned: + logger.info('Successfully assigned a voucher to ' + 'user {username} for the course {course_key}.'.format( + username=user.username, + course_key=course_key, + )) + else: + logger.error('Failed to send request to assign a voucher to ' + 'user {username} for the course {course_key}.' + '\nError message: {message}'.format( + username=user.username, + course_key=course_key, + message=message + )) diff --git a/openedx/features/ucsd_features/ecommerce/tests/__init__.py b/openedx/features/ucsd_features/ecommerce/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/ucsd_features/ecommerce/tests/test_ecommerce_client.py b/openedx/features/ucsd_features/ecommerce/tests/test_ecommerce_client.py new file mode 100644 index 000000000000..45d86498837a --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/tests/test_ecommerce_client.py @@ -0,0 +1,58 @@ +""" +Test cases for celery tasks +""" +import httpretty + +from django.conf import settings +from mock import patch + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory + +from openedx.features.ucsd_features.ecommerce.EcommerceClient import EcommerceRestAPIClient +from openedx.features.ucsd_features.ecommerce.tests.utils import make_ecommerce_url + + +class UCSDFeaturesEcommerceClientTests(ModuleStoreTestCase): + + def setUp(self): + super(UCSDFeaturesEcommerceClientTests, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory() + self.client = EcommerceRestAPIClient(self.user) + + @httpretty.activate + def test_assign_voucher_to_user_with_success_response(self): + url = make_ecommerce_url('/ucsd/api/v1/assign_voucher/') + course_key = str(self.course.id) + + httpretty.register_uri( + httpretty.POST, + url, + status=200, + body='{}', + content_type='application/json' + ) + is_successfull, message = self.client.assign_voucher_to_user(self.user, course_key) + self.assertEqual(is_successfull, True) + self.assertEqual(message, '') + + @httpretty.activate + @patch('openedx.features.ucsd_features.ecommerce.EcommerceClient.logger.error', autospec=True) + def test_assign_voucher_to_user_with_failure_response(self, mocked_logger): + url = make_ecommerce_url('/ucsd/api/v1/assign_voucher/') + course_key = str(self.course.id) + + httpretty.register_uri( + httpretty.POST, + url, + status=400, + body='{}', + content_type='application/json' + ) + + is_successfull, message = self.client.assign_voucher_to_user(self.user, course_key) + expected_message = 'Client Error 400: {}'.format(url) + self.assertEqual(is_successfull, False) + self.assertEqual(message, expected_message) diff --git a/openedx/features/ucsd_features/ecommerce/tests/test_tasks.py b/openedx/features/ucsd_features/ecommerce/tests/test_tasks.py new file mode 100644 index 000000000000..57ab6ca27bd4 --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/tests/test_tasks.py @@ -0,0 +1,67 @@ +""" +Test cases for celery tasks +""" +import httpretty + +from django.conf import settings +from mock import patch + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory + +from openedx.features.ucsd_features.ecommerce.tasks import assign_course_voucher_to_user +from openedx.features.ucsd_features.ecommerce.tests.utils import make_ecommerce_url + + +class UCSDFeaturesEcommerceTasksTests(ModuleStoreTestCase): + + def setUp(self): + super(UCSDFeaturesEcommerceTasksTests, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory() + + @patch('openedx.features.ucsd_features.ecommerce.tasks.logger.error', autospec=True) + def test_assign_course_voucher_to_user_when_no_user_exists(self, mocked_logger): + invalid_user_email = 'doesnotexsts@mail.com' + course_key = str(self.course.id) + assign_course_voucher_to_user(invalid_user_email, course_key) + mocked_logger.assert_called_once_with('User with email: doesnotexsts@mail.com not found. Cannot assign a voucher.') + + @httpretty.activate + @patch('openedx.features.ucsd_features.ecommerce.tasks.logger.info', autospec=True) + def test_assign_course_voucher_to_user_with_successful_assignment(self, mocked_logger): + url = make_ecommerce_url('/ucsd/api/v1/assign_voucher/') + httpretty.register_uri( + httpretty.POST, + url, + status=200, + body='{}', + content_type='application/json' + ) + course_key = str(self.course.id) + assign_course_voucher_to_user(self.user.email, course_key) + expected_log_message = 'Successfully assigned a voucher to user {} for the course {}.'.format(self.user.username, course_key) + mocked_logger.assert_called_once_with(expected_log_message) + + @httpretty.activate + @patch('openedx.features.ucsd_features.ecommerce.tasks.logger.error', autospec=True) + def test_assign_course_voucher_to_user_with_failed_assignment(self, mocked_logger): + url = make_ecommerce_url('/ucsd/api/v1/assign_voucher/') + httpretty.register_uri( + httpretty.POST, + url, + status=400, + body='{}', + content_type='application/json' + ) + course_key = str(self.course.id) + assign_course_voucher_to_user(self.user.email, course_key) + expected_log_message = ('Failed to send request to assign a voucher to user' + ' {} for the course {}.\nError message: ' + 'Client Error 400: {}'.format( + self.user.username, + course_key, + url + )) + mocked_logger.assert_called_once_with(expected_log_message) diff --git a/openedx/features/ucsd_features/ecommerce/tests/test_utils.py b/openedx/features/ucsd_features/ecommerce/tests/test_utils.py new file mode 100644 index 000000000000..4c1eec39b0e0 --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/tests/test_utils.py @@ -0,0 +1,30 @@ +""" +Tests for ecommerce utils +""" +import httpretty + +from django.conf import settings +from mock import patch + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory + +from openedx.features.ucsd_features.ecommerce.utils import is_user_eligible_for_discount + + +class UCSDFeaturesEcommerceUtilsTests(ModuleStoreTestCase): + + def setUpClass(self): + super(UCSDFeaturesEcommerceUtilsTests, self).setUpClass() + self.course = CourseFactory.create() + + def setUp(self): + super(UCSDFeaturesEcommerceUtilsTests, self).setUp() + self.user = UserFactory() + + def test_is_user_eligible_for_discount_with_disabled_feature(self): + request = mock.MagicMock() + course_key = str(self.course.id) + return_value = is_user_eligible_for_discount(request, course_key) + self.assertFalse(return_value) diff --git a/openedx/features/ucsd_features/ecommerce/tests/utils.py b/openedx/features/ucsd_features/ecommerce/tests/utils.py new file mode 100644 index 000000000000..000c415ffc8a --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/tests/utils.py @@ -0,0 +1,12 @@ +""" +Utils for tests related to ecommerce +""" +from django.conf import settings + + +def get_ecommerce_host_url(): + return settings.ECOMMERCE_API_URL.strip('/api/v2/') + + +def make_ecommerce_url(url): + return '{}{}'.format(get_ecommerce_host_url(), url) diff --git a/openedx/features/ucsd_features/ecommerce/utils.py b/openedx/features/ucsd_features/ecommerce/utils.py new file mode 100644 index 000000000000..5a72c6efd7fc --- /dev/null +++ b/openedx/features/ucsd_features/ecommerce/utils.py @@ -0,0 +1,25 @@ +""" +Util methods related to Open edX e-commerce service +""" +import logging + +from django.conf import settings + +from openedx.features.ucsd_features.ecommerce.EcommerceClient import EcommerceRestAPIClient + + +logger = logging.getLogger(__name__) + + +def is_user_eligible_for_discount(request, course_key): + if not settings.FEATURES.get('ENABLE_GEOGRAPHIC_DISCOUNTS', False): + logger.info('Geographics discounts are not enabled hence skipping further processing.') + return False + + country_code = request.session.get('country_code', None) + ecommerce_client = EcommerceRestAPIClient(user=request.user) + + if country_code not in settings.COUNTRIES_ELIGIBLE_FOR_DISCOUNTS: + return False + + return ecommerce_client.check_coupon_availability_for_course(course_key) From c3678a8ba547d1270d356bba6ba22cb4b6657e4a Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Wed, 26 Feb 2020 18:10:38 +0500 Subject: [PATCH 115/119] Remove verified word --- lms/static/js/dashboard/legacy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index e475cefdd25f..fb0872a2da49 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -103,11 +103,11 @@ diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from %(courseName)s ' + '(%(courseNumber)s)?'); } else if (showRefundOption) { - diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' + + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the ' + '%(certNameLong)s track of %(courseName)s (%(courseNumber)s)?'); diagAttr['data-refund-info'] = gettext('You will be refunded the amount you paid.'); } else { - diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' + + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the ' + '%(certNameLong)s track of %(courseName)s (%(courseNumber)s)?'); diagAttr['data-refund-info'] = gettext('The refund deadline for this course has passed,' + 'so you will not receive a refund.'); From 7d8004d57d0b2f8ee0cd0052425c23893d986ca7 Mon Sep 17 00:00:00 2001 From: mdandrad <55721889+mdandrad@users.noreply.github.com> Date: Tue, 3 Mar 2020 13:40:56 -0800 Subject: [PATCH 116/119] Broken Link update link --- .../js/student_account/components/StudentAccountDeletion.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/js/student_account/components/StudentAccountDeletion.jsx b/lms/static/js/student_account/components/StudentAccountDeletion.jsx index 1f553a774262..216ff226f1ad 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletion.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletion.jsx @@ -67,7 +67,7 @@ export class StudentAccountDeletion extends React.Component { const changeAcctInfoText = StringUtils.interpolate( gettext('{htmlStart}Want to change your email, name, or password instead?{htmlEnd}'), { - htmlStart: '', + htmlStart: '', htmlEnd: '', }, ); From 614266c47beb0817ce62cbf41ed9fba666156c1e Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 10 Mar 2020 15:46:59 -0700 Subject: [PATCH 117/119] Bug Fix Link element is being displayed text vs link --- common/djangoapps/student/views/dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 97e2d5ed0234..13b3badf9b58 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -619,10 +619,11 @@ def student_dashboard(request): # Display activation message activate_account_message = '' + activation_email_support_link = 'http://edtech.ucsd.edu/uc-san-diego-online-help' if not user.is_active: activate_account_message = Text(_( "Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " - "If you need help, contact {platform_name} Support." + "If you need help, contact {link_start}{platform_name} Support{link_end}." )).format( platform_name=platform_name, email_start=HTML(""), From f236f49854fca76333bb3b73a201f5e587b8a4b3 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 10 Mar 2020 15:51:24 -0700 Subject: [PATCH 118/119] content update Update sentence --- common/djangoapps/student/views/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 13b3badf9b58..6fb1216f6d19 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -623,7 +623,7 @@ def student_dashboard(request): if not user.is_active: activate_account_message = Text(_( "Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " - "If you need help, contact {link_start}{platform_name} Support{link_end}." + "If you need help, visit the {link_start}{platform_name} Center{link_end}." )).format( platform_name=platform_name, email_start=HTML(""), From e165960f3d49cfbb59cfe01ea7e50cca2dc7e142 Mon Sep 17 00:00:00 2001 From: Maria Andrade Date: Tue, 10 Mar 2020 16:13:45 -0700 Subject: [PATCH 119/119] missing text Added "Help" --- common/djangoapps/student/views/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 6fb1216f6d19..ef36de537878 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -623,7 +623,7 @@ def student_dashboard(request): if not user.is_active: activate_account_message = Text(_( "Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " - "If you need help, visit the {link_start}{platform_name} Center{link_end}." + "If you need help, visit the {link_start}{platform_name} Help Center{link_end}." )).format( platform_name=platform_name, email_start=HTML(""),

    {% if course %} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt index e511d032a1da..c3dda3b09725 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/body.txt @@ -2,7 +2,7 @@ {% block content %} Hello, - We were contacted by a user on LearnX @ UC San Diego. Please take a moment to review and respond. + We were contacted by a user on UC San Diego Online. Please take a moment to review and respond. Learner: {{name}} ({{email}}) {% if course %} Course: {{course}} diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt index 8406c1f2146e..4bd4b979fc81 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/contactsupportnotification/email/subject.txt @@ -1,3 +1,3 @@ {% load i18n %} -{% blocktrans %}[LearnX] Support / Information Request {{name}}{% endblocktrans %} {% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} +{% blocktrans %}[UC San Diego Online] Support / Information Request {{name}}{% endblocktrans %} {% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} From 60ef2c34feb6c9f42bc9679e1e700a495029769d Mon Sep 17 00:00:00 2001 From: danialmalik Date: Fri, 17 Jan 2020 12:14:31 +0500 Subject: [PATCH 081/119] update subject for email --- .../edx_ace/commercesupportnotification/email/subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt index 36d7b9e3168b..0447ffd7d114 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt @@ -1,3 +1,3 @@ {% load i18n %} -{% blocktrans %}[UC San Diego Online] Administrative Action Needed {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} +{% blocktrans %}[UC San Diego Online] OpenEdX Administrative Action Needed for {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} From 3247a74cde8425b536a4496363f9a22417753ef3 Mon Sep 17 00:00:00 2001 From: imhassantariq Date: Tue, 21 Jan 2020 14:08:44 +0500 Subject: [PATCH 082/119] Default Course Image --- cms/envs/production.py | 1 + openedx/features/ucsd_features/signals.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/cms/envs/production.py b/cms/envs/production.py index ffc356c139c3..47852715fd2c 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -635,3 +635,4 @@ ########################## Derive Any Derived Settings ####################### derive_settings(__name__) +INSTALLED_APPS.append('openedx.features.ucsd_features') diff --git a/openedx/features/ucsd_features/signals.py b/openedx/features/ucsd_features/signals.py index 0fc5083db574..2b36f21b82a4 100644 --- a/openedx/features/ucsd_features/signals.py +++ b/openedx/features/ucsd_features/signals.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_save from lms.djangoapps.verify_student.models import ManualVerification +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from student.models import UserProfile @@ -29,3 +30,14 @@ def generate_manual_verification_for_user(sender, instance, created, **kwargs): ) except Exception: # pylint: disable=broad-except logger.error('Error while generating ManualVerification for user: %s', instance.user.email, exc_info=True) + + +@receiver(post_save, sender=CourseOverview) +def course_image_change(sender, instance, created, **kwargs): + """ + Change the default course image whenever new course is created + """ + if created: + if instance.course_image_url.endswith('images_course_image.jpg'): + instance.course_image_url = "/static/" + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL + instance.save() From a96b1b14eb332ca4fd186f1ccad709d72f24ea56 Mon Sep 17 00:00:00 2001 From: imhassantariq Date: Tue, 21 Jan 2020 14:08:44 +0500 Subject: [PATCH 083/119] Default Course Image --- openedx/features/ucsd_features/signals.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openedx/features/ucsd_features/signals.py b/openedx/features/ucsd_features/signals.py index 2b36f21b82a4..16739b29c04b 100644 --- a/openedx/features/ucsd_features/signals.py +++ b/openedx/features/ucsd_features/signals.py @@ -2,7 +2,7 @@ from django.conf import settings from django.dispatch import receiver -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_save from lms.djangoapps.verify_student.models import ManualVerification from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -32,12 +32,10 @@ def generate_manual_verification_for_user(sender, instance, created, **kwargs): logger.error('Error while generating ManualVerification for user: %s', instance.user.email, exc_info=True) -@receiver(post_save, sender=CourseOverview) -def course_image_change(sender, instance, created, **kwargs): +@receiver(pre_save, sender=CourseOverview) +def course_image_change(sender, instance, **kwargs): """ Change the default course image whenever new course is created """ - if created: - if instance.course_image_url.endswith('images_course_image.jpg'): - instance.course_image_url = "/static/" + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL - instance.save() + if instance.course_image_url.endswith('images_course_image.jpg'): + instance.course_image_url = "/static/" + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL From b74449c2b7970e7e9be6b424a52e27fe97d31465 Mon Sep 17 00:00:00 2001 From: Husnain Raza Ghaffar Date: Wed, 22 Jan 2020 12:52:14 +0500 Subject: [PATCH 084/119] Updated Test Eng scripts according to UCSD env --- scripts/Jenkinsfiles/bokchoy | 10 +++++----- scripts/Jenkinsfiles/lettuce | 10 +++++----- scripts/Jenkinsfiles/python | 12 ++++++------ scripts/Jenkinsfiles/quality | 12 ++++++------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/scripts/Jenkinsfiles/bokchoy b/scripts/Jenkinsfiles/bokchoy index 2d92adabd0a2..95c290fa5597 100644 --- a/scripts/Jenkinsfiles/bokchoy +++ b/scripts/Jenkinsfiles/bokchoy @@ -12,7 +12,7 @@ def runBokchoyTests() { checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, noTags: true, shallow: true]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: git_refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] + refspec: git_refspec, url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] console_output = sh(returnStdout: true, script: 'bash scripts/all-tests.sh').trim() dir('stdout') { writeFile file: "${TEST_SUITE}-${SHARD}-stdout.log", text: console_output @@ -43,7 +43,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), @@ -103,7 +103,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), @@ -127,12 +127,12 @@ pipeline { } } emailext body: email_body, - subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { slackSend botUser: true, message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", - subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } } } diff --git a/scripts/Jenkinsfiles/lettuce b/scripts/Jenkinsfiles/lettuce index 852fee6ca34d..f3375424ea63 100644 --- a/scripts/Jenkinsfiles/lettuce +++ b/scripts/Jenkinsfiles/lettuce @@ -11,7 +11,7 @@ def runLettuceTests() { checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, noTags: true, shallow: true]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: git_refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] + refspec: git_refspec, url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] console_output = sh(returnStdout: true, script: 'bash scripts/all-tests.sh').trim() dir('stdout') { writeFile file: "${TEST_SUITE}-stdout.log", text: console_output @@ -42,7 +42,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), @@ -118,7 +118,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), @@ -141,11 +141,11 @@ pipeline { } } emailext body: email_body, - subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { slackSend "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", - subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } } } diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index e2b11c1a5e32..44b3a991348a 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -11,7 +11,7 @@ def runPythonTests() { checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, noTags: true, shallow: true]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: git_refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] + refspec: git_refspec, url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] console_output = sh(returnStdout: true, script: 'bash scripts/all-tests.sh').trim() dir('stdout') { writeFile file: "${TEST_SUITE}-stdout.log", text: console_output @@ -59,7 +59,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), @@ -158,7 +158,7 @@ pipeline { doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, noTags: true, shallow: true]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: git_refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] + refspec: git_refspec, url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] unstash 'lms-unit-reports' unstash 'cms-unit-reports' unstash 'commonlib-unit-reports' @@ -208,7 +208,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), @@ -232,12 +232,12 @@ pipeline { } } emailext body: email_body, - subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { slackSend botUser: true, message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", - subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } } } diff --git a/scripts/Jenkinsfiles/quality b/scripts/Jenkinsfiles/quality index 07a94aaae2e3..be92d2eea3af 100644 --- a/scripts/Jenkinsfiles/quality +++ b/scripts/Jenkinsfiles/quality @@ -19,7 +19,7 @@ def runQualityTests() { checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], doGenerateSubmoduleConfigurations: false, extensions: git_extensions, submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', - refspec: refspec, url: "git@github.com:edx/${REPO_NAME}.git"]]] + refspec: refspec, url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] sh "bash scripts/all-tests.sh" stash includes: '**/reports/**/*', name: "${TEST_SUITE}-${SHARD}-reports" @@ -57,7 +57,7 @@ pipeline { build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: 'Pending'), @@ -165,7 +165,7 @@ pipeline { honorRefspec: true, noTags: true, shallow: false], [$class: 'WipeWorkspace']], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-worker', refspec: "+refs/heads/${ghprbTargetBranch}:refs/remotes/origin/${ghprbTargetBranch} +refs/pull/${ghprbPullId}/*:refs/remotes/origin/pr/${ghprbPullId}/*", - url: "git@github.com:edx/${REPO_NAME}.git"]]] + url: "git@github.com:ucsd-ets/${REPO_NAME}.git"]]] unstash 'quality-1-reports' unstash 'quality-2-reports' unstash 'quality-3-reports' @@ -220,7 +220,7 @@ pipeline { commit_sha = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() build job: 'github-build-status', parameters: [ string(name: 'GIT_SHA', value: commit_sha), - string(name: 'GITHUB_ORG', value: 'edx'), + string(name: 'GITHUB_ORG', value: 'ucsd-ets'), string(name: 'GITHUB_REPO', value: "${REPO_NAME}"), string(name: 'TARGET_URL', value: "${BUILD_URL}"), string(name: 'DESCRIPTION', value: build_description), @@ -244,12 +244,12 @@ pipeline { } } emailext body: email_body, - subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Build failed in Jenkins: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } else if (currentBuild.currentResult == "SUCCESS" && currentBuild.previousBuild.currentResult != "SUCCESS") { slackSend botUser: true, message: "`${JOB_NAME}` #${BUILD_NUMBER}: Back to normal after ${currentBuild.durationString.replace(' and counting', '')}\n${BUILD_URL}" emailext body: "See <${BUILD_URL}>", - subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'testeng@edx.org' + subject: "Jenkins Build is back to normal: ${JOB_NAME} #${BUILD_NUMBER}", to: 'devops@arbisoft.com' } } } From a0206667a5181482d13fb4402c48605295fcaaa0 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Wed, 29 Jan 2020 12:16:57 +0500 Subject: [PATCH 085/119] Update log message to a more meaningful string --- lms/djangoapps/commerce/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 232d6878a62a..007ac8e85b15 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -39,6 +39,7 @@ def is_account_activation_requirement_disabled(): class EcommerceService(object): """ Helper class for ecommerce service integration. """ + def __init__(self): self.config = CommerceConfiguration.current() @@ -312,7 +313,8 @@ def _process_refund(refund_ids, api_client, mode, user, always_notify=False): # the "Refund Failure" emails from ecommerce and thus no need to send that notification # from here. if settings.FEATURES.get('DISABLE_REFUND_FAILURE_NOTIFICATION'): - log.info('Skipping refund failure support notification') + log.info('Skipping support notification for refund failure from edx-platform.' + 'The email will be sent from the ecommerce service') return True return _send_refund_notification(user, refunds_requiring_approval) except: # pylint: disable=bare-except From 6e2c73867c77b66832bf3c7d4eb3dcee1bac9c14 Mon Sep 17 00:00:00 2001 From: Hassan Tariq Date: Wed, 29 Jan 2020 14:34:27 +0500 Subject: [PATCH 086/119] Changed the Refund Failure email subject --- .../edx_ace/commercesupportnotification/email/subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt index 0447ffd7d114..0ca7e680d915 100644 --- a/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt +++ b/openedx/features/ucsd_features/templates/ucsd_features/edx_ace/commercesupportnotification/email/subject.txt @@ -1,3 +1,3 @@ {% load i18n %} -{% blocktrans %}[UC San Diego Online] OpenEdX Administrative Action Needed for {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} +{% blocktrans %}[online.ucsd.edu] Open edX Administrative Action Needed for {{name}} {% endblocktrans %}{% if course %}{% blocktrans %}/ {{course}}{% endblocktrans %}{% endif %} From 817de1339647989d00c03c3b7462374e9a6d3d9a Mon Sep 17 00:00:00 2001 From: Hamza Farooq Date: Wed, 29 Jan 2020 14:50:58 +0500 Subject: [PATCH 087/119] FIX: Pep8 violations --- common/djangoapps/student/views/dashboard.py | 5 ++--- openedx/core/djangoapps/user_api/helpers.py | 2 +- openedx/core/djangoapps/zendesk_proxy/utils.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 85d9c534fc84..265035b03ab2 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -807,7 +807,7 @@ def student_dashboard(request): valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired'] display_sidebar_on_dashboard = (len(order_history_list) or (verification_status['status'] in valid_verification_statuses and - verification_status['should_display'])) + verification_status['should_display'])) # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]: @@ -871,14 +871,13 @@ def student_dashboard(request): # course_id for which AuthorizeNet transaction has been perfromed but notification is yet to be received. transaction_hash = request.COOKIES.get(ECOMMERCE_TRANSACTION_COOKIE_NAME) if transaction_hash: - decoded_course_id = base64.b64decode(transaction_hash) + decoded_course_id = base64.b64decode(transaction_hash) transaction_course_id = CourseKey.from_string(decoded_course_id) pending_transaction_course_name = CourseOverview.get_from_id(transaction_course_id).display_name context.update({ 'pending_upgrade_course_name': pending_transaction_course_name, }) - if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, diff --git a/openedx/core/djangoapps/user_api/helpers.py b/openedx/core/djangoapps/user_api/helpers.py index 2ca438eda9d6..f018f5c2468f 100644 --- a/openedx/core/djangoapps/user_api/helpers.py +++ b/openedx/core/djangoapps/user_api/helpers.py @@ -372,6 +372,7 @@ class LocalizedJSONEncoder(DjangoJSONEncoder): JSON handler that evaluates ugettext_lazy promises. """ # pylint: disable=method-hidden + def default(self, obj): """ Forces evaluation of ugettext_lazy promises. @@ -516,7 +517,6 @@ def _inner(request): # pylint: disable=missing-docstring else: response.content = msg - # Return the response, preserving the original headers. # This is really important, since the student views set cookies # that are used elsewhere in the system (such as the marketing site). diff --git a/openedx/core/djangoapps/zendesk_proxy/utils.py b/openedx/core/djangoapps/zendesk_proxy/utils.py index d7c2aa80fce5..f59dd9394008 100644 --- a/openedx/core/djangoapps/zendesk_proxy/utils.py +++ b/openedx/core/djangoapps/zendesk_proxy/utils.py @@ -12,6 +12,7 @@ log = logging.getLogger(__name__) + def create_zendesk_ticket(requester_name, requester_email, subject, body, custom_fields=None, uploads=None, tags=None): """ Create a Zendesk ticket via API or send an email to support team. From 4640bacf3ac2ea71c8e29986e320c735f2d429e7 Mon Sep 17 00:00:00 2001 From: Hamza Farooq Date: Thu, 30 Jan 2020 17:58:21 +0500 Subject: [PATCH 088/119] initial commit to replace certificate by statement of accomplishment --- lms/djangoapps/courseware/date_summary.py | 12 ++++++------ lms/djangoapps/courseware/tests/test_date_summary.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 60850439bb2d..7af08377630e 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -435,7 +435,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary): Verified track. """ css_class = 'verified-upgrade-deadline' - link_text = ugettext_lazy('Upgrade to Verified Certificate') + link_text = ugettext_lazy('Update Enrollment to Earn Statement of Accomplishment') @property def link(self): @@ -470,9 +470,9 @@ def date(self): def title(self): dynamic_deadline = self._dynamic_deadline() if dynamic_deadline is not None: - return _('Upgrade to Verified Certificate') + return _('Update Enrollment to Earn Statement of Accomplishment') - return _('Verification Upgrade Deadline') + return _('Statement of Accomplishment Enrollment Deadline') def _dynamic_deadline(self): if not self.enrollment: @@ -484,10 +484,10 @@ def _dynamic_deadline(self): def description(self): dynamic_deadline = self._dynamic_deadline() if dynamic_deadline is not None: - return _('Don\'t miss the opportunity to highlight your new knowledge and skills by earning a verified' - ' certificate.') + return _('Don\'t miss the opportunity to highlight your new knowledge and skills by' + ' earning a Statement of Accomplishment') - return _('You are still eligible to upgrade to a Verified Certificate! ' + return _('You are still eligible to update your enrollment and earn a Statement of Accomplishment! ' 'Pursue it to highlight the knowledge and skills you gain in this course.') @property diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 30c13deb3f61..2c69ab2ded62 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -593,7 +593,7 @@ def test_date_with_self_paced_with_enrollment_before_course_start(self): self._check_text(block) def _check_text(self, upgrade_date_summary): - self.assertEqual(upgrade_date_summary.title, 'Upgrade to Verified Certificate') + self.assertEqual(upgrade_date_summary.title, 'Update Enrollment to Earn Statement of Accomplishment') self.assertEqual( upgrade_date_summary.description, 'Don\'t miss the opportunity to highlight your new knowledge and skills by earning a verified' From 76709e677157499d7a48082dfe2f40e253dd0246 Mon Sep 17 00:00:00 2001 From: danialmalik Date: Tue, 4 Feb 2020 17:16:23 +0500 Subject: [PATCH 089/119] Apply changes required for jenkins testing --- pavelib/paver_tests/test_paver_pytest_cmds.py | 6 +- pavelib/utils/test/suites/pytest_suite.py | 12 +- scripts/Jenkinsfiles/python | 42 ++-- scripts/Jenkinsfiles/quality | 10 +- scripts/unit-tests.sh | 4 +- scripts/xblock/xblock_counts.py | 2 +- scripts/xdist/prepare_xdist_nodes.sh | 29 ++- scripts/xdist/pytest_container_manager.py | 203 ---------------- scripts/xdist/pytest_worker_manager.py | 218 ++++++++++++++++++ scripts/xdist/terminate_xdist_nodes.sh | 10 +- 10 files changed, 283 insertions(+), 253 deletions(-) delete mode 100644 scripts/xdist/pytest_container_manager.py create mode 100644 scripts/xdist/pytest_worker_manager.py diff --git a/pavelib/paver_tests/test_paver_pytest_cmds.py b/pavelib/paver_tests/test_paver_pytest_cmds.py index 9a25f15c5a1c..ee0584a297e6 100644 --- a/pavelib/paver_tests/test_paver_pytest_cmds.py +++ b/pavelib/paver_tests/test_paver_pytest_cmds.py @@ -54,9 +54,9 @@ def _expected_command(self, root, test_id, pytestSubclass, run_under_coverage=Tr else: django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='openedx.tests.settings'" - xdist_string = '--tx {}*ssh="ubuntu@{} -o StrictHostKeyChecking=no"' \ - '//python="source /edx/app/edxapp/edxapp_env; {}; python"' \ - '//chdir="/edx/app/edxapp/edx-platform"' \ + xdist_string = '--tx {}*ssh="jenkins@{} -o StrictHostKeyChecking=no"' \ + '//python="source edx-venv/bin/activate; {}; python"' \ + '//chdir="edx-platform"' \ .format(processes, ip, django_env_var_cmd) expected_statement.append(xdist_string) for rsync_dir in Env.rsync_dirs(): diff --git a/pavelib/utils/test/suites/pytest_suite.py b/pavelib/utils/test/suites/pytest_suite.py index 5fb755bc90d4..ba0598d32d6f 100644 --- a/pavelib/utils/test/suites/pytest_suite.py +++ b/pavelib/utils/test/suites/pytest_suite.py @@ -163,9 +163,9 @@ def cmd(self): # The django settings runtime command does not propagate to xdist remote workers django_env_var_cmd = 'export DJANGO_SETTINGS_MODULE={}' \ .format('{}.envs.{}'.format(self.root, self.settings)) - xdist_string = '--tx {}*ssh="ubuntu@{} -o StrictHostKeyChecking=no"' \ - '//python="source /edx/app/edxapp/edxapp_env; {}; python"' \ - '//chdir="/edx/app/edxapp/edx-platform"' \ + xdist_string = '--tx {}*ssh="jenkins@{} -o StrictHostKeyChecking=no"' \ + '//python="source edx-venv/bin/activate; {}; python"' \ + '//chdir="edx-platform"' \ .format(xdist_remote_processes, ip, django_env_var_cmd) cmd.append(xdist_string) for rsync_dir in Env.rsync_dirs(): @@ -284,9 +284,9 @@ def cmd(self): django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='lms.envs.test'" else: django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='openedx.tests.settings'" - xdist_string = '--tx {}*ssh="ubuntu@{} -o StrictHostKeyChecking=no"' \ - '//python="source /edx/app/edxapp/edxapp_env; {}; python"' \ - '//chdir="/edx/app/edxapp/edx-platform"' \ + xdist_string = '--tx {}*ssh="jenkins@{} -o StrictHostKeyChecking=no"' \ + '//python="source edx-venv/bin/activate; {}; python"' \ + '//chdir="edx-platform"' \ .format(xdist_remote_processes, ip, django_env_var_cmd) cmd.append(xdist_string) for rsync_dir in Env.rsync_dirs(): diff --git a/scripts/Jenkinsfiles/python b/scripts/Jenkinsfiles/python index 44b3a991348a..d472efa41a63 100644 --- a/scripts/Jenkinsfiles/python +++ b/scripts/Jenkinsfiles/python @@ -1,12 +1,7 @@ def runPythonTests() { // Determine git refspec, branch, and clone type - if (env.ghprbActualCommit) { - git_branch = "${ghprbActualCommit}" - git_refspec = "+refs/pull/${ghprbPullId}/*:refs/remotes/origin/pr/${ghprbPullId}/*" - } else { - git_branch = "${BRANCH_NAME}" - git_refspec = "+refs/heads/${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" - } + git_branch = xdist_git_branch() + git_refspec = xdist_git_refspec() sshagent(credentials: ['jenkins-worker', 'jenkins-worker-pem'], ignoreMissing: true) { checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: git_branch]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CloneOption', honorRefspec: true, @@ -35,17 +30,30 @@ def xdist_git_branch() { } } +def xdist_git_refspec() { + if (env.ghprbActualCommit) { + return "+refs/pull/${ghprbPullId}/*:refs/remotes/origin/pr/${ghprbPullId}/*" + } else { + return "+refs/heads/${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" + } +} + pipeline { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } options { timestamps() timeout(60) } environment { - XDIST_CONTAINER_SUBNET = credentials('XDIST_CONTAINER_SUBNET') - XDIST_CONTAINER_SECURITY_GROUP = credentials('XDIST_CONTAINER_SECURITY_GROUP') - XDIST_CONTAINER_TASK_NAME = "ironwood-jenkins-worker-task" XDIST_GIT_BRANCH = xdist_git_branch() + XDIST_GIT_REFSPEC = xdist_git_refspec() + XDIST_INSTANCE_TYPE = "c5d.large" + XDIST_WORKER_AMI = credentials('XDIST_WORKER_AMI') + XDIST_WORKER_IAM_PROFILE_ARN = credentials('XDIST_WORKER_IAM_PROFILE_ARN') + XDIST_WORKER_KEY_NAME = "ucsd-jenkins-worker" + XDIST_WORKER_SUBNET = credentials('XDIST_WORKER_SUBNET') + XDIST_WORKER_SECURITY_GROUP = credentials('XDIST_WORKER_SECURITY_GROUP') + WTW_CONTEXT = "python" } stages { stage('Mark build as pending on Github') { @@ -74,10 +82,10 @@ pipeline { stage('Run Tests') { parallel { stage("lms-unit") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "lms-unit" - XDIST_NUM_TASKS = 10 + XDIST_NUM_WORKERS = 10 XDIST_REMOTE_NUM_PROCESSES = 1 } steps { @@ -94,10 +102,10 @@ pipeline { } } stage("cms-unit") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "cms-unit" - XDIST_NUM_TASKS = 3 + XDIST_NUM_WORKERS = 3 XDIST_REMOTE_NUM_PROCESSES = 1 } steps { @@ -114,10 +122,10 @@ pipeline { } } stage("commonlib-unit") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "commonlib-unit" - XDIST_NUM_TASKS = 3 + XDIST_NUM_WORKERS = 3 XDIST_REMOTE_NUM_PROCESSES = 1 } steps { diff --git a/scripts/Jenkinsfiles/quality b/scripts/Jenkinsfiles/quality index be92d2eea3af..e06c1beae9c8 100644 --- a/scripts/Jenkinsfiles/quality +++ b/scripts/Jenkinsfiles/quality @@ -40,7 +40,7 @@ def qualityTestCleanup() { } pipeline { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } options { timestamps() timeout(60) @@ -72,7 +72,7 @@ pipeline { stage('Run Tests') { parallel { stage("commonlib pylint") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 1 @@ -91,7 +91,7 @@ pipeline { } } stage("lms pylint") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 2 @@ -110,7 +110,7 @@ pipeline { } } stage("cms/openedx/pavelib pylint") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 3 @@ -129,7 +129,7 @@ pipeline { } } stage("Other quality checks") { - agent { label "ironwood-jenkins-worker" } + agent { label "jenkins-worker" } environment { TEST_SUITE = "quality" SHARD = 4 diff --git a/scripts/unit-tests.sh b/scripts/unit-tests.sh index 938708c8acff..93aa2b0bda70 100755 --- a/scripts/unit-tests.sh +++ b/scripts/unit-tests.sh @@ -36,9 +36,9 @@ if [[ -n "$TOXENV" ]]; then export NO_PREREQ_INSTALL="True" fi -if [[ -n "$XDIST_NUM_TASKS" ]]; then +if [[ -n "$XDIST_NUM_WORKERS" ]]; then bash scripts/xdist/prepare_xdist_nodes.sh - PAVER_ARGS="-v --xdist_ip_addresses="$( 0: + logger.info("Still waiting on {} workers to spin up".format(len(not_running))) + time.sleep(5) + else: + logger.info("Finished spinning up workers") + all_running = True + break + + if not all_running: + raise Exception( + "Timed out waiting to spin up all workers." + ) + logger.info("Successfully booted up {} workers.".format(number_of_workers)) + + not_ready_ip_addresses = ip_addresses[:] + logger.info("Checking ssh connection to workers.") + pool = Pool(processes=number_of_workers) + for ssh_try in range(0, self.WORKER_SSH_ATTEMPTS): + results = pool.map(_check_worker_ready, not_ready_ip_addresses) + deleted_ips = 0 + for num in range(0, len(results)): + if results[num] == 0: + del(not_ready_ip_addresses[num - deleted_ips]) + deleted_ips += 1 + + if len(not_ready_ip_addresses) == 0: + logger.info("All workers are ready for tests.") + break + + if ssh_try == self.WORKER_SSH_ATTEMPTS - 1: + raise Exception( + "Max ssh tries to remote workers reached." + ) + + logger.info("Not all workers are ready. Sleeping for 5 seconds then retrying.") + time.sleep(5) + + # Generate .txt files containing IP addresses and instance ids + ip_list_string = ",".join(ip_addresses) + logger.info("Worker IP list: {}".format(ip_list_string)) + ip_list_file = open("pytest_worker_ips.txt", "w") + ip_list_file.write(ip_list_string) + ip_list_file.close() + + worker_instance_id_list_string = ",".join(worker_instance_ids) + logger.info("Worker Instance Id list: {}".format(worker_instance_id_list_string)) + worker_arn_file = open("pytest_worker_instance_ids.txt", "w") + worker_arn_file.write(worker_instance_id_list_string) + worker_arn_file.close() + + def terminate_workers(self, worker_instance_ids): + """ + Terminates workers based on a list of worker_instance_ids. + """ + instance_id_list = worker_instance_ids.split(',') + response = self.ec2.terminate_instances( + InstanceIds=instance_id_list + ) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="PytestWorkerManager, manages EC2 workers in an AWS cluster." + ) + + parser.add_argument('--action', '-a', choices=['up', 'down'], default=None, + help="Action for PytestWorkerManager to perform. " + "Either up for spinning up AWS EC2 workers or down for terminating them") + + parser.add_argument('--region', '-g', default='us-west-2', + help="AWS region where EC2 infrastructure lives. Defaults to us-west-2") + + # Spinning up workers + parser.add_argument('--num-workers', '-n', type=int, default=None, + help="Number of EC2 workers to spin up") + + parser.add_argument('--ami', '-ami', default=None, + help="AMI for workers") + + parser.add_argument('--instance-type', '-type', default=None, + help="Desired EC2 instance type") + + parser.add_argument('--subnet-id', '-s', default=None, + help="Subnet for the workers to exist in") + + parser.add_argument('--security_groups', '-sg', nargs='+', default=None, + help="List of security group ids to apply to workers") + + parser.add_argument('--key-name', '-key', default=None, + help="Key pair name for sshing to worker") + + parser.add_argument('--iam-arn', '-iam', default=None, + help="Iam Instance Profile ARN for the workers") + + # Terminating workers + parser.add_argument('--instance-ids', '-ids', default=None, + help="Instance ids terminate") + + args = parser.parse_args() + workerManager = PytestWorkerManager(args.region) + + if args.action == 'up': + workerManager.spin_up_workers( + args.num_workers, + args.ami, + args.instance_type, + args.subnet_id, + args.security_groups, + args.key_name, + args.iam_arn + ) + elif args.action == 'down': + workerManager.terminate_workers( + args.instance_ids + ) + else: + logger.info("No action specified for PytestWorkerManager") diff --git a/scripts/xdist/terminate_xdist_nodes.sh b/scripts/xdist/terminate_xdist_nodes.sh index ee077a2c5739..e2997da6f7c5 100644 --- a/scripts/xdist/terminate_xdist_nodes.sh +++ b/scripts/xdist/terminate_xdist_nodes.sh @@ -1,10 +1,10 @@ #!/bin/bash set -e -if [ -f pytest_task_arns.txt ]; then - echo "Terminating xdist containers with pytest_container_manager.py" - xdist_task_arns=$( Date: Thu, 6 Feb 2020 12:29:29 +0500 Subject: [PATCH 090/119] Fixed the default course image issue in elasticsearch --- openedx/core/lib/courses.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openedx/core/lib/courses.py b/openedx/core/lib/courses.py index 929a4ed81529..c0f59e780a30 100644 --- a/openedx/core/lib/courses.py +++ b/openedx/core/lib/courses.py @@ -10,6 +10,7 @@ from xmodule.assetstore.assetmgr import AssetManager from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore +from xmodule.exceptions import NotFoundError from xmodule.modulestore.django import modulestore @@ -32,8 +33,12 @@ def course_image_url(course, image_key='course_image'): url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL else: loc = StaticContent.compute_location(course.id, getattr(course, image_key)) - url = StaticContent.serialize_asset_key_with_slash(loc) - + try: + AssetManager.find(loc) + except NotFoundError: + url = '/static/' + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL + else: + url = StaticContent.serialize_asset_key_with_slash(loc) return url From 89c8afa630a4539a50daaa5f05fbfe1cdab169bf Mon Sep 17 00:00:00 2001 From: Muhammad Umar Khan Date: Thu, 6 Feb 2020 18:33:57 +0500 Subject: [PATCH 091/119] Update content for user deletion --- .../js/student_account/components/StudentAccountDeletion.jsx | 2 +- .../student_account/components/StudentAccountDeletionModal.jsx | 2 +- .../templates/course_experience/course-home-fragment.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/static/js/student_account/components/StudentAccountDeletion.jsx b/lms/static/js/student_account/components/StudentAccountDeletion.jsx index 0cf71ea1449c..556d0a8b59d9 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletion.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletion.jsx @@ -39,7 +39,7 @@ export class StudentAccountDeletion extends React.Component { render() { const { deletionModalOpen, socialAuthConnected, isActive } = this.state; const loseAccessText = StringUtils.interpolate( - gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'), + gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), { htmlStart: '', htmlEnd: '', diff --git a/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx b/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx index e2e2b301a0a8..3619e20857b3 100644 --- a/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx +++ b/lms/static/js/student_account/components/StudentAccountDeletionModal.jsx @@ -94,7 +94,7 @@ class StudentAccountDeletionConfirmationModal extends React.Component { } = this.state; const { onClose } = this.props; const loseAccessText = StringUtils.interpolate( - gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'), + gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a statement of accomplishment{htmlEnd}.'), { htmlStart: '', htmlEnd: '', diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html index c6dee98b27ac..22ba178055ec 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html @@ -141,7 +141,7 @@