diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 94ace67974..1ba26f8d58 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,3 +8,5 @@ build: python: install: - requirements: "requirements/edx/doc.txt" + - method: pip + path: . diff --git a/Makefile b/Makefile index 649b1f77fc..1efa916edb 100644 --- a/Makefile +++ b/Makefile @@ -94,10 +94,10 @@ shell: ## launch a bash shell in a Docker container with all edx-platform depend # Order is very important in this list: files must appear after everything they include! REQ_FILES = \ requirements/edx/coverage \ - requirements/edx/doc \ requirements/edx/paver \ requirements/edx-sandbox/py38 \ requirements/edx/base \ + requirements/edx/doc \ requirements/edx/testing \ requirements/edx/development \ scripts/xblock/requirements diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index d452b2f004..4d4fff967c 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -296,7 +296,7 @@ class CourseExportTask(UserTask): # pylint: disable=abstract-method arguments_dict (dict): The arguments given to the task function Returns: - text_type: The generated name + str: The generated name """ key = arguments_dict['course_key_string'] return f'Export of {key}' @@ -431,7 +431,7 @@ class CourseImportTask(UserTask): # pylint: disable=abstract-method arguments_dict (dict): The arguments given to the task function Returns: - text_type: The generated name + str: The generated name """ key = arguments_dict['course_key_string'] filename = arguments_dict['archive_name'] diff --git a/cms/djangoapps/pipeline_js/js/xmodule.js b/cms/djangoapps/pipeline_js/js/xmodule.js index 96f8f1afa9..9688d97a4b 100644 --- a/cms/djangoapps/pipeline_js/js/xmodule.js +++ b/cms/djangoapps/pipeline_js/js/xmodule.js @@ -63,7 +63,14 @@ define( t = -1; }, delay); } - }; + + // this is added to compensate for custom css that accidentally hide mathjax + $('.MathJax_SVG>svg').toArray().forEach(el => { + if ($(el).width() === 0) { + $(el).css('max-width', 'inherit'); + } + }); + }; } ); window.CodeMirror = CodeMirror; diff --git a/cms/djangoapps/pipeline_js/utils.py b/cms/djangoapps/pipeline_js/utils.py deleted file mode 100644 index 2584b023ac..0000000000 --- a/cms/djangoapps/pipeline_js/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Utilities for returning XModule JS (used by requirejs) -""" - - -from django.conf import settings -from django.contrib.staticfiles.storage import staticfiles_storage - - -def get_xmodule_urls(): - """ - Returns a list of the URLs to hit to grab all the XModule JS - """ - pipeline_js_settings = settings.PIPELINE['JAVASCRIPT']["module-js"] - if settings.DEBUG: - paths = [path.replace(".coffee", ".js") for path in pipeline_js_settings["source_filenames"]] - else: - paths = [pipeline_js_settings["output_filename"]] - return [staticfiles_storage.url(path) for path in paths] diff --git a/cms/envs/common.py b/cms/envs/common.py index 1e4914af94..fcd78d7441 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1380,15 +1380,6 @@ PIPELINE['JAVASCRIPT'] = { 'source_filenames': base_vendor_js, 'output_filename': 'js/cms-base-vendor.js', }, - 'module-js': { - 'source_filenames': ( - rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') + - rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js') + - rooted_glob(COMMON_ROOT / 'static/', 'common/js/discussion/*.js') - ), - 'output_filename': 'js/cms-modules.js', - 'test_order': 1 - }, } STATICFILES_IGNORE_PATTERNS = ( diff --git a/cms/envs/production.py b/cms/envs/production.py index c3c88abeb6..92e14799b9 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -338,7 +338,7 @@ AWS_S3_CUSTOM_DOMAIN = AUTH_TOKENS.get('AWS_S3_CUSTOM_DOMAIN', 'edxuploads.s3.am if AUTH_TOKENS.get('DEFAULT_FILE_STORAGE'): DEFAULT_FILE_STORAGE = AUTH_TOKENS.get('DEFAULT_FILE_STORAGE') elif AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: - DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' + DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' else: DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' diff --git a/cms/templates/certificates.html b/cms/templates/certificates.html index edf4e45c6c..400d300bf5 100644 --- a/cms/templates/certificates.html +++ b/cms/templates/certificates.html @@ -9,7 +9,6 @@ from openedx.core.djangolib.markup import HTML, Text from openedx.core.djangolib.js_utils import ( dump_js_escaped_json, js_escaped_string ) -import six from six.moves.urllib.parse import quote %> @@ -104,7 +103,7 @@ CMS.User.isGlobalStaff = '${is_global_staff | n, js_escaped_string}'=='True' ? t
% if context_course: <% - url_encoded_course_id = quote(six.text_type(context_course.id).encode('utf-8'), safe='') + url_encoded_course_id = quote(str(context_course.id).encode('utf-8'), safe='') details_url = utils.reverse_course_url('settings_handler', context_course.id) grading_url = utils.reverse_course_url('grading_handler', context_course.id) course_team_url = utils.reverse_course_url('course_team_handler', context_course.id) diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 44d456de26..e949f37d79 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -2,8 +2,6 @@ <%inherit file="base.html" /> <%def name="online_help_token()"><% return "files" %> <%! - import six - from cms.djangoapps.contentstore import utils from cms.djangoapps.contentstore.config.waffle_utils import should_show_checklists_quality from django.urls import reverse @@ -40,7 +38,7 @@ <%static:studiofrontend entry="courseHealthCheck"> <% - course_key = six.text_type(context_course.id) + course_key = str(context_course.id) certificates_url = '' if has_certificates_enabled(context_course): certificates_url = utils.reverse_course_url('certificates_list_handler', course_key) diff --git a/cms/templates/content_libraries/xblock_iframe.html b/cms/templates/content_libraries/xblock_iframe.html index 79d0de1409..6a60530cc9 100644 --- a/cms/templates/content_libraries/xblock_iframe.html +++ b/cms/templates/content_libraries/xblock_iframe.html @@ -115,6 +115,13 @@ t = -1; }, delay); } + + // this is added to compensate for custom css that accidentally hide mathjax + $('.MathJax_SVG>svg').toArray().forEach(el => { + if ($(el).width() === 0) { + $(el).css('max-width', 'inherit'); + } + }); }; diff --git a/docs/guides/conf.py b/docs/guides/conf.py index b18de3c48d..0135285246 100644 --- a/docs/guides/conf.py +++ b/docs/guides/conf.py @@ -278,11 +278,14 @@ autodoc_mock_imports = [ # run sphinx-apidoc against and the directories under "docs" in which to store # the generated *.rst files modules = { - 'cms': 'references/docstrings/cms', 'lms': 'references/docstrings/lms', 'openedx': 'references/docstrings/openedx', - 'common': 'references/docstrings/common', - 'xmodule': 'references/docstrings/xmodule', + # Commenting this out for now because they blow up the build + # time and memory limits for RTD. We can come back to these + # later once we get parallel builds working hopefully. + # 'cms': 'references/docstrings/cms', + # 'common': 'references/docstrings/common', + # 'xmodule': 'references/docstrings/xmodule', } diff --git a/docs/technical/conf.py b/docs/technical/conf.py index afa3bdefee..3b36915217 100644 --- a/docs/technical/conf.py +++ b/docs/technical/conf.py @@ -9,13 +9,22 @@ import git # -- Project information ----------------------------------------------------- project = "edx-platform Technical Reference" -copyright = f'{datetime.now().year}, Axim Collaborative, Inc' # pylint: disable=redefined-builtin -author = 'Axim Collaborative, Inc' +copyright = f"{datetime.now().year}, Axim Collaborative, Inc" # pylint: disable=redefined-builtin +author = "Axim Collaborative, Inc" release = "" # -- General configuration --------------------------------------------------- -extensions = ["code_annotations.contrib.sphinx.extensions.featuretoggles", "code_annotations.contrib.sphinx.extensions.settings"] +extensions = [ + "code_annotations.contrib.sphinx.extensions.featuretoggles", + "code_annotations.contrib.sphinx.extensions.settings", + "sphinx_reredirects", +] + +redirects = { + "*": "https://docs.openedx.org/projects/edx-platform/en/latest/$source.html", +} + templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] @@ -38,7 +47,7 @@ settings_repo_version = edx_platform_version # -- Options for HTML output ------------------------------------------------- -html_theme = 'sphinx_book_theme' +html_theme = "sphinx_book_theme" html_static_path = ["_static"] html_favicon = "https://logos.openedx.org/open-edx-favicon.ico" html_logo = "https://logos.openedx.org/open-edx-logo-color.png" @@ -72,5 +81,5 @@ html_theme_options = { rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/" >Creative Commons Attribution-ShareAlike 4.0 International License. - """ + """, } diff --git a/lms/djangoapps/ccx/views.py b/lms/djangoapps/ccx/views.py index 66e18b2bd0..3c5f3130a1 100644 --- a/lms/djangoapps/ccx/views.py +++ b/lms/djangoapps/ccx/views.py @@ -1,8 +1,6 @@ """ Views related to the Custom Courses feature. """ - - import csv import datetime import functools @@ -11,7 +9,6 @@ import logging from copy import deepcopy import pytz -import six from ccx_keys.locator import CCXLocator from django.conf import settings from django.contrib import messages @@ -538,8 +535,7 @@ def ccx_grades_csv(request, course, ccx=None): if not header: # Encode the header row in utf-8 encoding in case there are # unicode characters - header = [section['label'].encode('utf-8') if six.PY2 else section['label'] - for section in course_grade.summary['section_breakdown']] + header = [section['label'] for section in course_grade.summary['section_breakdown']] rows.append(["id", "email", "username", "grade"] + header) percents = { diff --git a/lms/djangoapps/discussion/notification_prefs/views.py b/lms/djangoapps/discussion/notification_prefs/views.py index 470f9a821a..0084b079b0 100644 --- a/lms/djangoapps/discussion/notification_prefs/views.py +++ b/lms/djangoapps/discussion/notification_prefs/views.py @@ -7,7 +7,6 @@ from base64 import urlsafe_b64decode, urlsafe_b64encode from binascii import Error from hashlib import sha256 -import six from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES @@ -53,7 +52,7 @@ class UsernameCipher: @staticmethod def _get_aes_cipher(initialization_vector): hash_ = sha256() - hash_.update(six.b(settings.SECRET_KEY)) + hash_.update(settings.SECRET_KEY.encode()) return Cipher(AES(hash_.digest()), CBC(initialization_vector), backend=default_backend()) @staticmethod diff --git a/lms/djangoapps/grades/api.py b/lms/djangoapps/grades/api.py index fb06e28013..bb1b43fa7f 100644 --- a/lms/djangoapps/grades/api.py +++ b/lms/djangoapps/grades/api.py @@ -9,7 +9,6 @@ from datetime import datetime import pytz from django.core.exceptions import ObjectDoesNotExist from opaque_keys.edx.keys import CourseKey, UsageKey -from six import text_type from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type # Public Grades Modules diff --git a/lms/djangoapps/learner_dashboard/programs.py b/lms/djangoapps/learner_dashboard/programs.py index 2db218ef6d..d567a4b9a3 100644 --- a/lms/djangoapps/learner_dashboard/programs.py +++ b/lms/djangoapps/learner_dashboard/programs.py @@ -6,6 +6,7 @@ import json from abc import ABC, abstractmethod from urllib.parse import quote +from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.http import Http404 from django.template.loader import render_to_string @@ -65,18 +66,22 @@ class ProgramsFragmentView(EdxFragmentView): if is_user_b2c_subscriptions_enabled else [] ) - subscriptions_marketing_url = ( - get_program_subscriptions_marketing_url() + subscription_upsell_data = ( + { + 'marketing_url': get_program_subscriptions_marketing_url(), + 'minimum_price': settings.SUBSCRIPTIONS_MINIMUM_PRICE, + 'trial_length': settings.SUBSCRIPTIONS_TRIAL_LENGTH, + } if is_user_b2c_subscriptions_enabled - else '' + else {} ) context = { 'marketing_url': get_program_marketing_url(programs_config, mobile_only), - 'subscriptions_marketing_url': subscriptions_marketing_url, 'programs': meter.engaged_programs, 'progress': meter.progress(), 'programs_subscription_data': programs_subscription_data, + 'subscription_upsell_data': subscription_upsell_data, 'user_preferences': get_user_preferences(user), 'is_user_b2c_subscriptions_enabled': is_user_b2c_subscriptions_enabled, 'mobile_only': bool(mobile_only) @@ -152,12 +157,13 @@ class ProgramDetailsFragmentView(EdxFragmentView): 'user_preferences': get_user_preferences(user), 'program_data': program_data, 'program_subscription_data': program_subscription_data, - 'is_user_b2c_subscriptions_enabled': is_user_b2c_subscriptions_enabled, 'course_data': course_data, 'certificate_data': certificate_data, 'industry_pathways': industry_pathways, 'credit_pathways': credit_pathways, 'program_tab_view_enabled': program_tab_view_enabled(), + 'is_user_b2c_subscriptions_enabled': is_user_b2c_subscriptions_enabled, + 'subscriptions_trial_length': settings.SUBSCRIPTIONS_TRIAL_LENGTH, 'discussion_fragment': { 'configured': program_discussion_lti.is_configured, 'iframe': program_discussion_lti.render_iframe() diff --git a/lms/djangoapps/save_for_later/admin.py b/lms/djangoapps/save_for_later/admin.py deleted file mode 100644 index 9c94e5e94b..0000000000 --- a/lms/djangoapps/save_for_later/admin.py +++ /dev/null @@ -1,29 +0,0 @@ -""" Django admin pages for save_for_later app """ - -from django.contrib import admin - -from lms.djangoapps.save_for_later.models import SavedCourse, SavedProgram - - -class SavedCourseAdmin(admin.ModelAdmin): - """ - Admin for the Saved Course table. - """ - - list_display = ['email', 'course_id', 'email_sent_count', 'reminder_email_sent'] - - search_fields = ['email', 'course_id'] - - -class SavedProgramAdmin(admin.ModelAdmin): - """ - Admin for the Saved Program table. - """ - - list_display = ['email', 'program_uuid', 'email_sent_count', 'reminder_email_sent'] - - search_fields = ['email', 'program_uuid'] - - -admin.site.register(SavedCourse, SavedCourseAdmin) -admin.site.register(SavedProgram, SavedProgramAdmin) diff --git a/lms/djangoapps/save_for_later/api/__init__.py b/lms/djangoapps/save_for_later/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/api/urls.py b/lms/djangoapps/save_for_later/api/urls.py deleted file mode 100644 index 888e03dfe9..0000000000 --- a/lms/djangoapps/save_for_later/api/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -URL definitions for the save_for_later API. -""" - - -from django.conf.urls import include -from django.urls import path - -app_name = 'lms.djangoapps.save_for_later' - -urlpatterns = [ - path('v1/', include(('lms.djangoapps.save_for_later.api.v1.urls', 'v1'), namespace='v1')), -] diff --git a/lms/djangoapps/save_for_later/api/v1/__init__.py b/lms/djangoapps/save_for_later/api/v1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/api/v1/tests/__init__.py b/lms/djangoapps/save_for_later/api/v1/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/api/v1/tests/test_views.py b/lms/djangoapps/save_for_later/api/v1/tests/test_views.py deleted file mode 100644 index 038016cd09..0000000000 --- a/lms/djangoapps/save_for_later/api/v1/tests/test_views.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -Save for later tests -""" - -from unittest.mock import patch, MagicMock - -import ddt -from django.conf import settings -from django.core.cache import cache -from django.urls import reverse -from django.test.utils import override_settings -from rest_framework.test import APITestCase -from opaque_keys.edx.keys import CourseKey - -from openedx.core.djangolib.testing.utils import skip_unless_lms -from common.djangoapps.third_party_auth.tests.testutil import ThirdPartyAuthTestMixin -from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory - - -@skip_unless_lms -@ddt.ddt -class CourseSaveForLaterApiViewTest(ThirdPartyAuthTestMixin, APITestCase): - """ - Tests for CourseSaveForLaterApiView - """ - - def setUp(self): # pylint: disable=arguments-differ - """ - Test Setup - """ - super().setUp() - - self.api_url = reverse('api:v1:save_course') - self.email = 'test@edx.org' - self.invalid_email = 'test@edx' - self.course_id = 'course-v1:TestX+ProEnroll+P' - self.org_img_url = '/path/logo.png' - self.course_key = CourseKey.from_string(self.course_id) - CourseOverviewFactory.create(id=self.course_key) - - @override_settings( - EDX_BRAZE_API_KEY='test-key', - EDX_BRAZE_API_SERVER='http://test.url' - ) - @patch('lms.djangoapps.utils.BrazeClient', MagicMock()) - def test_save_course_using_email(self): - """ - Test successfully email sent - """ - request_payload = { - 'email': self.email, - 'course_id': self.course_id, - 'marketing_url': 'http://google.com', - 'org_img_url': self.org_img_url, - } - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 200 - - @override_settings( - CACHES={ - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'registration_proxy', - } - } - ) - def test_save_course_api_rate_limiting(self): - """ - Test api rate limit - """ - request_payload = { - 'email': self.email, - 'course_id': self.course_id, - 'marketing_url': 'http://google.com', - 'org_img_url': self.org_img_url, - } - for _ in range(int(settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT.split('/')[0])): - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code != 403 - - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 403 - cache.clear() - - for _ in range(int(settings.SAVE_FOR_LATER_IP_RATE_LIMIT.split('/')[0])): - request_payload['email'] = 'test${_}@edx.org'.format(_=_) - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code != 403 - - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 403 - cache.clear() - - def test_invalid_email_address(self): - """ - Test email validation - """ - request_payload = {'email': self.invalid_email, 'course_id': self.course_id} - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 400 - - -@skip_unless_lms -@ddt.ddt -class ProgramSaveForLaterApiViewTest(ThirdPartyAuthTestMixin, APITestCase): - """ - Tests for ProgramSaveForLaterApiView - """ - - def setUp(self): # pylint: disable=arguments-differ - """ - Test Setup - """ - super().setUp() - - self.api_url = reverse('api:v1:save_program') - self.email = 'test@edx.org' - self.invalid_email = 'test@edx' - self.uuid = '587f6abe-bfa4-4125-9fbe-4789bf3f97f1' - self.program = ProgramFactory(uuid=self.uuid) - - @override_settings( - EDX_BRAZE_API_KEY='test-key', - EDX_BRAZE_API_SERVER='http://test.url' - ) - @patch('lms.djangoapps.utils.BrazeClient', MagicMock()) - @patch('lms.djangoapps.save_for_later.api.v1.views.get_programs') - def test_save_program_using_email(self, mock_get_programs): - """ - Test successfully email sent - """ - mock_get_programs.return_value = self.program - request_payload = { - 'email': self.email, - 'program_uuid': self.uuid, - } - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 200 - - @override_settings( - CACHES={ - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'registration_proxy', - } - } - ) - def test_save_program_api_rate_limiting(self): - """ - Test api rate limit - """ - request_payload = { - 'email': self.email, - 'program_uuid': self.uuid, - } - for _ in range(int(settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT.split('/')[0])): - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code != 403 - - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 403 - cache.clear() - - for _ in range(int(settings.SAVE_FOR_LATER_IP_RATE_LIMIT.split('/')[0])): - request_payload['email'] = 'test${_}@edx.org'.format(_=_) - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code != 403 - - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 403 - cache.clear() - - def test_invalid_email_address(self): - """ - Test email validation - """ - request_payload = {'email': self.invalid_email, 'program_uuid': self.uuid} - response = self.client.post(self.api_url, data=request_payload) - assert response.status_code == 400 diff --git a/lms/djangoapps/save_for_later/api/v1/urls.py b/lms/djangoapps/save_for_later/api/v1/urls.py deleted file mode 100644 index b58e213179..0000000000 --- a/lms/djangoapps/save_for_later/api/v1/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -URLs for save_for_later v1 -""" - -from django.urls import re_path - -from lms.djangoapps.save_for_later.api.v1.views import CourseSaveForLaterApiView, ProgramSaveForLaterApiView - -urlpatterns = [ - re_path(r'^save/program/$', ProgramSaveForLaterApiView.as_view(), name='save_program'), - re_path(r'^save/course/$', CourseSaveForLaterApiView.as_view(), name='save_course'), -] diff --git a/lms/djangoapps/save_for_later/api/v1/views.py b/lms/djangoapps/save_for_later/api/v1/views.py deleted file mode 100644 index 598b7ee564..0000000000 --- a/lms/djangoapps/save_for_later/api/v1/views.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Save for later views -""" - -import logging - -from django.conf import settings -from django.utils.decorators import method_decorator -from django_ratelimit.decorators import ratelimit -from rest_framework.response import Response -from rest_framework.views import APIView -from django.db import transaction -from opaque_keys.edx.keys import CourseKey -from opaque_keys import InvalidKeyError - -from openedx.core.djangoapps.user_api.accounts.api import get_email_validation_error -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.catalog.utils import get_programs - -from lms.djangoapps.save_for_later.helper import send_email -from lms.djangoapps.save_for_later.models import SavedCourse, SavedProgram - -log = logging.getLogger(__name__) - -POST_EMAIL_KEY = 'openedx.core.djangoapps.util.ratelimit.request_data_email' -REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip' -USER_SEND_SAVE_FOR_LATER_EMAIL = 'user.send.save.for.later.email' - - -class CourseSaveForLaterApiView(APIView): - """ - Save course API VIEW - """ - - @transaction.atomic - @method_decorator(ratelimit(key=POST_EMAIL_KEY, - rate=settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT, - method='POST', block=False)) - @method_decorator(ratelimit(key=REAL_IP_KEY, - rate=settings.SAVE_FOR_LATER_IP_RATE_LIMIT, - method='POST', block=False)) - def post(self, request): - """ - **Use Case** - - * Send favorite course through email to user for later learning. - - **Example Request for course** - - POST /api/v1/save/course/ - - **Example POST Request for course** - - { - "email": "test@edx.org", - "course_id": "course-v1:edX+DemoX+2021", - "marketing_url": "https://test.com", - "org_img_url": "https://test.com/logo.png", - "weeks_to_complete": 7, - "min_effort": 4, - "max_effort": 5, - - } - """ - user = request.user - data = request.data - course_id = data.get('course_id') - email = data.get('email') - org_img_url = data.get('org_img_url') - marketing_url = data.get('marketing_url') - weeks_to_complete = data.get('weeks_to_complete', 0) - min_effort = data.get('min_effort', 0) - max_effort = data.get('max_effort', 0) - user_id = request.user.id - pref_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') - send_to_self = bool(not request.user.is_anonymous and request.user.email == email) - - if getattr(request, 'limited', False): - return Response({'error_code': 'rate-limited'}, status=403) - - if get_email_validation_error(email): - return Response({'error_code': 'incorrect-email'}, status=400) - - try: - course_key = CourseKey.from_string(course_id) - course = CourseOverview.get_from_id(course_key) - except InvalidKeyError: - return Response({'error_code': 'invalid-course-key'}, status=400) - except CourseOverview.DoesNotExist: - return Response({'error_code': 'course-not-found'}, status=404) - - SavedCourse.objects.update_or_create( - email=email, - course_id=course_id, - defaults={ - 'user_id': user.id, - 'org_img_url': org_img_url, - 'marketing_url': marketing_url, - 'weeks_to_complete': weeks_to_complete, - 'min_effort': min_effort, - 'max_effort': max_effort, - 'reminder_email_sent': False, - } - ) - course_data = { - 'course': course, - 'send_to_self': send_to_self, - 'user_id': user_id, - 'pref-lang': pref_lang, - 'org_img_url': org_img_url, - 'marketing_url': marketing_url, - 'weeks_to_complete': weeks_to_complete, - 'min_effort': min_effort, - 'max_effort': max_effort, - 'type': 'course', - 'reminder': False, - 'braze_event': USER_SEND_SAVE_FOR_LATER_EMAIL, - } - if send_email(email, course_data): - return Response({'result': 'success'}, status=200) - else: - return Response({'error_code': 'email-not-send'}, status=400) - - -class ProgramSaveForLaterApiView(APIView): - """ - API VIEW - """ - - @transaction.atomic - @method_decorator(ratelimit(key=POST_EMAIL_KEY, - rate=settings.SAVE_FOR_LATER_EMAIL_RATE_LIMIT, - method='POST', block=False)) - @method_decorator(ratelimit(key=REAL_IP_KEY, - rate=settings.SAVE_FOR_LATER_IP_RATE_LIMIT, - method='POST', block=False)) - def post(self, request): - """ - **Use Case** - - * Send favorite program through email to user for later learning. - - **Example Request for program** - - POST /api/v1/save/program/ - - **Example POST Request for program** - - { - "email": "test@edx.org", - "program_uuid": "587f6abe-bfa4-4125-9fbe-4789bf3f97f1" - } - """ - user = request.user - data = request.data - program_uuid = data.get('program_uuid') - email = data.get('email') - user_id = request.user.id - pref_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') - send_to_self = bool(not request.user.is_anonymous and request.user.email == email) - - if getattr(request, 'limited', False): - return Response({'error_code': 'rate-limited'}, status=403) - - if get_email_validation_error(email): - return Response({'error_code': 'incorrect-email'}, status=400) - - if not program_uuid: - return Response({'error_code': 'program-uuid-missing'}, status=400) - - program = get_programs(uuid=program_uuid) - SavedProgram.objects.update_or_create( - email=email, - program_uuid=program_uuid, - defaults={ - 'user_id': user.id, - 'reminder_email_sent': False, - } - ) - if program: - program_data = { - 'program': program, - 'send_to_self': send_to_self, - 'user_id': user_id, - 'pref-lang': pref_lang, - 'type': 'program', - 'reminder': False, - 'braze_event': USER_SEND_SAVE_FOR_LATER_EMAIL, - } - if send_email(email, program_data): - return Response({'result': 'success'}, status=200) - else: - return Response({'error_code': 'email-not-send'}, status=400) - - return Response({'error_code': 'program-not-found'}, status=404) diff --git a/lms/djangoapps/save_for_later/helper.py b/lms/djangoapps/save_for_later/helper.py deleted file mode 100644 index e023f203ee..0000000000 --- a/lms/djangoapps/save_for_later/helper.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -helper functions -""" - -import logging -from datetime import datetime -from django.conf import settings -from eventtracking import tracker - -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from common.djangoapps.course_modes.models import CourseMode -from lms.djangoapps.utils import get_braze_client - -log = logging.getLogger(__name__) - -USER_SAVE_FOR_LATER_EMAIL_SENT = 'edx.bi.user.saveforlater.email.sent' -USER_SAVE_FOR_LATER_REMINDER_EMAIL_SENT = 'edx.bi.user.saveforlater.reminder.email.sent' - - -def _get_program_pacing(course_runs): - """ - get pacing type of published course run of course for program - """ - - pacing = [course_run.get('pacing_type') if course_run.get('status') == 'published' - else '' for course_run in course_runs][0] - return 'Self-paced' if pacing == 'self_paced' else 'Instructor-led' - - -def _get_course_price(course): - """ - Get price of a course - """ - return CourseMode.min_course_price_for_currency(course_id=str(course.id), currency='USD') - - -def _get_event_properties(data): - """ - set event properties for course and program which are required in braze email template - """ - lms_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL) - event_properties = { - 'time': datetime.now().isoformat(), - 'name': data.get('braze_event'), - } - - event_type = data.get('type') - if event_type == 'course': - course = data.get('course') - price = _get_course_price(course) - event_properties.update({ - 'properties': { - 'course_image_url': '{base_url}{image_path}'.format( - base_url=lms_url, image_path=course.course_image_url - ), - 'partner_image_url': data.get('org_img_url'), - 'enroll_course_url': '{base_url}/register?course_id={course_id}&enrollment_action=enroll&email_opt_in=' - 'false&save_for_later=true'.format(base_url=lms_url, course_id=course.id), - 'view_course_url': data.get('marketing_url') + '?save_for_later=true' if data.get( - 'marketing_url') else '#', - 'display_name': course.display_name, - 'short_description': course.short_description, - 'weeks_to_complete': data.get('weeks_to_complete'), - 'min_effort': data.get('min_effort'), - 'max_effort': data.get('max_effort'), - 'pacing_type': 'Self-paced' if course.self_paced else 'Instructor-led', - 'type': event_type, - 'price': 'Free' if price == 0 else f'${price} USD', - } - }) - - if event_type == 'program': - program = data.get('program') - price = int(program.get('price_ranges')[0].get('total')) - event_properties.update({ - 'properties': { - 'program_image_url': program.get('card_image_url'), - 'partner_image_url': program.get('authoring_organizations')[0].get('logo_image_url') if program.get( - 'authoring_organizations') else None, - 'view_program_url': program.get('marketing_url') + '?save_for_later=true' if program.get( - 'marketing_url') else '#', - 'title': program.get('title'), - 'education_level': program.get('type'), - 'total_courses': len(program.get('courses')) if program.get('courses') else 0, - 'weeks_to_complete': program.get('weeks_to_complete'), - 'min_effort': program.get('min_hours_effort_per_week'), - 'max_effort': program.get('max_hours_effort_per_week'), - 'pacing_type': _get_program_pacing(program.get('courses')[0].get('course_runs')), - 'price': f'${price} USD', - 'registered': bool(program.get('type') in ['MicroMasters', 'MicroBachelors']), - 'type': event_type, - } - }) - return event_properties - - -def send_email(email, data): - """ - Send email through Braze - """ - event_properties = _get_event_properties(data) - braze_client = get_braze_client() - - if not braze_client: - return False - - try: - attributes = None - external_id = braze_client.get_braze_external_id(email) - if external_id: - event_properties.update({'external_id': external_id}) - else: - braze_client.create_braze_alias(emails=[email], alias_label='save_for_later') - user_alias = { - 'alias_label': 'save_for_later', - 'alias_name': email, - } - event_properties.update({'user_alias': user_alias}) - attributes = [{ - 'user_alias': user_alias, - 'pref-lang': data.get('pref-lang', 'en') - }] - - braze_client.track_user(events=[event_properties], attributes=attributes) - - event_type = data.get('type') - event_data = { - 'user_id': data.get('user_id'), - 'category': 'save-for-later', - 'type': event_type, - 'send_to_self': data.get('send_to_self'), - } - if event_type == 'program': - program = data.get('program') - event_data.update({'program_uuid': program.get('uuid')}) - elif event_type == 'course': - course = data.get('course') - event_data.update({'course_key': str(course.id)}) - - tracker.emit( - USER_SAVE_FOR_LATER_REMINDER_EMAIL_SENT if data.get('reminder') else USER_SAVE_FOR_LATER_EMAIL_SENT, - event_data - ) - except Exception: # pylint: disable=broad-except - log.warning('Unable to send save for later email ', exc_info=True) - return False - else: - return True diff --git a/lms/djangoapps/save_for_later/management/__init__.py b/lms/djangoapps/save_for_later/management/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/management/commands/__init__.py b/lms/djangoapps/save_for_later/management/commands/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py deleted file mode 100644 index 5669925dd4..0000000000 --- a/lms/djangoapps/save_for_later/management/commands/send_course_reminder_emails.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Management command to send reminder emails. -""" - -import logging - -from textwrap import dedent -from datetime import datetime, timedelta - -from django.conf import settings -from django.core.management import BaseCommand -from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user - -from lms.djangoapps.save_for_later.helper import send_email -from lms.djangoapps.save_for_later.models import SavedCourse -from common.djangoapps.student.models import CourseEnrollment -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from common.djangoapps.util.query import use_read_replica_if_available - -logger = logging.getLogger(__name__) - -USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL = 'user.send.save.for.later.reminder.email' - - -class Command(BaseCommand): - """ - Command to send reminder emails to those users who - saved course by email but not register within 15 days. - - - Examples: - - ./manage.py lms send_course_reminder_emails --batch-size=100 - """ - help = dedent(__doc__) - - def add_arguments(self, parser): - parser.add_argument( - '--batch-size', - type=int, - default=1000, - help='Maximum number of users to send reminder email in one chunk') - - def handle(self, *args, **options): - """ - Handle the send save for later reminder emails. - """ - - reminder_email_threshold_date = datetime.now() - timedelta( - days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) - saved_courses_ids = SavedCourse.objects.filter( - reminder_email_sent=False, modified__lt=reminder_email_threshold_date - ).values_list('id', flat=True) - total = saved_courses_ids.count() - batch_size = max(1, options.get('batch_size')) - num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 - - for batch_num in range(int(num_batches)): - reminder_email_sent_ids = [] - start = batch_num * batch_size - end = min(start + batch_size, total) - 1 - saved_courses_batch_ids = list(saved_courses_ids)[start:end + 1] - - query = SavedCourse.objects.filter(id__in=saved_courses_batch_ids).order_by('-modified') - saved_courses = use_read_replica_if_available(query) - for saved_course in saved_courses: - user = User.objects.filter(email=saved_course.email).first() - course_overview = CourseOverview.get_from_id(saved_course.course_id) - course_data = { - 'course': course_overview, - 'send_to_self': None, - 'user_id': saved_course.user_id, - 'org_img_url': saved_course.org_img_url, - 'marketing_url': saved_course.marketing_url, - 'weeks_to_complete': saved_course.weeks_to_complete, - 'min_effort': saved_course.min_effort, - 'max_effort': saved_course.max_effort, - 'type': 'course', - 'reminder': True, - 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, - } - if user: - enrollment = CourseEnrollment.get_enrollment(user, saved_course.course_id) - if enrollment: - continue - email_sent = send_email(saved_course.email, course_data) - if email_sent: - reminder_email_sent_ids.append(saved_course.id) - else: - logging.info("Unable to send reminder email to {user} for {course} course" - .format(user=str(saved_course.email), course=str(saved_course.course_id))) - SavedCourse.objects.filter(id__in=reminder_email_sent_ids).update(reminder_email_sent=True) diff --git a/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py deleted file mode 100644 index 77bffd81cb..0000000000 --- a/lms/djangoapps/save_for_later/management/commands/send_program_reminder_emails.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Management command to send program reminder emails. -""" - -import logging - -from textwrap import dedent -from datetime import datetime, timedelta - -from django.conf import settings -from django.core.management import BaseCommand -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user - -from lms.djangoapps.save_for_later.helper import send_email -from lms.djangoapps.save_for_later.models import SavedProgram -from lms.djangoapps.program_enrollments.api import get_program_enrollment -from openedx.core.djangoapps.catalog.utils import get_programs -from common.djangoapps.util.query import use_read_replica_if_available - -LOGGER = logging.getLogger(__name__) - -USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL = 'user.send.save.for.later.reminder.email' - - -class Command(BaseCommand): - """ - Command to send reminder emails to those users who saved - program by email but not enroll program within 15 days. - - - Examples: - - ./manage.py lms send_program_reminder_emails --batch-size=100 - """ - help = dedent(__doc__) - - def add_arguments(self, parser): - parser.add_argument( - '--batch-size', - type=int, - default=1000, - help='Maximum number of users to send reminder email in one chunk') - - def handle(self, *args, **options): - """ - Handle the send save for later reminder emails. - """ - reminder_email_threshold_date = datetime.now() - timedelta( - days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) - saved_program_ids = SavedProgram.objects.filter( - reminder_email_sent=False, modified__lt=reminder_email_threshold_date - ).values_list('id', flat=True) - total = saved_program_ids.count() - batch_size = max(1, options.get('batch_size')) - num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 - - for batch_num in range(int(num_batches)): - reminder_email_sent_ids = [] - start = batch_num * batch_size - end = min(start + batch_size, total) - 1 - saved_program_batch_ids = list(saved_program_ids)[start:end + 1] - - query = SavedProgram.objects.filter(id__in=saved_program_batch_ids).order_by('-modified') - saved_programs = use_read_replica_if_available(query) - for saved_program in saved_programs: - user = User.objects.filter(email=saved_program.email).first() - program = get_programs(uuid=saved_program.program_uuid) - if program: - program_data = { - 'program': program, - 'send_to_self': None, - 'user_id': saved_program.user_id, - 'type': 'program', - 'reminder': True, - 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, - } - try: - if user and get_program_enrollment(program_uuid=saved_program.program_uuid, user=user): - continue - except ObjectDoesNotExist: - pass - email_sent = send_email(saved_program.email, program_data) - if email_sent: - reminder_email_sent_ids.append(saved_program.id) - else: - logging.info("Unable to send reminder email to {user} for {program} program" - .format(user=str(saved_program.email), program=str(saved_program.program_uuid))) - SavedProgram.objects.filter(id__in=reminder_email_sent_ids).update(reminder_email_sent=True) diff --git a/lms/djangoapps/save_for_later/management/commands/tests/__init__.py b/lms/djangoapps/save_for_later/management/commands/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py deleted file mode 100644 index 2c93492ae3..0000000000 --- a/lms/djangoapps/save_for_later/management/commands/tests/test_send_course_reminder_emails.py +++ /dev/null @@ -1,44 +0,0 @@ -""" Test the test_send_course_reminder_emails command line script.""" - -from unittest.mock import patch - -import ddt -from django.core.management import call_command -from django.test.utils import override_settings -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase - -from openedx.core.djangolib.testing.utils import skip_unless_lms -from common.djangoapps.student.tests.factories import UserFactory -from lms.djangoapps.save_for_later.tests.factories import SavedCourseFactory -from lms.djangoapps.save_for_later.models import SavedCourse -from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory - - -@ddt.ddt -@skip_unless_lms -class SavedCourseReminderEmailsTest(SharedModuleStoreTestCase): - """ - Test the test_send_course_reminder_emails management command - """ - - def setUp(self): - super().setUp() - self.course_id = 'course-v1:edX+DemoX+Demo_Course' - self.user = UserFactory(email='email@test.com', username='jdoe') - self.saved_course = SavedCourseFactory.create(course_id=self.course_id, user_id=self.user.id) - self.saved_course_1 = SavedCourseFactory.create(course_id=self.course_id) - CourseOverviewFactory.create(id=self.saved_course.course_id) - CourseOverviewFactory.create(id=self.saved_course_1.course_id) - - @override_settings( - EDX_BRAZE_API_KEY='test-key', - EDX_BRAZE_API_SERVER='http://test.url' - ) - def test_send_reminder_emails(self): - with patch('lms.djangoapps.utils.BrazeClient') as mock_task: - call_command('send_course_reminder_emails', '--batch-size=1') - mock_task.assert_called() - - saved_course = SavedCourse.objects.filter(course_id=self.course_id).first() - assert saved_course.reminder_email_sent is True - assert saved_course.email_sent_count > 0 diff --git a/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py b/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py deleted file mode 100644 index b3665a0456..0000000000 --- a/lms/djangoapps/save_for_later/management/commands/tests/test_send_program_reminder_emails.py +++ /dev/null @@ -1,43 +0,0 @@ -""" Test the test_send_program_reminder_emails command line script.""" - - -from unittest.mock import patch - -import ddt -from django.core.management import call_command -from django.test.utils import override_settings -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase - -from openedx.core.djangolib.testing.utils import skip_unless_lms -from lms.djangoapps.save_for_later.tests.factories import SavedPogramFactory -from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory -from lms.djangoapps.save_for_later.models import SavedProgram - - -@ddt.ddt -@skip_unless_lms -class SavedProgramReminderEmailsTest(SharedModuleStoreTestCase): - """ - Test the send_program_reminder_emails management command - """ - - def setUp(self): - super().setUp() - self.uuid = '587f6abe-bfa4-4125-9fbe-4789bf3f97f1' - self.program = ProgramFactory(uuid=self.uuid) - self.saved_program = SavedPogramFactory.create(program_uuid=self.uuid) - - @override_settings( - EDX_BRAZE_API_KEY='test-key', - EDX_BRAZE_API_SERVER='http://test.url' - ) - @patch('lms.djangoapps.save_for_later.management.commands.send_program_reminder_emails.get_programs') - def test_send_reminder_emails(self, mock_get_programs): - mock_get_programs.return_value = self.program - with patch('lms.djangoapps.utils.BrazeClient') as mock_task: - call_command('send_program_reminder_emails', '--batch-size=1') - mock_task.assert_called() - - saved_program = SavedProgram.objects.filter(program_uuid=self.uuid).first() - assert saved_program.reminder_email_sent is True - assert saved_program.email_sent_count > 0 diff --git a/lms/djangoapps/save_for_later/message_types.py b/lms/djangoapps/save_for_later/message_types.py deleted file mode 100644 index b2fe0c7dfb..0000000000 --- a/lms/djangoapps/save_for_later/message_types.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -ACE message types for the save_for_later module. -""" - - -from openedx.core.djangoapps.ace_common.message import BaseMessageType - - -class SaveForLater(BaseMessageType): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.options['transactional'] = True diff --git a/lms/djangoapps/save_for_later/migrations/0003_auto_20230711_1431.py b/lms/djangoapps/save_for_later/migrations/0003_auto_20230711_1431.py new file mode 100644 index 0000000000..41e076ca85 --- /dev/null +++ b/lms/djangoapps/save_for_later/migrations/0003_auto_20230711_1431.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.20 on 2023-07-11 14:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('save_for_later', '0002_auto_20220322_1621'), + ] + + operations = [ + migrations.DeleteModel( + name='SavedCourse', + ), + migrations.DeleteModel( + name='SavedProgram', + ), + ] diff --git a/lms/djangoapps/save_for_later/models.py b/lms/djangoapps/save_for_later/models.py index 23eef180b0..e69de29bb2 100644 --- a/lms/djangoapps/save_for_later/models.py +++ b/lms/djangoapps/save_for_later/models.py @@ -1,59 +0,0 @@ -""" -Django ORM models for save_for_later APP -""" - - -from model_utils.models import TimeStampedModel -from django.db import models -from opaque_keys.edx.django.models import CourseKeyField - -from openedx.core.djangolib.model_mixins import DeletableByUserValue - - -class SavedCourse(DeletableByUserValue, TimeStampedModel): - """ - Tracks save course by email. - - .. pii: Stores email address of the User. - .. pii_types: email_address - .. pii_retirement: local_api - """ - user_id = models.IntegerField(null=True, blank=True) - email = models.EmailField(db_index=True) - course_id = CourseKeyField(max_length=255, db_index=True) - marketing_url = models.CharField(max_length=255, null=True, blank=True) - org_img_url = models.CharField(max_length=255, null=True, blank=True) - weeks_to_complete = models.IntegerField(null=True) - min_effort = models.IntegerField(null=True) - max_effort = models.IntegerField(null=True) - email_sent_count = models.IntegerField(null=True, default=0) - reminder_email_sent = models.BooleanField(default=False, null=True) - - class Meta: - unique_together = ('email', 'course_id',) - - def save(self, *args, **kwargs): - self.email_sent_count = self.email_sent_count + 1 - super().save(*args, **kwargs) - - -class SavedProgram(DeletableByUserValue, TimeStampedModel): - """ - Tracks save program by email. - - .. pii: Stores email address of the User. - .. pii_types: email_address - .. pii_retirement: local_api - """ - user_id = models.IntegerField(null=True, blank=True) - email = models.EmailField(db_index=True) - program_uuid = models.UUIDField() - email_sent_count = models.IntegerField(null=True, default=0) - reminder_email_sent = models.BooleanField(default=False, null=True) - - class Meta: - unique_together = ('email', 'program_uuid',) - - def save(self, *args, **kwargs): - self.email_sent_count = self.email_sent_count + 1 - super().save(*args, **kwargs) diff --git a/lms/djangoapps/save_for_later/signals.py b/lms/djangoapps/save_for_later/signals.py deleted file mode 100644 index 1537dcc549..0000000000 --- a/lms/djangoapps/save_for_later/signals.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Signal handler for save for later -""" -from django.dispatch.dispatcher import receiver - -from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_CRITICAL -from .models import SavedCourse, SavedProgram - - -@receiver(USER_RETIRE_LMS_CRITICAL) -def _listen_for_lms_retire(sender, **kwargs): # pylint: disable=unused-argument - user = kwargs.get('user') - SavedCourse.delete_by_user_value(user.id, field='user_id') - SavedProgram.delete_by_user_value(user.id, field='user_id') diff --git a/lms/djangoapps/save_for_later/tests/__init__.py b/lms/djangoapps/save_for_later/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/save_for_later/tests/factories.py b/lms/djangoapps/save_for_later/tests/factories.py deleted file mode 100644 index 5c10295ec5..0000000000 --- a/lms/djangoapps/save_for_later/tests/factories.py +++ /dev/null @@ -1,46 +0,0 @@ -""" - Provides factories for save_for_later models. -""" - - -from datetime import datetime -from pytz import UTC - -import factory -from factory.django import DjangoModelFactory - -from lms.djangoapps.save_for_later.models import SavedCourse, SavedProgram - - -class SavedCourseFactory(DjangoModelFactory): - """ - Factory for SavedCourses objects - """ - class Meta: - model = SavedCourse - django_get_or_create = ('course_id', 'user_id') - - email = 'abc@test.com' - course_id = factory.Sequence('{}'.format) - user_id = factory.Sequence('{}'.format) - reminder_email_sent = False - email_sent_count = 0 - created = datetime(2020, 1, 1, tzinfo=UTC) - modified = datetime(2020, 2, 1, tzinfo=UTC) - - -class SavedPogramFactory(DjangoModelFactory): - """ - Factory for SavedProgram objects - """ - class Meta: - model = SavedProgram - django_get_or_create = ('program_uuid', ) - - email = 'abc@test.com' - user_id = factory.Sequence('{}'.format) - program_uuid = factory.Sequence('{}'.format) - reminder_email_sent = False - email_sent_count = 0 - created = datetime(2020, 1, 1, tzinfo=UTC) - modified = datetime(2020, 2, 1, tzinfo=UTC) diff --git a/lms/djangoapps/save_for_later/tests/test_signals.py b/lms/djangoapps/save_for_later/tests/test_signals.py deleted file mode 100644 index e3d19b8760..0000000000 --- a/lms/djangoapps/save_for_later/tests/test_signals.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Unit tests for the signals -""" -from uuid import uuid4 -from django.test import TestCase - -from common.djangoapps.student.tests.factories import UserFactory -from ..models import SavedCourse, SavedProgram -from ..signals import _listen_for_lms_retire - - -class RetirementSignalTest(TestCase): - """ - Tests for the user retirement signal - """ - - def setUp(self): - super().setUp() - self.user = UserFactory() - self.email = self.user.email - - def _create_objects(self): - """ - Create test objects. - """ - SavedCourse.objects.create(user_id=self.user.id, email=self.email, course_id='course-v1:TestX+TestX101+1T2022') - SavedProgram.objects.create(user_id=self.user.id, email=self.email, program_uuid=uuid4()) - - assert SavedCourse.objects.filter(email=self.email).exists() - assert SavedProgram.objects.filter(email=self.email).exists() - - def test_retire_success(self): - self._create_objects() - _listen_for_lms_retire(sender=self.__class__, user=self.user, email=self.email) - - assert not SavedCourse.objects.filter(email=self.email).exists() - assert not SavedProgram.objects.filter(email=self.email).exists() - - def test_retire_success_no_entries(self): - assert not SavedCourse.objects.filter(email=self.email).exists() - assert not SavedProgram.objects.filter(email=self.email).exists() - _listen_for_lms_retire(sender=self.__class__, user=self.user, email=self.email) diff --git a/lms/djangoapps/save_for_later/urls.py b/lms/djangoapps/save_for_later/urls.py deleted file mode 100644 index ae1e1158c5..0000000000 --- a/lms/djangoapps/save_for_later/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -""" URLs for save_for_later """ - -from django.conf.urls import include -from django.urls import path - -urlpatterns = [ - path('api/', include(('lms.djangoapps.save_for_later.api.urls', 'api'), namespace='api')), -] diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index a1282c5835..3dfa5b51ae 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -4,8 +4,6 @@ <%! import json %> <%! -import six - from django.utils.translation import gettext as _ from openedx.core.djangolib.js_utils import ( dump_js_escaped_json, js_escaped_string @@ -41,7 +39,7 @@ from openedx.core.djangolib.js_utils import ( <%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory"> TeamsTabFactory({ - courseID: '${six.text_type(course.id) | n, js_escaped_string}', + courseID: '${str(course.id) | n, js_escaped_string}', topics: ${topics | n, dump_js_escaped_json}, hasManagedTopic: ${has_managed_teamset | n, dump_js_escaped_json}, hasPublicManagedTopic: ${has_public_managed_teamset | n, dump_js_escaped_json}, diff --git a/lms/djangoapps/teams/tests/test_models.py b/lms/djangoapps/teams/tests/test_models.py index dddea7cff7..508377cfc4 100644 --- a/lms/djangoapps/teams/tests/test_models.py +++ b/lms/djangoapps/teams/tests/test_models.py @@ -96,7 +96,7 @@ class TestModelStrings(SharedModuleStoreTestCase): "" ) - def test_team_membership_text_type(self): + def test_team_membership_str(self): assert str(self.team_membership) == ( "the-user is member of The Team in edx/the-course/1" ) diff --git a/lms/envs/common.py b/lms/envs/common.py index 906365113e..69683cb62d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1080,12 +1080,6 @@ MARKETING_EMAILS_OPT_IN = False # .. toggle_tickets: 'https://openedx.atlassian.net/browse/VAN-622' ENABLE_COPPA_COMPLIANCE = False -# VAN-741 - save for later api put behind a flag to make it only available for edX -ENABLE_SAVE_FOR_LATER = False - -# VAN-887 - save for later reminder emails threshold days -SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD = 15 - ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms REPO_ROOT = PROJECT_ROOT.dirname() @@ -2586,14 +2580,6 @@ PIPELINE['JAVASCRIPT'] = { 'source_filenames': main_vendor_js, 'output_filename': 'js/lms-main_vendor.js', }, - 'module-descriptor-js': { - 'source_filenames': rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js'), - 'output_filename': 'js/lms-module-descriptors.js', - }, - 'module-js': { - 'source_filenames': rooted_glob(COMMON_ROOT / 'static', 'xmodule/modules/js/*.js'), - 'output_filename': 'js/lms-modules.js', - }, 'discussion': { 'source_filenames': discussion_js, 'output_filename': 'js/discussion.js', @@ -4784,10 +4770,6 @@ OPTIONAL_FIELD_API_RATELIMIT = '10/h' PASSWORD_RESET_IP_RATE = '1/m' PASSWORD_RESET_EMAIL_RATE = '2/h' -#### SAVE FOR LATER EMAIL RATE LIMIT SETTINGS #### -SAVE_FOR_LATER_IP_RATE_LIMIT = '100/d' -SAVE_FOR_LATER_EMAIL_RATE_LIMIT = '5/h' - #### BRAZE API SETTINGS #### @@ -5332,6 +5314,8 @@ SUBSCRIPTIONS_API_PATH = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscription/" SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL = None SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscribe/" SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL = None +SUBSCRIPTIONS_MINIMUM_PRICE = '$39' +SUBSCRIPTIONS_TRIAL_LENGTH = 7 SUBSCRIPTIONS_SERVICE_WORKER_USERNAME = 'subscriptions_worker' ############## NOTIFICATIONS EXPIRY ############## diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 856e9ef9c1..6e4498a200 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -509,6 +509,8 @@ SUBSCRIPTIONS_API_PATH = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscription/" SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL = None SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscribe/" SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL = None +SUBSCRIPTIONS_MINIMUM_PRICE = '$39' +SUBSCRIPTIONS_TRIAL_LENGTH = 7 # API access management API_ACCESS_MANAGER_EMAIL = 'api-access@example.com' diff --git a/lms/envs/test.py b/lms/envs/test.py index 4cc08fdadd..284ebc915d 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -633,13 +633,6 @@ RESET_PASSWORD_API_RATELIMIT = '2/m' CORS_ORIGIN_WHITELIST = ['https://sandbox.edx.org'] -# enable /api/v1/save/course/ api for testing -ENABLE_SAVE_FOR_LATER = True - -# rate limit for /api/v1/save/course/ api -SAVE_FOR_LATER_IP_RATE_LIMIT = '5/d' -SAVE_FOR_LATER_EMAIL_RATE_LIMIT = '5/m' - #################### Network configuration #################### # Tests are not behind any proxies CLOSEST_CLIENT_IP_FROM_HEADERS = [] @@ -682,3 +675,5 @@ SUBSCRIPTIONS_API_PATH = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscription/" SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL = None SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscribe/" SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL = None +SUBSCRIPTIONS_MINIMUM_PRICE = '$39' +SUBSCRIPTIONS_TRIAL_LENGTH = 7 diff --git a/lms/static/js/learner_dashboard/models/program_subscription_model.js b/lms/static/js/learner_dashboard/models/program_subscription_model.js index f907b8938c..5d80eaf831 100644 --- a/lms/static/js/learner_dashboard/models/program_subscription_model.js +++ b/lms/static/js/learner_dashboard/models/program_subscription_model.js @@ -15,6 +15,7 @@ class ProgramSubscriptionModel extends Backbone.Model { programData: { subscription_prices }, urls = {}, userPreferences = {}, + subscriptionsTrialLength: trialLength = 7, } = context; const priceInUSD = subscription_prices?.find(({ currency }) => currency === 'USD'); @@ -56,8 +57,6 @@ class ProgramSubscriptionModel extends Backbone.Model { userPreferences ); - const trialLength = 7; - super( { hasActiveTrial, diff --git a/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js index c3714b2f7a..40e1a707cd 100644 --- a/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/sidebar_view_spec.js @@ -6,7 +6,11 @@ describe('Sidebar View', () => { let view = null; const context = { marketingUrl: 'https://www.example.org/programs', - subscriptionsMarketingUrl: 'https://www.example.org/program-subscriptions', + subscriptionUpsellData: { + marketing_url: 'https://www.example.org/program-subscriptions', + minimum_price: '$39', + trial_length: 7, + }, isUserB2CSubscriptionsEnabled: true, }; @@ -72,7 +76,7 @@ describe('Sidebar View', () => { el: '.sidebar', context: { isUserB2CSubscriptionsEnabled: true, - subscriptionsMarketingUrl: '', + subscriptionUpsellData: context.subscriptionUpsellData, }, }); view.render(); diff --git a/lms/static/js/learner_dashboard/views/sidebar_view.js b/lms/static/js/learner_dashboard/views/sidebar_view.js index 8f4a75c166..c092e707b0 100644 --- a/lms/static/js/learner_dashboard/views/sidebar_view.js +++ b/lms/static/js/learner_dashboard/views/sidebar_view.js @@ -31,7 +31,7 @@ class SidebarView extends Backbone.View { postRender() { if (this.context.isUserB2CSubscriptionsEnabled) { this.subscriptionUpsellView = new SubscriptionUpsellView({ - context: this.context, + subscriptionUpsellData: this.context.subscriptionUpsellData, }); } diff --git a/lms/static/js/learner_dashboard/views/subscription_upsell_view.js b/lms/static/js/learner_dashboard/views/subscription_upsell_view.js index 06235c2a47..c7e8c89c7c 100644 --- a/lms/static/js/learner_dashboard/views/subscription_upsell_view.js +++ b/lms/static/js/learner_dashboard/views/subscription_upsell_view.js @@ -12,17 +12,16 @@ class SubscriptionUpsellView extends Backbone.View { super(Object.assign({}, defaults, options)); } - initialize({ context }) { + initialize(options) { this.tpl = HtmlUtils.template(subscriptionUpsellTpl); - this.context = context; + this.subscriptionUpsellModel = new Backbone.Model( + options.subscriptionUpsellData, + ); this.render(); } render() { - const data = $.extend({}, this.context, { - minSubscriptionPrice: '$39', - trialLength: 7, - }); + const data = this.subscriptionUpsellModel.toJSON(); HtmlUtils.setHtml(this.$el, this.tpl(data)); } } diff --git a/lms/templates/conditional_block.html b/lms/templates/conditional_block.html index 0b6c348cbd..4e9311a877 100644 --- a/lms/templates/conditional_block.html +++ b/lms/templates/conditional_block.html @@ -3,14 +3,13 @@ from django.urls import reverse from django.utils.translation import ugettext as _ from openedx.core.djangolib.markup import HTML, Text -from six import text_type %> <% def _message(reqm, message): return Text(message).format(link=HTML("{url_name}").format( - url = reverse('jump_to', kwargs=dict(course_id=text_type(reqm.course_id), - location=text_type(reqm.location))), + url = reverse('jump_to', kwargs=dict(course_id=str(reqm.course_id), + location=str(reqm.location))), url_name = reqm.display_name_with_default)) %> % if message: diff --git a/lms/templates/course.html b/lms/templates/course.html index db765a62ab..bdc9c54ea7 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -3,11 +3,10 @@ <%! from django.utils.translation import gettext as _ from django.urls import reverse -from six import text_type %> <%page args="course" expression_filter="h"/>
- +
${course.display_name_with_default} ${course.display_number_with_default} diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 629bd3fc2e..91d7a2a28e 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -6,13 +6,10 @@ from django.utils.translation import pgettext from django.urls import reverse from lms.djangoapps.courseware.courses import get_course_about_section from django.conf import settings -from six import text_type from common.djangoapps.edxmako.shortcuts import marketing_link from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.markup import clean_dangerous_html, HTML, Text from openedx.core.lib.courses import course_image_url - -from six import string_types %> <%inherit file="../main.html" /> @@ -171,7 +168,7 @@ from six import string_types
  • ${_("Classes Start")}

    - % if isinstance(course_start_date, string_types): + % if isinstance(course_start_date, str): ${course_start_date} % else: <% @@ -191,7 +188,7 @@ from six import string_types
  • ${_("Classes End")}

    - % if isinstance(course_end_date, string_types): + % if isinstance(course_end_date, str): ${course_end_date} % else: <% @@ -217,7 +214,7 @@ from six import string_types %endif % if pre_requisite_courses: - <% prc_target = reverse('about_course', args=[text_type(pre_requisite_courses[0]['key'])]) %> + <% prc_target = reverse('about_course', args=[str(pre_requisite_courses[0]['key'])]) %>
  • ${_("Prerequisites")}

    diff --git a/lms/templates/courseware/course_about_sidebar_header.html b/lms/templates/courseware/course_about_sidebar_header.html index c15ae42e7d..b511fe7ce6 100644 --- a/lms/templates/courseware/course_about_sidebar_header.html +++ b/lms/templates/courseware/course_about_sidebar_header.html @@ -6,7 +6,6 @@ import six from django.utils.translation import ugettext as _ from django.urls import reverse from django.conf import settings -from six import text_type %>
    @@ -20,7 +19,7 @@ from six import text_type site_domain = static.get_value('site_domain', settings.SITE_NAME) site_protocol = 'https' if settings.HTTPS == 'on' else 'http' platform_name = static.get_platform_name() - course_path = reverse('about_course', args=[text_type(course.id)]) + course_path = reverse('about_course', args=[str(course.id)]) course_url = f"{site_protocol}://{site_domain}{course_path}" ## Translators: This text will be automatically posted to the student's diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 1003efb57f..094a6766c5 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -3,7 +3,6 @@ <%namespace name='static' file='/static_content.html'/> <%def name="online_help_token()"><% return "courseware" %> <%! -import six import waffle from django.conf import settings @@ -224,13 +223,13 @@ ${HTML(fragment.foot_html())} % endif % if chapter: - ${chapter.display_name_with_default} + ${chapter.display_name_with_default} % endif % if section: - ${section.display_name_with_default} + ${section.display_name_with_default} % endif diff --git a/lms/templates/courseware/gradebook.html b/lms/templates/courseware/gradebook.html index 5207431873..c5f4152a02 100644 --- a/lms/templates/courseware/gradebook.html +++ b/lms/templates/courseware/gradebook.html @@ -4,7 +4,6 @@ <%! from django.utils.translation import ugettext as _ from django.urls import reverse -from six import text_type %> <%block name="js_extra"> @@ -58,7 +57,7 @@ from six import text_type %for student in students: - ${student['username']} + ${student['username']} %endfor diff --git a/lms/templates/courseware/xqa_interface.html b/lms/templates/courseware/xqa_interface.html index e765a4e00a..11f895453f 100644 --- a/lms/templates/courseware/xqa_interface.html +++ b/lms/templates/courseware/xqa_interface.html @@ -1,8 +1,5 @@ ## xss-lint: disable=mako-missing-default <%namespace name='static' file='/static_content.html'/> -<%! -import six -%> @@ -28,7 +25,7 @@ function setup_debug(element_id, edit_link, staff_context){ var username_or_email = $("#" + element_id + "_history_student_username").val(); var location = $("#" + element_id + "_history_location").val(); // xss-lint: disable=mako-invalid-js-filter - $("#" + element_id + "_history_text").load('/courses/' + "${six.text_type(getattr(course,'id','')) | u}" + + $("#" + element_id + "_history_text").load('/courses/' + "${str(getattr(course,'id','')) | u}" + "/submission_history/" + encodeURIComponent(username_or_email) + "/" + location); return false; } diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 34358488f9..d44cdefc63 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -4,7 +4,6 @@ <%namespace name='static' file='static_content.html'/> <%! import pytz -import six from datetime import datetime, timedelta from django.urls import reverse from django.utils.translation import gettext as _ @@ -220,7 +219,7 @@ from common.djangoapps.student.models import CourseEnrollment is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) is_course_voucher_refundable = (session_id in enrolled_courses_voucher_refundable) course_requirements = courses_requirements_not_met.get(session_id) - related_programs = inverted_programs.get(six.text_type(entitlement.course_uuid if is_unfulfilled_entitlement else session_id)) + related_programs = inverted_programs.get(str(entitlement.course_uuid if is_unfulfilled_entitlement else session_id)) show_consent_link = (session_id in consent_required_courses) resume_button_url = resume_button_urls[dashboard_index] %> diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 882f70aec2..55718cb92f 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -2,7 +2,6 @@ <%! import datetime -import six from django.conf import settings from django.utils.http import urlencode, urlquote_plus @@ -174,7 +173,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG % endif % else: - % if isinstance(course_date, six.string_types): + % if isinstance(course_date, str): ${container_string.format(date=course_date)} % elif course_date is not None: @@ -272,7 +271,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG diff --git a/lms/templates/header/navbar-not-authenticated.html b/lms/templates/header/navbar-not-authenticated.html index 47aa9abd2c..c1e7200ed5 100644 --- a/lms/templates/header/navbar-not-authenticated.html +++ b/lms/templates/header/navbar-not-authenticated.html @@ -9,7 +9,6 @@ from django.conf import settings from django.urls import reverse from django.utils.translation import gettext as _ -from six import text_type from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_microfrontend %> diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 7f0046a79d..c0392f3293 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -54,7 +54,8 @@ from openedx.core.djangolib.markup import HTML - <%static:js group='module-descriptor-js'/> + <%static:webpack entry='HtmlBlockEditor'/> + <%static:js group='instructor_dash'/> <%static:js group='application'/> diff --git a/lms/templates/learner_dashboard/program_details_fragment.html b/lms/templates/learner_dashboard/program_details_fragment.html index d27b5e4467..7aff07a6a3 100644 --- a/lms/templates/learner_dashboard/program_details_fragment.html +++ b/lms/templates/learner_dashboard/program_details_fragment.html @@ -23,6 +23,7 @@ ProgramDetailsFactory({ creditPathways: ${credit_pathways | n, dump_js_escaped_json}, programTabViewEnabled: ${program_tab_view_enabled | n, dump_js_escaped_json}, isUserB2CSubscriptionsEnabled: ${is_user_b2c_subscriptions_enabled | n, dump_js_escaped_json}, + subscriptionsTrialLength: ${subscriptions_trial_length | n, dump_js_escaped_json}, discussionFragment: ${discussion_fragment, | n, dump_js_escaped_json}, live_fragment: ${live_fragment, | n, dump_js_escaped_json} }); diff --git a/lms/templates/learner_dashboard/programs_fragment.html b/lms/templates/learner_dashboard/programs_fragment.html index 0bf18cfaec..d3e9f3d8ef 100644 --- a/lms/templates/learner_dashboard/programs_fragment.html +++ b/lms/templates/learner_dashboard/programs_fragment.html @@ -30,9 +30,9 @@ from openedx.core.djangolib.js_utils import ( <%static:webpack entry="ProgramListFactory"> ProgramListFactory({ marketingUrl: '${marketing_url | n, js_escaped_string}', - subscriptionsMarketingUrl: '${subscriptions_marketing_url | n, js_escaped_string}', programsData: ${programs | n, dump_js_escaped_json}, programsSubscriptionData: ${programs_subscription_data | n, dump_js_escaped_json}, + subscriptionUpsellData: ${subscription_upsell_data | n, dump_js_escaped_json}, userProgress: ${progress | n, dump_js_escaped_json}, userPreferences: ${user_preferences | n, dump_js_escaped_json}, isUserB2CSubscriptionsEnabled: ${is_user_b2c_subscriptions_enabled | n, dump_js_escaped_json}, diff --git a/lms/templates/learner_dashboard/subscription_upsell_view.underscore b/lms/templates/learner_dashboard/subscription_upsell_view.underscore index ef765da17b..cc01d47c81 100644 --- a/lms/templates/learner_dashboard/subscription_upsell_view.underscore +++ b/lms/templates/learner_dashboard/subscription_upsell_view.underscore @@ -8,10 +8,13 @@ - + <%- gettext('Explore subscription options') %> diff --git a/lms/templates/main.html b/lms/templates/main.html index a723e1835f..1cbd04c1fa 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -14,7 +14,6 @@ <%namespace name='static' file='static_content.html'/> <% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %> <%! -import six from lms.djangoapps.branding import api as branding_api from django.urls import reverse from django.utils.http import urlquote_plus @@ -93,7 +92,7 @@ from common.djangoapps.pipeline_mako import render_require_js_path_overrides <% rtl_css_file = self.attr.main_css.replace('.css', '-rtl.css') %> - + % else: % endif diff --git a/lms/templates/navigation/navbar-not-authenticated.html b/lms/templates/navigation/navbar-not-authenticated.html index a82a24e878..5a82a76ef0 100644 --- a/lms/templates/navigation/navbar-not-authenticated.html +++ b/lms/templates/navigation/navbar-not-authenticated.html @@ -7,7 +7,6 @@ <%! from django.urls import reverse from django.utils.translation import ugettext as _ -from six import text_type %>