Merge branch 'master' into edx-depr31

This commit is contained in:
Yagnesh1998
2023-07-19 10:28:30 +05:30
committed by GitHub
135 changed files with 2521 additions and 1796 deletions

View File

@@ -8,3 +8,5 @@ build:
python:
install:
- requirements: "requirements/edx/doc.txt"
- method: pip
path: .

View File

@@ -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

View File

@@ -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']

View File

@@ -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;

View File

@@ -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]

View File

@@ -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 = (

View File

@@ -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'

View File

@@ -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
<div class="bit">
% 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)

View File

@@ -2,8 +2,6 @@
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "files" %></%def>
<%!
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)

View File

@@ -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');
}
});
};
</script>
<script type="text/x-mathjax-config">

View File

@@ -3,7 +3,6 @@
<%def name="online_help_token()"><% return "develop_course" %></%def>
<%!
import logging
import six
from six.moves.urllib.parse import quote
from cms.djangoapps.contentstore.config.waffle_utils import should_show_checklists_quality
@@ -160,7 +159,7 @@ from django.urls import reverse
<h2 class="title title-3">${_("This course has proctored exam settings that are incomplete or invalid.")}</h2>
<p>
% if mfe_proctored_exam_settings_url:
<% 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='') %>
${Text(_("To update these settings go to the {link_start}Proctored Exam Settings page{link_end}.")).format(
link_start=HTML('<a href="${mfe_proctored_exam_settings_url}">').format(
mfe_proctored_exam_settings_url=mfe_proctored_exam_settings_url
@@ -263,7 +262,7 @@ from django.urls import reverse
},
"enable_quality": ${should_show_checklists_quality(context_course.id) | n, dump_js_escaped_json},
"links": {
"settings": ${reverse('settings_handler', kwargs={'course_key_string': six.text_type(course_key)})| n, dump_js_escaped_json}
"settings": ${reverse('settings_handler', kwargs={'course_key_string': str(course_key)})| n, dump_js_escaped_json}
}
}
</%static:studiofrontend>

View File

@@ -3,8 +3,6 @@
<%namespace name='static' file='static_content.html'/>
<%!
import six
from django.urls import reverse
from django.utils.translation import gettext as _
%>
@@ -41,7 +39,7 @@
% else:
<ul class="list-actions">
<li class="item-action">
<a class="action action-export-git action-primary" href="${reverse('export_git', kwargs=dict(course_key_string=six.text_type(context_course.id)))}?action=push">
<a class="action action-export-git action-primary" href="${reverse('export_git', kwargs=dict(course_key_string=str(context_course.id)))}?action=push">
<span class="icon fa fa-arrow-circle-o-down" aria-hidden="true"></span>
<span class="copy">${_("Export to Git")}</span>
</a>

View File

@@ -11,7 +11,6 @@ from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
from openedx.core.djangolib.markup import HTML, Text
import six
from six.moves.urllib.parse import quote
%>
@@ -115,7 +114,7 @@ from six.moves.urllib.parse import quote
<div class="bit">
% 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)

View File

@@ -1,8 +1,6 @@
## xss-lint: disable=mako-missing-default
<%inherit file="base.html" />
<%!
import six
from django.utils.translation import gettext as _
from django.urls import reverse
@@ -125,7 +123,7 @@ from openedx.core.djangolib.js_utils import (
"${context_course.display_name_with_default | h}",
${users | n, dump_js_escaped_json},
// xss-lint: disable=mako-invalid-js-filter
"${reverse('course_team_handler', kwargs={'course_key_string': six.text_type(context_course.id), 'email': '@@EMAIL@@'}) | n, js_escaped_string}",
"${reverse('course_team_handler', kwargs={'course_key_string': str(context_course.id), 'email': '@@EMAIL@@'}) | n, js_escaped_string}",
${request.user.id | n, dump_js_escaped_json},
${allow_actions | n, dump_js_escaped_json}
);

View File

@@ -14,7 +14,6 @@
dump_js_escaped_json, js_escaped_string
)
from openedx.core.djangolib.markup import HTML, Text
import six
from six.moves.urllib.parse import quote
from six.moves.urllib import parse as urllib
%>
@@ -711,7 +710,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
<div class="bit">
% 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='')
course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
grading_config_url = utils.reverse_course_url('grading_handler', context_course.id)
advanced_config_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)

View File

@@ -3,7 +3,6 @@
<%def name="online_help_token()"><% return "advanced" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
import six
from six.moves.urllib.parse import quote
from django.utils.translation import gettext as _
from cms.djangoapps.contentstore import utils
@@ -43,7 +42,7 @@
<h2 class="title title-3">${_("This course has proctored exam settings that are incomplete or invalid.")}</h2>
<p>
% if mfe_proctored_exam_settings_url:
<% 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='') %>
${Text(_("You will be unable to make changes until the errors are resolved. To update these settings go to the {link_start}Proctored Exam Settings page{link_end}.")).format(
link_start=HTML('<a href="{mfe_proctored_exam_settings_url}">').format(
mfe_proctored_exam_settings_url=mfe_proctored_exam_settings_url
@@ -136,7 +135,7 @@
<div class="bit">
% 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)

View File

@@ -6,7 +6,6 @@
<%namespace name='static' file='static_content.html'/>
<%!
import six
from six.moves.urllib.parse import quote
import json
from cms.djangoapps.contentstore import utils
@@ -158,7 +157,7 @@
<div class="bit">
% 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='')
detailed_settings_url = utils.reverse_course_url('settings_handler', context_course.id)
course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
advanced_settings_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)

View File

@@ -1,7 +1,6 @@
<%page expression_filter="h" args="online_help_token"/>
<%namespace name='static' file='../static_content.html'/>
<%!
import six
from six.moves.urllib.parse import quote
from django.conf import settings
from django.urls import reverse
@@ -39,23 +38,23 @@
% if context_course:
<%
course_key = context_course.id
url_encoded_course_key = quote(six.text_type(course_key).encode('utf-8'), safe='')
index_url = reverse('course_handler', kwargs={'course_key_string': six.text_type(course_key)})
course_team_url = reverse('course_team_handler', kwargs={'course_key_string': six.text_type(course_key)})
assets_url = reverse('assets_handler', kwargs={'course_key_string': six.text_type(course_key)})
textbooks_url = reverse('textbooks_list_handler', kwargs={'course_key_string': six.text_type(course_key)})
videos_url = reverse('videos_handler', kwargs={'course_key_string': six.text_type(course_key)})
import_url = reverse('import_handler', kwargs={'course_key_string': six.text_type(course_key)})
course_info_url = reverse('course_info_handler', kwargs={'course_key_string': six.text_type(course_key)})
export_url = reverse('export_handler', kwargs={'course_key_string': six.text_type(course_key)})
settings_url = reverse('settings_handler', kwargs={'course_key_string': six.text_type(course_key)})
grading_url = reverse('grading_handler', kwargs={'course_key_string': six.text_type(course_key)})
advanced_settings_url = reverse('advanced_settings_handler', kwargs={'course_key_string': six.text_type(course_key)})
tabs_url = reverse('tabs_handler', kwargs={'course_key_string': six.text_type(course_key)})
url_encoded_course_key = quote(str(course_key).encode('utf-8'), safe='')
index_url = reverse('course_handler', kwargs={'course_key_string': str(course_key)})
course_team_url = reverse('course_team_handler', kwargs={'course_key_string': str(course_key)})
assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_key)})
textbooks_url = reverse('textbooks_list_handler', kwargs={'course_key_string': str(course_key)})
videos_url = reverse('videos_handler', kwargs={'course_key_string': str(course_key)})
import_url = reverse('import_handler', kwargs={'course_key_string': str(course_key)})
course_info_url = reverse('course_info_handler', kwargs={'course_key_string': str(course_key)})
export_url = reverse('export_handler', kwargs={'course_key_string': str(course_key)})
settings_url = reverse('settings_handler', kwargs={'course_key_string': str(course_key)})
grading_url = reverse('grading_handler', kwargs={'course_key_string': str(course_key)})
advanced_settings_url = reverse('advanced_settings_handler', kwargs={'course_key_string': str(course_key)})
tabs_url = reverse('tabs_handler', kwargs={'course_key_string': str(course_key)})
certificates_url = ''
if settings.FEATURES.get("CERTIFICATES_HTML_VIEW") and context_course.cert_html_view_enabled:
certificates_url = reverse('certificates_list_handler', kwargs={'course_key_string': six.text_type(course_key)})
checklists_url = reverse('checklists_handler', kwargs={'course_key_string': six.text_type(course_key)})
certificates_url = reverse('certificates_list_handler', kwargs={'course_key_string': str(course_key)})
checklists_url = reverse('checklists_handler', kwargs={'course_key_string': str(course_key)})
pages_and_resources_mfe_enabled = ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND.is_enabled(context_course.id)
studio_home_mfe_enabled = toggles.use_new_home_page()
course_outline_mfe_enabled = toggles.use_new_course_outline_page(context_course.id)
@@ -192,7 +191,7 @@
</li>
% endif
<li class="nav-item nav-course-settings-group-configurations">
<a href="${reverse('group_configurations_list_handler', kwargs={'course_key_string': six.text_type(course_key)})}">${_("Group Configurations")}</a>
<a href="${reverse('group_configurations_list_handler', kwargs={'course_key_string': str(course_key)})}">${_("Group Configurations")}</a>
</li>
% if mfe_proctored_exam_settings_url:
<li class="nav-item nav-course-settings-exams">
@@ -251,7 +250,7 @@
% endif
% if toggles.EXPORT_GIT.is_enabled() and context_course.giturl:
<li class="nav-item nav-course-tools-export-git">
<a href="${reverse('export_git', kwargs=dict(course_key_string=six.text_type(course_key)))}">${_("Export to Git")}</a>
<a href="${reverse('export_git', kwargs=dict(course_key_string=str(course_key)))}">${_("Export to Git")}</a>
</li>
% endif
<li class="nav-item nav-course-tools-checklists">
@@ -266,10 +265,10 @@
% elif context_library:
<%
library_key = context_library.location.course_key
index_url = reverse('library_handler', kwargs={'library_key_string': six.text_type(library_key)})
import_url = reverse('import_handler', kwargs={'course_key_string': six.text_type(library_key)})
lib_users_url = reverse('manage_library_users', kwargs={'library_key_string': six.text_type(library_key)})
export_url = reverse('export_handler', kwargs={'course_key_string': six.text_type(library_key)})
index_url = reverse('library_handler', kwargs={'library_key_string': str(library_key)})
import_url = reverse('import_handler', kwargs={'course_key_string': str(library_key)})
lib_users_url = reverse('manage_library_users', kwargs={'library_key_string': str(library_key)})
export_url = reverse('export_handler', kwargs={'course_key_string': str(library_key)})
%>
<h2 class="info-course">
<span class="sr">${_("Current Library:")}</span>

View File

@@ -5,9 +5,8 @@
import hashlib
import copy
import json
from six import text_type
from xmodule.modulestore import EdxJSONEncoder
hlskey = hashlib.md5(text_type(module.location).encode('utf-8')).hexdigest()
hlskey = hashlib.md5(str(module.location).encode('utf-8')).hexdigest()
%>
## js templates

View File

@@ -2,8 +2,7 @@
<%
import hashlib
from openedx.core.djangolib.js_utils import js_escaped_string
from six import text_type
hlskey = hashlib.md5(text_type(module.location).encode('utf-8')).hexdigest()
hlskey = hashlib.md5(str(module.location).encode('utf-8')).hexdigest()
%>
<section id="hls-modal-${hlskey}" class="upload-modal modal" style="overflow:scroll; background:#ddd; padding: 10px 0;box-shadow: 0 0 5px 0 #555;" >

View File

@@ -1,14 +1,11 @@
"""
Set up lookup paths for mako templates.
"""
import contextlib
import hashlib
import os
import pkg_resources
import six
from django.conf import settings
from mako.exceptions import TopLevelLookupException
from mako.lookup import TemplateLookup
@@ -54,7 +51,7 @@ class DynamicTemplateLookup(TemplateLookup):
# and "foo.html.py" in the module directory has no way to know that.
# Update the module_directory argument to point to a directory
# specifically for this lookup path.
unique = hashlib.md5(six.b(":".join(str(d) for d in self.directories))).hexdigest()
unique = hashlib.md5((":".join(str(d) for d in self.directories)).encode()).hexdigest()
self.template_args['module_directory'] = os.path.join(self.__original_module_directory, unique)
# Also clear the internal caches. Ick.

View File

@@ -1392,4 +1392,4 @@ class RevokeSubscriptionsVerifiedAccessViewTest(ModuleStoreTestCase):
assert response.status_code == 204
mock_task.assert_called_once_with(args=([str(course_entitlement.uuid)],
[str(enrollment.course_id)],
self.user.id))
self.user.username))

View File

@@ -4,6 +4,7 @@ Views for the Entitlements v1 API.
import logging
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.http import HttpResponseBadRequest
@@ -42,6 +43,7 @@ from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticati
from openedx.core.djangoapps.credentials.utils import get_courses_completion_status
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
User = get_user_model()
log = logging.getLogger(__name__)
@@ -552,36 +554,38 @@ class SubscriptionsRevokeVerifiedAccessView(APIView):
course completion status.
"""
entitled_course_ids = []
user = User.objects.get(id=user_id)
username = user.username
for course_entitlement in course_entitlements:
if course_entitlement.enrollment_course_run is not None:
entitled_course_ids.append(str(course_entitlement.enrollment_course_run.course_id))
log.info('B2C_SUBSCRIPTIONS: Getting course completion status for user %s and entitled_course_ids %s',
user_id,
log.info('B2C_SUBSCRIPTIONS: Getting course completion status for user [%s] and entitled_course_ids %s',
username,
entitled_course_ids)
awarded_cert_course_ids, is_exception = get_courses_completion_status(user_id, entitled_course_ids)
awarded_cert_course_ids, is_exception = get_courses_completion_status(username, entitled_course_ids)
log.info('B2C_SUBSCRIPTIONS: Got course completion response %s for user %s and entitled_course_ids %s',
log.info('B2C_SUBSCRIPTIONS: Got course completion response %s for user [%s] and entitled_course_ids %s',
awarded_cert_course_ids,
user_id,
username,
entitled_course_ids)
if is_exception:
# Trigger the retry task asynchronously
log.exception('B2C_SUBSCRIPTIONS: Exception occurred while getting course completion status for user %s '
'and entitled_course_ids %s',
user_id,
username,
entitled_course_ids)
retry_revoke_subscriptions_verified_access.apply_async(args=(revocable_entitlement_uuids,
entitled_course_ids,
user_id))
username))
return
log.info('B2C_SUBSCRIPTIONS: Starting revoke_entitlements_and_downgrade_courses_to_audit for user %s and '
log.info('B2C_SUBSCRIPTIONS: Starting revoke_entitlements_and_downgrade_courses_to_audit for user [%s] and '
'awarded_cert_course_ids %s and revocable_entitlement_uuids %s',
user_id,
username,
awarded_cert_course_ids,
revocable_entitlement_uuids)
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, user_id, awarded_cert_course_ids,
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, username, awarded_cert_course_ids,
revocable_entitlement_uuids)
def post(self, request):

View File

@@ -157,7 +157,7 @@ def expire_and_create_entitlements(self, entitlement_ids, support_username):
@shared_task(bind=True)
def retry_revoke_subscriptions_verified_access(self, revocable_entitlement_uuids, entitled_course_ids, user_id):
def retry_revoke_subscriptions_verified_access(self, revocable_entitlement_uuids, entitled_course_ids, username):
"""
Task to process course access revoke and move to audit.
This is called only if call to get_courses_completion_status fails due to any exception.
@@ -165,7 +165,7 @@ def retry_revoke_subscriptions_verified_access(self, revocable_entitlement_uuids
course_entitlements = CourseEntitlement.objects.filter(uuid__in=revocable_entitlement_uuids)
course_entitlements = course_entitlements.select_related('user').select_related('enrollment_course_run')
if course_entitlements.exists():
awarded_cert_course_ids, is_exception = get_courses_completion_status(user_id, entitled_course_ids)
awarded_cert_course_ids, is_exception = get_courses_completion_status(username, entitled_course_ids)
if is_exception:
try:
countdown = 2 ** self.request.retries
@@ -173,20 +173,20 @@ def retry_revoke_subscriptions_verified_access(self, revocable_entitlement_uuids
except MaxRetriesExceededError:
log.exception(
'B2C_SUBSCRIPTIONS: Failed to process retry_revoke_subscriptions_verified_access '
'for user_id %s and entitlement_uuids %s',
user_id,
'for user [%s] and entitlement_uuids %s',
username,
revocable_entitlement_uuids
)
return
log.info('B2C_SUBSCRIPTIONS: Starting revoke_entitlements_and_downgrade_courses_to_audit for user %s and '
log.info('B2C_SUBSCRIPTIONS: Starting revoke_entitlements_and_downgrade_courses_to_audit for user [%s] and '
'awarded_cert_course_ids %s and revocable_entitlement_uuids %s from retry task',
user_id,
username,
awarded_cert_course_ids,
revocable_entitlement_uuids)
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, user_id, awarded_cert_course_ids,
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, username, awarded_cert_course_ids,
revocable_entitlement_uuids)
else:
log.info('B2C_SUBSCRIPTIONS: Entitlements not found for the provided entitlements uuids %s '
'for user_id %s duing the retry_revoke_subscriptions_verified_access task',
'for user [%s] duing the retry_revoke_subscriptions_verified_access task',
revocable_entitlement_uuids,
user_id)
username)

View File

@@ -61,7 +61,7 @@ def is_course_run_entitlement_fulfillable(
return course_overview.start and can_upgrade and (is_enrolled or can_enroll)
def revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, user_id, awarded_cert_course_ids,
def revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, username, awarded_cert_course_ids,
revocable_entitlement_uuids):
"""
This method expires the entitlements for provided course_entitlements and also moves the enrollments
@@ -69,8 +69,8 @@ def revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, user
"""
log.info('B2C_SUBSCRIPTIONS: Starting revoke_entitlements_and_downgrade_courses_to_audit for '
'user: %s, course_entitlements_uuids: %s, awarded_cert_course_ids: %s',
user_id,
'user: [%s], course_entitlements_uuids: %s, awarded_cert_course_ids: %s',
username,
revocable_entitlement_uuids,
awarded_cert_course_ids)
for course_entitlement in course_entitlements:
@@ -87,11 +87,11 @@ def revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, user
course_entitlement.expire_entitlement()
update_enrollment(username, str(course_id), CourseMode.AUDIT, include_expired=True)
else:
log.warning('B2C_SUBSCRIPTIONS: Enrollment mode mismatch for user_id: %s and course_id: %s',
user_id,
log.warning('B2C_SUBSCRIPTIONS: Enrollment mode mismatch for user: %s and course_id: %s',
username,
course_id)
log.info('B2C_SUBSCRIPTIONS: Completed revoke_entitlements_and_downgrade_courses_to_audit for '
'user: %s, course_entitlements_uuids: %s, awarded_cert_course_ids: %s',
user_id,
'user: [%s], course_entitlements_uuids: %s, awarded_cert_course_ids: %s',
username,
revocable_entitlement_uuids,
awarded_cert_course_ids)

View File

@@ -3,7 +3,6 @@
from tempfile import NamedTemporaryFile
import pytest
import six
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.management.base import CommandError
@@ -50,7 +49,7 @@ class BulkChangeEnrollmentCSVTests(SharedModuleStoreTestCase):
"""Write a test csv file with the lines provided"""
csv.write(b"course_id,user,mode,\n")
for line in lines:
csv.write(six.b(line))
csv.write(line.encode())
csv.seek(0)
return csv

View File

@@ -2,7 +2,6 @@
from tempfile import NamedTemporaryFile
import six
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from testfixtures import LogCapture
@@ -48,7 +47,7 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
"""Write a test csv file with the lines provided"""
csv.write(b"username,course_id\n")
for line in lines:
csv.write(six.b(line))
csv.write(line.encode())
csv.seek(0)
return csv

View File

@@ -4,7 +4,6 @@ Test cases for recover account management command
import re
from tempfile import NamedTemporaryFile
import pytest
import six
from django.core import mail
from django.conf import settings
@@ -40,7 +39,7 @@ class RecoverAccountTests(TestCase):
"""Write a test csv file with the lines provided"""
csv.write(b"username,current_email,desired_email\n")
for line in lines:
csv.write(six.b(line))
csv.write(line.encode())
csv.seek(0)
return csv

View File

@@ -4,7 +4,6 @@ from tempfile import NamedTemporaryFile
from unittest.mock import patch
import pytest
import six
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
@@ -34,7 +33,7 @@ class UnsubscribeUserEmailTests(TestCase):
csv.write(b"email\n")
for line in lines:
csv.write(six.b(line))
csv.write(line.encode())
csv.seek(0)
return csv

View File

@@ -4,7 +4,6 @@ Tests of student.roles
import ddt
import six
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
@@ -70,7 +69,7 @@ class RolesTestCase(TestCase):
f'Student has premature access to {self.course_key}'
CourseStaffRole(self.course_key).add_users(self.student)
assert CourseStaffRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key)}"
f"Student doesn't have access to {str(self.course_key)}"
# remove access and confirm
CourseStaffRole(self.course_key).remove_users(self.student)
@@ -85,7 +84,7 @@ class RolesTestCase(TestCase):
f'Student has premature access to {self.course_key.org}'
OrgStaffRole(self.course_key.org).add_users(self.student)
assert OrgStaffRole(self.course_key.org).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key.org)}"
f"Student doesn't have access to {str(self.course_key.org)}"
# remove access and confirm
OrgStaffRole(self.course_key.org).remove_users(self.student)
@@ -101,16 +100,16 @@ class RolesTestCase(TestCase):
OrgInstructorRole(self.course_key.org).add_users(self.student)
CourseInstructorRole(self.course_key).add_users(self.student)
assert OrgInstructorRole(self.course_key.org).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key.org)}"
f"Student doesn't have access to {str(self.course_key.org)}"
assert CourseInstructorRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key)}"
f"Student doesn't have access to {str(self.course_key)}"
# remove access and confirm
OrgInstructorRole(self.course_key.org).remove_users(self.student)
assert not OrgInstructorRole(self.course_key.org).has_user(self.student), \
f'Student still has access to {self.course_key.org}'
assert CourseInstructorRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key)}"
f"Student doesn't have access to {str(self.course_key)}"
# ok now keep org role and get rid of course one
OrgInstructorRole(self.course_key.org).add_users(self.student)
@@ -118,7 +117,7 @@ class RolesTestCase(TestCase):
assert OrgInstructorRole(self.course_key.org).has_user(self.student), \
f'Student lost has access to {self.course_key.org}'
assert not CourseInstructorRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {six.text_type(self.course_key)}"
f"Student doesn't have access to {str(self.course_key)}"
def test_get_user_for_role(self):
"""

View File

@@ -202,7 +202,7 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler):
self.end_headers()
if content is not None:
if not six.PY2 and isinstance(content, str):
if isinstance(content, str):
content = content.encode('utf-8')
self.wfile.write(content)

View File

@@ -5,7 +5,6 @@ import logging
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, LearningContextKey
from six import text_type # lint-amnesty, pylint: disable=unused-import
from openedx.core.lib.request_utils import COURSE_REGEX

View File

@@ -114,6 +114,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');
}
});
};
</script>

View File

@@ -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',
}

View File

@@ -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</a>.
"""
""",
}

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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')),
]

View File

@@ -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

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',
),
]

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')),
]

View File

@@ -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},

View File

@@ -96,7 +96,7 @@ class TestModelStrings(SharedModuleStoreTestCase):
"<CourseTeamMembership id=1 user_id=1 team_id=1>"
)
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"
)

View File

@@ -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 ##############

View File

@@ -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'

View File

@@ -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

View File

@@ -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,

View File

@@ -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();

View File

@@ -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,
});
}

View File

@@ -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));
}
}

View File

@@ -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("<a href={url}>{url_name}</a>").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:

View File

@@ -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"/>
<article class="course" id="${course.id}" role="region" aria-label="${course.display_name_with_default}">
<a href="${reverse('about_course', args=[text_type(course.id)])}">
<a href="${reverse('about_course', args=[str(course.id)])}">
<header class="course-image">
<div class="cover-image">
<img src="${course.course_image_url}" alt="${course.display_name_with_default} ${course.display_number_with_default}" />

View File

@@ -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
<li class="important-dates-item">
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<p class="important-dates-item-title">${_("Classes Start")}</p>
% if isinstance(course_start_date, string_types):
% if isinstance(course_start_date, str):
<span class="important-dates-item-text start-date">${course_start_date}</span>
% else:
<%
@@ -191,7 +188,7 @@ from six import string_types
<li class="important-dates-item">
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<p class="important-dates-item-title">${_("Classes End")}</p>
% if isinstance(course_end_date, string_types):
% if isinstance(course_end_date, str):
<span class="important-dates-item-text final-date">${course_end_date}</span>
% 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'])]) %>
<li class="prerequisite-course important-dates-item">
<span class="icon fa fa-list-ul" aria-hidden="true"></span>
<p class="important-dates-item-title">${_("Prerequisites")}</p>

View File

@@ -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
%>
<header>
@@ -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

View File

@@ -3,7 +3,6 @@
<%namespace name='static' file='/static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%!
import six
import waffle
from django.conf import settings
@@ -224,13 +223,13 @@ ${HTML(fragment.foot_html())}
% endif
% if chapter:
<span class="nav-item nav-item-chapter" data-course-position="${course.position}" data-chapter-position="${chapter.position}">
<a href="${course_url}#${six.text_type(chapter.location)}">${chapter.display_name_with_default}</a>
<a href="${course_url}#${str(chapter.location)}">${chapter.display_name_with_default}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
% endif
% if section:
<span class="nav-item nav-item-section">
<a href="${course_url}#${six.text_type(section.location)}">${section.display_name_with_default}</a>
<a href="${course_url}#${str(section.location)}">${section.display_name_with_default}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
% endif

View File

@@ -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:
<tr>
<td>
<a href="${reverse('student_progress', kwargs=dict(course_id=text_type(course_id), student_id=student['id']))}">${student['username']}</a>
<a href="${reverse('student_progress', kwargs=dict(course_id=str(course_id), student_id=student['id']))}">${student['username']}</a>
</td>
</tr>
%endfor

View File

@@ -1,8 +1,5 @@
## xss-lint: disable=mako-missing-default
<%namespace name='static' file='/static_content.html'/>
<%!
import six
%>
<script type="text/javascript" src="${static.url('js/vendor/jquery.leanModal.js')}"></script>
<script type="text/javascript" src="${static.url('js/staff_debug_actions.js')}"></script>
@@ -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;
}

View File

@@ -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]
%>

View File

@@ -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
</span>
% else:
% if isinstance(course_date, six.string_types):
% if isinstance(course_date, str):
<span class="info-date-block">${container_string.format(date=course_date)}</span>
% elif course_date is not None:
<span class="info-date-block localized-datetime" data-language="${user_language}" data-timezone="${user_timezone}" data-datetime="${course_date.strftime('%Y-%m-%dT%H:%M:%S%z')}" data-format=${format} data-string="${container_string}"></span>
@@ -272,7 +271,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
<ul class="actions-dropdown-list" id="actions-dropdown-list-${dashboard_index}" aria-label="${_('Available Actions')}" role="menu">
% if can_unenroll:
<li class="actions-item" id="actions-item-unenroll-${dashboard_index}" role="menuitem">
<% course_refund_url = reverse('course_run_refund_status', args=[six.text_type(course_overview.id)]) %>
<% course_refund_url = reverse('course_run_refund_status', args=[str(course_overview.id)]) %>
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal"
data-course-id="${course_overview.id}"
data-course-number="${course_overview.number}"
@@ -419,7 +418,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
% if course_requirements:
## Multiple pre-requisite courses are not supported on frontend that's why we are pulling first element
<% prc_target = reverse('about_course', args=[six.text_type(course_requirements['courses'][0]['key'])]) %>
<% prc_target = reverse('about_course', args=[str(course_requirements['courses'][0]['key'])]) %>
<div class="prerequisites">
<p class="tip">
${Text(_("You must successfully complete {link_start}{prc_display}{link_end} before you begin this course.")).format(

View File

@@ -1,7 +1,6 @@
<%page args="course_overview, entitlement, dashboard_index, can_refund_entitlement, show_email_settings" expression_filter="h"/>
<%!
import six
from django.utils.translation import gettext as _
from django.urls import reverse
%>
@@ -39,7 +38,7 @@ dropdown_btn_id = "entitlement-actions-dropdown-btn-{}".format(dashboard_index)
data-dropdown-button-selector="#${dropdown_btn_id}"
data-course-name="${course_overview.display_name_with_default}"
data-course-number="${course_overview.number}"
data-entitlement-api-endpoint="${reverse('entitlements_api:v1:enrollments', args=[six.text_type(entitlement.uuid)]) + '?is_refund=true'}">
data-entitlement-api-endpoint="${reverse('entitlements_api:v1:enrollments', args=[str(entitlement.uuid)]) + '?is_refund=true'}">
${_('Unenroll')}
</a>
</li>
@@ -54,7 +53,7 @@ dropdown_btn_id = "entitlement-actions-dropdown-btn-{}".format(dashboard_index)
data-course-id="${course_overview.id}"
data-course-number="${course_overview.number}"
data-dashboard-index="${dashboard_index}"
data-optout="${six.text_type(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
data-optout="${str(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
</li>
% endif
</ul>

View File

@@ -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
%>

View File

@@ -54,7 +54,8 @@ from openedx.core.djangolib.markup import HTML
<script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qubit.js')}"></script>
<%static:js group='module-descriptor-js'/>
<%static:webpack entry='HtmlBlockEditor'/>
<link rel="stylesheet" href="${static.url('css/HtmlBlockEditor.css')}">
<%static:js group='instructor_dash'/>
<%static:js group='application'/>

View File

@@ -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}
});

View File

@@ -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},

View File

@@ -8,10 +8,13 @@
<p class="advertise-message">
<%- StringUtils.interpolate(
gettext('Now available for many popular programs, affordable monthly subscription pricing can help you manage your budget more effectively. Subscriptions start at {minSubscriptionPrice}/month USD per program, after a 7-day full access free trial. Cancel at any time.'),
{ minSubscriptionPrice, trialLength }
{
minSubscriptionPrice: minimum_price,
trialLength: trial_length,
}
) %>
</p>
<a href="<%- subscriptionsMarketingUrl %>" class="js-subscription-upsell-cta btn-brand btn cta-primary view-button align-self-stretch">
<a href="<%- marketing_url %>" class="js-subscription-upsell-cta btn-brand btn cta-primary view-button align-self-stretch">
<span class="icon fa fa-search" aria-hidden="true"></span>
<span><%- gettext('Explore subscription options') %></span>
</a>

View File

@@ -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')
%>
<link rel="stylesheet" href="${six.text_type(static.url(rtl_css_file))}" type="text/css" media="all" />
<link rel="stylesheet" href="${str(static.url(rtl_css_file))}" type="text/css" media="all" />
% else:
<link rel="stylesheet" href="${static.url(self.attr.main_css)}" type="text/css" media="all" />
% endif

View File

@@ -7,7 +7,6 @@
<%!
from django.urls import reverse
from django.utils.translation import ugettext as _
from six import text_type
%>
<ol class="left list-inline nav-global">

View File

@@ -5,7 +5,6 @@
<%namespace name='static' file='static_content.html'/>
<%!
import six
from lms.djangoapps.branding import api as branding_api
from django.utils.translation import gettext as _
from django.utils.translation import get_language_bidi
@@ -45,7 +44,7 @@ from openedx.core.djangolib.markup import HTML
<%
rtl_css_file = self.attr.main_css.replace('.css', '-rtl.css')
%>
<link rel="stylesheet" href="${six.text_type(static.url(rtl_css_file))}" type="text/css" media="all" />
<link rel="stylesheet" href="${str(static.url(rtl_css_file))}" type="text/css" media="all" />
% else:
<link rel="stylesheet" href="${static.url(self.attr.main_css)}" type="text/css" media="all" />
% endif

View File

@@ -1,205 +0,0 @@
{% extends 'ace_common/edx_ace/common/base_body.html' %}
{% load django_markup %}
{% load i18n %}
{% load static %}
{% block content %}
<div style="display: flex;">
<div style="background-color: #1C8DBE;flex: 50%;">
<div style="text-align: center;">
<h1 style="color: #fff;
line-height: 40px;
font-size: 36px;
padding-top: 110px;
padding-bottom: 24px;
padding-left: 32px;
padding-right: 32px;
min-width: 256px;
margin: 0;
text-align: left;"
>
{% filter force_escape %}
{% blocktrans %}Check out this course on edx{% endblocktrans %}
{% endfilter %}
</h1>
<div style="text-align: left;
padding-left: 32px;"
>
<a style="background: #00688D;
width: 94px;
text-decoration: none;
color: #fff;
font-weight: 500;
font-size: 14px;
line-height: 2.25rem;
height: 36px;
padding: 8px 12px;"
href="{{ lms_url|add:enroll_course_url }}"
>
{% filter force_escape %}
{% blocktrans %}Enroll now{% endblocktrans %}
{% endfilter %}
</a>
<a style="background: #03c7e8;
border: 1px solid #fff;
margin-left: 8px;
width: 107px;
text-decoration: none;
color: #fff;
font-weight: 500;
font-size: 14px;
line-height: 2.25rem;
height: 36px;
padding: 8px 12px;"
href="{{ view_course_url }}"
>
{% filter force_escape %}
{% blocktrans %}View Course{% endblocktrans %}
{% endfilter %}
</a>
</div>
</div>
</div>
<div style="flex: 50%;">
<div style="height: 140px;min-width: 320px;background: url({{lms_url|add:course_image_url}})">
{% if partner_image_url %}
<div style="padding-top: 82px;padding-left: 24px;">
<img style="width: 116px;
height: 66px;
background: #FFFFFF;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15), 0px 1px 4px rgba(0, 0, 0, 0.15);
border-radius: 4px;
"src={{partner_image_url}} />
</div>
{% endif %}
</div>
<div style="background: #FBFAF9;">
<h2 style="color: #002121;
font-weight: bold;
font-size: 22px;
line-height: 28px;
padding-top: 24px;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 20px;
margin: 0;"
>
{{ display_name }}
</h2>
<p style="color: #707070;
font-style: normal;
font-weight: normal;
font-size: 12px;
line-height: 20px;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 20px;
margin: 0;"
>
{{ short_description }}
</p>
</div>
</div>
</div>
<div style="background-color: #F2F0EF;display: flex;color: #00262B;">
<div style="width: 33.3%;">
<div style="display: inline-block;">
<svg width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
focusable="false"
aria-hidden="true"
style="margin-bottom: 5px;margin-left: 20px;"
>
<path d="M16.24 7.76A5.974 5.974 0 0012 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0a5.99 5.99 0 00-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="currentColor"></path>
</svg>
</div>
<div style="display: inline-block;margin-left: 5px !important">
<p style="padding-top: 8px;
margin: 0;
font-weight: bold;
font-size: 14px;
line-height: 24px;"
>
{% trans "Estimated 6 weeks" as estimated_weeks_heading %}{{ estimated_weeks_heading | force_escape }}
</p>
<p style=" margin: 0;
padding-bottom: 9px;
font-size: 12px;
color: #707070;"
>
{% trans "4-6 hours per week" as estimated_weeks_msg %}{{ estimated_weeks_msg | force_escape }}
</p>
</div>
</div>
<div style="width: 33.3%;">
<div style="display: inline-block;">
<svg width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
focusable="false"
aria-hidden="true"
style="margin-bottom: 5px;"
>
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"></path>
</svg>
</div>
<div style="display: inline-block;margin-left: 5px;">
<p style="padding-top: 8px;
margin: 0;
font-weight: bold;
font-size: 14px;
line-height: 24px;"
>
{% trans "Self-paced" as self_paced_heading %}{{ self_paced_heading | force_escape }}
</p>
<p style=" margin: 0;
padding-bottom: 9px;
font-size: 12px;
color: #707070;"
>
{% trans "Progress at your own speed" as self_paced_msg %}{{ self_paced_msg | force_escape }}
</p>
</div>
</div>
<div style="width: 33.3%">
<div style="display: inline-block;">
<svg width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
focusable="false"
aria-hidden="true"
style="margin-bottom: 5px;"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.73-2.77-.01-2.2-1.9-2.96-3.66-3.42z" fill="currentColor"></path>
</svg>
</div>
<div style="display: inline-block;margin-left: 5px;">
<p style="padding-top: 8px;
margin: 0;
font-weight: bold;
font-size: 14px;
line-height: 24px;"
>
{% trans "Free" as upgrade_heading %}{{ upgrade_heading | force_escape }}
</p>
<p style=" margin: 0;
padding-bottom: 9px;
font-size: 12px;
color: #707070;"
>
{% trans "Optional upgrade available" as upgrade_msg %}{{ upgrade_msg | force_escape }}
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +0,0 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Check out this course on {{ platform_name }}.{% endblocktrans %}
{% blocktrans %}This email message was automatically sent by {{ lms_url }} {% endblocktrans %}
{{ confirm_link }}
{% endautoescape %}

View File

@@ -1 +0,0 @@
{{ platform_name }}

View File

@@ -1 +0,0 @@
{% extends 'ace_common/edx_ace/common/base_head.html' %}

View File

@@ -1,4 +0,0 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans trimmed %} {{ display_name }} {% endblocktrans %}
{% endautoescape %}

View File

@@ -4,7 +4,6 @@
from django.utils.translation import gettext as _
from openedx.core.djangolib.markup import HTML
from openedx.core.djangolib.js_utils import js_escaped_string
from six import text_type
%>
## The JS for this is defined in xqa_interface.html
@@ -100,7 +99,7 @@ ${block_content | n, decode.utf8}
<div class="staff_info" style="display:block">
is_released = ${is_released}
location = ${text_type(location)}
location = ${str(location)}
<table summary="${_('Module Fields')}">
<tr><th>${_('Module Fields')}</th></tr>

View File

@@ -1,7 +1,6 @@
## mako
<%page expression_filter="h"/>
<%!
import six
from django.urls import reverse
from django.utils.translation import gettext as _
%>
@@ -18,7 +17,7 @@ ${_("Student Support")}
<h1>${_("Student Support")}</h1>
<ul>
% for url in urls:
<li><a href="${url["url"]}">${six.text_type(url["name"])}</a>: ${six.text_type(url["description"])}</li>
<li><a href="${url["url"]}">${str(url["name"])}</a>: ${str(url["description"])}</li>
% endfor
</ul>
</section>

View File

@@ -2,7 +2,6 @@
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
import six
from django.utils.translation import gettext as _
from django.urls import reverse
from django.utils import html
@@ -24,7 +23,7 @@ from openedx.core.djangolib.markup import Text, HTML
<input type="hidden" name="_redirect_url" value="${redirect_url}" />
% if course:
<input type="hidden" name="course_id" value="${six.text_type(course.id)}" />
<input type="hidden" name="course_id" value="${str(course.id)}" />
<div class="header-survey">
<h4 class="course-info">

View File

@@ -40,7 +40,6 @@
</section>
{% javascript 'application' %}
{% javascript 'module-js' %}
{% with mathjax_mode='wiki' %}
{% include "mathjax_include.html" %}
{% endwith %}

View File

@@ -1031,12 +1031,6 @@ if getattr(settings, 'PROVIDER_STATES_URL', None):
)
]
# save_for_later API urls
if settings.ENABLE_SAVE_FOR_LATER:
urlpatterns += [
path('', include('lms.djangoapps.save_for_later.urls')),
]
# Enhanced Staff Grader (ESG) URLs
urlpatterns += [
path('api/ora_staff_grader/', include('lms.djangoapps.ora_staff_grader.urls', 'ora-staff-grader')),

Some files were not shown because too many files have changed in this diff Show More