Merge branch 'master' into dwong/updgrade-django-5-get-storages
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Test the enable/disable discussions for all units API endpoint.
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
|
||||
|
||||
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class BulkEnableDisableDiscussionsTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Test the enable/disable discussions for all units API endpoint.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory(is_staff=True, is_superuser=True)
|
||||
self.user.set_password(self.user_password)
|
||||
self.user.save()
|
||||
|
||||
self.course_key = CourseKey.from_string("course-v1:edx+TestX+2025")
|
||||
|
||||
self.url = reverse('bulk_enable_disable_discussions', args=[str(self.course_key)])
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password=self.user_password)
|
||||
|
||||
# Create a test course
|
||||
self.course = CourseFactory.create(
|
||||
org=self.course_key.org,
|
||||
course=self.course_key.course,
|
||||
run=self.course_key.run,
|
||||
default_store=ModuleStoreEnum.Type.split,
|
||||
display_name="EnableDisableDiscussionsTestCase Course",
|
||||
)
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
section = BlockFactory.create(
|
||||
parent=self.course,
|
||||
category='chapter',
|
||||
display_name="Generated Section",
|
||||
)
|
||||
sequence = BlockFactory.create(
|
||||
parent=section,
|
||||
category='sequential',
|
||||
display_name="Generated Sequence",
|
||||
)
|
||||
unit1 = BlockFactory.create(
|
||||
parent=sequence,
|
||||
category='vertical',
|
||||
display_name="Unit in Section1",
|
||||
discussion_enabled=True,
|
||||
)
|
||||
unit2 = BlockFactory.create(
|
||||
parent=sequence,
|
||||
category='vertical',
|
||||
display_name="Unit in Section2",
|
||||
discussion_enabled=True,
|
||||
)
|
||||
|
||||
def test_disable_discussions_for_all_units(self):
|
||||
"""
|
||||
Test that the API successfully disables discussions for all units.
|
||||
"""
|
||||
self.enable_disable_discussions_for_all_units(False)
|
||||
|
||||
def test_enable_discussions_for_all_units(self):
|
||||
"""
|
||||
Test that the API successfully enables discussions for all units.
|
||||
"""
|
||||
self.enable_disable_discussions_for_all_units(True)
|
||||
|
||||
def enable_disable_discussions_for_all_units(self, is_enabled):
|
||||
"""
|
||||
Test that the API successfully enables/disables discussions for all units.
|
||||
"""
|
||||
data = {
|
||||
"discussion_enabled": is_enabled
|
||||
}
|
||||
response = self.client.put(self.url, data=json.dumps(data), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = response.json()
|
||||
print(response_data)
|
||||
self.assertEqual(response_data['updated_and_republished'], 0 if is_enabled else 2)
|
||||
|
||||
# Check that all verticals now have discussion_enabled set to the expected value
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
verticals = self.store.get_items(self.course_key, qualifiers={'block_type': 'vertical'})
|
||||
for vertical in verticals:
|
||||
self.assertEqual(vertical.discussion_enabled, is_enabled)
|
||||
|
||||
def test_permission_denied_for_non_staff(self):
|
||||
"""
|
||||
Test that non-staff users are denied access to the API.
|
||||
"""
|
||||
# Create a non-staff user
|
||||
non_staff_user = UserFactory(is_staff=False, is_superuser=False)
|
||||
non_staff_user.set_password(self.user_password)
|
||||
non_staff_user.save()
|
||||
|
||||
# Create a new client for the non-staff user
|
||||
non_staff_client = AjaxEnabledTestClient()
|
||||
non_staff_client.login(username=non_staff_user.username, password=self.user_password)
|
||||
|
||||
response = non_staff_client.put(self.url, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_badrequest_for_empty_request_body(self):
|
||||
"""
|
||||
Test that the API returns a 400 for an empty request body.
|
||||
"""
|
||||
response = self.client.put(self.url, data=json.dumps({}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
@@ -135,7 +135,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_listing',
|
||||
'course_notifications_handler',
|
||||
'textbooks_list_handler', 'textbooks_detail_handler',
|
||||
'group_configurations_list_handler', 'group_configurations_detail_handler',
|
||||
'get_course_and_check_access']
|
||||
'get_course_and_check_access', 'bulk_enable_disable_discussions']
|
||||
|
||||
|
||||
class AccessListFallback(Exception):
|
||||
@@ -1710,6 +1710,62 @@ def group_configurations_detail_handler(request, course_key_string, group_config
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["PUT"])
|
||||
def bulk_enable_disable_discussions(request, course_key_string):
|
||||
"""
|
||||
API endpoint to enable/disable discussions for all verticals in the course and republish them.
|
||||
|
||||
PUT
|
||||
json: enable/disable discussions for all units and republish
|
||||
"""
|
||||
try:
|
||||
# Validate the course key
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
except InvalidKeyError:
|
||||
return JsonResponseBadRequest({"error": "Invalid course key format"})
|
||||
|
||||
user = request.user
|
||||
|
||||
# check that logged in user has permissions to update this course
|
||||
if not has_studio_write_access(user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
return JsonResponseBadRequest({"error": "Only supports json requests"})
|
||||
|
||||
if 'discussion_enabled' not in request.json:
|
||||
return JsonResponseBadRequest({"error": "Missing 'discussion_enabled' field in request body"})
|
||||
discussion_enabled = request.json['discussion_enabled']
|
||||
log.info(
|
||||
"User %s is attempting to %s discussions for all verticals in course %s",
|
||||
user.username,
|
||||
"enable" if discussion_enabled else "disable",
|
||||
course_key
|
||||
)
|
||||
|
||||
if request.method == 'PUT':
|
||||
try:
|
||||
store = modulestore()
|
||||
changed = 0
|
||||
with store.bulk_operations(course_key):
|
||||
verticals = store.get_items(course_key, qualifiers={'block_type': 'vertical'})
|
||||
for vertical in verticals:
|
||||
if vertical.discussion_enabled != discussion_enabled:
|
||||
vertical.discussion_enabled = discussion_enabled
|
||||
store.update_item(vertical, user.id)
|
||||
|
||||
if store.has_published_version(vertical):
|
||||
store.publish(vertical.location, user.id)
|
||||
changed += 1
|
||||
return JsonResponse({"updated_and_republished": changed})
|
||||
except Exception as e: # lint-amnesty, pylint: disable=broad-except
|
||||
log.exception("Exception occurred while enabling/disabling discussion: %s", str(e))
|
||||
return JsonResponseBadRequest({"error": str(e)})
|
||||
|
||||
|
||||
def are_content_experiments_enabled(course):
|
||||
"""
|
||||
Returns True if content experiments have been enabled for the course.
|
||||
|
||||
@@ -211,12 +211,6 @@ upstream_info = UpstreamLink.try_get_for_block(xblock, log_error=False)
|
||||
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
|
||||
</li>
|
||||
% endif
|
||||
% elif not show_inline:
|
||||
<li class="action-item action-edit action-edit-view-only">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<span class="action-button-text">${_("Details")}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
</ul>
|
||||
|
||||
@@ -201,6 +201,9 @@ urlpatterns = oauth2_urlpatterns + [
|
||||
path('accessibility', contentstore_views.accessibility, name='accessibility'),
|
||||
re_path(fr'api/youtube/courses/{COURSELIKE_KEY_PATTERN}/edx-video-ids$',
|
||||
contentstore_views.get_course_youtube_edx_videos_ids, name='youtube_edx_video_ids'),
|
||||
re_path(fr'^api/courses/{settings.COURSE_KEY_PATTERN}/bulk_enable_disable_discussions$',
|
||||
contentstore_views.bulk_enable_disable_discussions,
|
||||
name='bulk_enable_disable_discussions'),
|
||||
]
|
||||
|
||||
if not settings.DISABLE_DEPRECATED_SIGNIN_URL:
|
||||
|
||||
@@ -696,10 +696,6 @@ def user_profile_pre_save_callback(sender, **kwargs):
|
||||
"""
|
||||
user_profile = kwargs['instance']
|
||||
|
||||
# Remove profile images for users who require parental consent
|
||||
if user_profile.requires_parental_consent() and user_profile.has_profile_image:
|
||||
user_profile.profile_image_uploaded_at = None
|
||||
|
||||
# Cache "old" field values on the model instance so that they can be
|
||||
# retrieved in the post_save callback when we emit an event with new and
|
||||
# old field values.
|
||||
|
||||
@@ -65,23 +65,3 @@ class ProfileParentalControlsTest(TestCase):
|
||||
self.set_year_of_birth(current_year - 14)
|
||||
assert not self.profile.requires_parental_consent()
|
||||
assert not self.profile.requires_parental_consent(year=current_year)
|
||||
|
||||
def test_profile_image(self):
|
||||
"""Verify that a profile's image obeys parental controls."""
|
||||
|
||||
# Verify that an image cannot be set for a user with no year of birth set
|
||||
self.profile.profile_image_uploaded_at = now()
|
||||
self.profile.save()
|
||||
assert not self.profile.has_profile_image
|
||||
|
||||
# Verify that an image can be set for an adult user
|
||||
current_year = now().year
|
||||
self.set_year_of_birth(current_year - 20)
|
||||
self.profile.profile_image_uploaded_at = now()
|
||||
self.profile.save()
|
||||
assert self.profile.has_profile_image
|
||||
|
||||
# verify that a user's profile image is removed when they switch to requiring parental controls
|
||||
self.set_year_of_birth(current_year - 10)
|
||||
self.profile.save()
|
||||
assert not self.profile.has_profile_image
|
||||
|
||||
@@ -74,7 +74,7 @@ of Content Groups to Cohorts.
|
||||
|
||||
While this might sound a little cumbersome, it actually allows for a cleaner
|
||||
separation of concerns. Content Groups describe what the content is: restricted
|
||||
copyright, advanced material, labratory exercises, etc. Cohorts describe who is
|
||||
copyright, advanced material, laboratory exercises, etc. Cohorts describe who is
|
||||
consuming that material: on campus students, alumni, the general MOOC audience,
|
||||
etc. The Content Group is an Authoring decision based on the properties of the
|
||||
content itself. The Cohort mapping is a policy decision about the Learning
|
||||
|
||||
@@ -28,6 +28,7 @@ from xmodule.capa.tests.response_xml_factory import (
|
||||
OptionResponseXMLFactory,
|
||||
SchematicResponseXMLFactory
|
||||
)
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentModule
|
||||
@@ -810,6 +811,7 @@ class ProblemWithUploadedFilesTest(TestSubmittingProblems):
|
||||
self.assertEqual(list(kwargs['files'].keys()), filenames.split())
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class TestPythonGradedResponse(TestSubmittingProblems):
|
||||
"""
|
||||
Check that we can submit a schematic and custom response, and it answers properly.
|
||||
|
||||
@@ -2742,22 +2742,35 @@ class ExportOra2DataView(DeveloperErrorViewMixin, APIView):
|
||||
return JsonResponse({"error": str(err)}, status=400)
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_course_permission(permissions.CAN_RESEARCH)
|
||||
@common_exceptions_400
|
||||
def export_ora2_summary(request, course_id):
|
||||
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
||||
class ExportOra2SummaryView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
Pushes a Celery task which will aggregate a summary students' progress in ora2 tasks for a course into a .csv
|
||||
Pushes a Celery task which will aggregate a summary of students' progress in ora2 tasks for a course into a .csv
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
report_type = _('ORA summary')
|
||||
task_api.submit_export_ora2_summary(request, course_key)
|
||||
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
|
||||
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
|
||||
permission_name = permissions.CAN_RESEARCH
|
||||
|
||||
return JsonResponse({"status": success_status})
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
|
||||
def post(self, request, course_id):
|
||||
"""
|
||||
Initiates a Celery task to generate an ORA summary report for the specified course.
|
||||
|
||||
Args:
|
||||
request: The HTTP request object
|
||||
course_id: The string representation of the course key
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message indicating the report generation has started
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
report_type = _('ORA summary')
|
||||
try:
|
||||
task_api.submit_export_ora2_summary(request, course_key)
|
||||
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
|
||||
return Response({"status": success_status})
|
||||
except (AlreadyRunningError, QueueConnectionError, AttributeError) as err:
|
||||
return JsonResponse({"error": str(err)}, status=400)
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
|
||||
@@ -67,7 +67,7 @@ urlpatterns = [
|
||||
# Reports..
|
||||
path('get_course_survey_results', api.GetCourseSurveyResults.as_view(), name='get_course_survey_results'),
|
||||
path('export_ora2_data', api.ExportOra2DataView.as_view(), name='export_ora2_data'),
|
||||
path('export_ora2_summary', api.export_ora2_summary, name='export_ora2_summary'),
|
||||
path('export_ora2_summary', api.ExportOra2SummaryView.as_view(), name='export_ora2_summary'),
|
||||
|
||||
path('export_ora2_submission_files', api.export_ora2_submission_files,
|
||||
name='export_ora2_submission_files'),
|
||||
|
||||
@@ -22,6 +22,7 @@ from django.urls import reverse
|
||||
|
||||
from xmodule.capa.responsetypes import StudentInputError
|
||||
from xmodule.capa.tests.response_xml_factory import CodeResponseXMLFactory, CustomResponseXMLFactory
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
from lms.djangoapps.courseware.model_data import StudentModule
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from lms.djangoapps.instructor_task.api import (
|
||||
@@ -71,6 +72,7 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(RATELIMIT_ENABLE=False)
|
||||
@use_unsafe_codejail()
|
||||
class TestRescoringTask(TestIntegrationTask):
|
||||
"""
|
||||
Integration-style tests for rescoring problems in a background task.
|
||||
|
||||
@@ -188,7 +188,7 @@ class Command(BaseCommand):
|
||||
return True
|
||||
|
||||
site = Site.objects.get_current()
|
||||
account_base_url = settings.ACCOUNT_MICROFRONTEND_URL.rstrip('/')
|
||||
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
|
||||
message_context = get_base_template_context(site)
|
||||
message_context.update({
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
|
||||
@@ -251,7 +251,7 @@ class IDVerificationService:
|
||||
Returns a string:
|
||||
Returns URL for IDV on Account Microfrontend
|
||||
"""
|
||||
account_base_url = settings.ACCOUNT_MICROFRONTEND_URL.rstrip('/')
|
||||
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
|
||||
location = f'{account_base_url}/id-verification'
|
||||
if course_id:
|
||||
location += f'?course_id={quote(str(course_id))}'
|
||||
|
||||
@@ -1128,7 +1128,7 @@ def results_callback(request): # lint-amnesty, pylint: disable=too-many-stateme
|
||||
log.info("[COSMO-184] Denied verification for receipt_id={receipt_id}.".format(receipt_id=receipt_id))
|
||||
|
||||
attempt.deny(json.dumps(reason), error_code=error_code)
|
||||
account_base_url = settings.ACCOUNT_MICROFRONTEND_URL.rstrip('/')
|
||||
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
|
||||
reverify_url = f'{account_base_url}/id-verification'
|
||||
verification_status_email_vars['reasons'] = reason
|
||||
verification_status_email_vars['reverify_url'] = reverify_url
|
||||
|
||||
@@ -5411,6 +5411,10 @@ NOTIFICATION_TYPE_ICONS = {}
|
||||
DEFAULT_NOTIFICATION_ICON_URL = ""
|
||||
NOTIFICATION_DIGEST_LOGO = DEFAULT_EMAIL_LOGO_URL
|
||||
|
||||
############## SELF PACED EMAIL ##############
|
||||
SELF_PACED_BANNER_URL = ""
|
||||
SELF_PACED_CLOUD_URL = ""
|
||||
|
||||
############## NUDGE EMAILS ###############
|
||||
# .. setting_name: DISABLED_ORGS_FOR_PROGRAM_NUDGE
|
||||
# .. setting_default: []
|
||||
|
||||
@@ -651,28 +651,40 @@ TOKEN_SIGNING = {
|
||||
'JWT_ISSUER': 'token-test-issuer',
|
||||
'JWT_SIGNING_ALGORITHM': 'RS512',
|
||||
'JWT_SUPPORTED_VERSION': '1.2.0',
|
||||
'JWT_PRIVATE_SIGNING_JWK': '''{
|
||||
"e": "AQAB",
|
||||
"d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"q": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
|
||||
"p": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
|
||||
"kid": "token-test-sign", "kty": "RSA"
|
||||
}''',
|
||||
'JWT_PUBLIC_SIGNING_JWK_SET': '''{
|
||||
"keys": [
|
||||
'JWT_PRIVATE_SIGNING_JWK': """
|
||||
{
|
||||
"kid": "token-test-sign",
|
||||
"kty": "RSA",
|
||||
"key_ops": [
|
||||
"sign"
|
||||
],
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"e": "AQAB",
|
||||
"d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
|
||||
"p": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
|
||||
"q": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
|
||||
"dp": "Azh08H8r2_sJuBXAzx_mQ6iZnAZQ619PnJFOXjTqnMgcaK8iSHLL2CgDIUQwteUcBphgP0uBrfWIBs5jmM8rUtVz4CcrPb5jdjhHjuu4NxmnFbPlhNoOp8OBUjPP3S-h-fPoaFjxDrUqz_zCdPVzp4S6UTkf6Hu-SiI9CFVFZ8E",
|
||||
"dq": "WQ44_KTIbIej9qnYUPMA1DoaAF8ImVDIdiOp9c79dC7FvCpN3w-lnuugrYDM1j9Tk5bRrY7-JuE6OaKQgOtajoS1BIxjYHj5xAVPD15CVevOihqeq5Zx0ZAAYmmCKRrfUe0iLx2QnIcoKH1-Azs23OXeeo6nysznZjvv9NVJv60",
|
||||
"qi": "KSWGH607H1kNG2okjYdmVdNgLxTUB-Wye9a9FNFE49UmQIOJeZYXtDzcjk8IiK3g-EU3CqBeDKVUgHvHFu4_Wj3IrIhKYizS4BeFmOcPDvylDQCmJcC9tXLQgHkxM_MEJ7iLn9FOLRshh7GPgZphXxMhezM26Cz-8r3_mACHu84"
|
||||
}
|
||||
""", # noqa: E501,
|
||||
|
||||
'JWT_PUBLIC_SIGNING_JWK_SET': """
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kid":"token-test-wrong-key",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dffgRQLD1qf5D6sprmYfWVokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"
|
||||
"kid": "token-test-sign",
|
||||
"kty": "RSA",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"e": "AQAB"
|
||||
},
|
||||
{
|
||||
"kid":"token-test-sign",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"
|
||||
"kid": "token-test-wrong-key",
|
||||
"kty": "RSA",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"e": "AQAB"
|
||||
}
|
||||
]
|
||||
}''',
|
||||
]
|
||||
}
|
||||
""", # noqa: E501
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartiti
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.course_block import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
|
||||
|
||||
INCLUDE_SCHEMES = [CohortPartitionScheme, RandomUserPartitionScheme, ]
|
||||
SCHEME_SUPPORTS_ASSIGNMENT = [RandomUserPartitionScheme, ]
|
||||
@@ -63,6 +64,6 @@ class LmsSearchFilterGenerator(SearchFilterGenerator):
|
||||
if not getattr(settings, "SEARCH_SKIP_INVITATION_ONLY_FILTERING", True):
|
||||
exclude_dictionary['invitation_only'] = True
|
||||
if not getattr(settings, "SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING", True):
|
||||
exclude_dictionary['catalog_visibility'] = 'none'
|
||||
exclude_dictionary['catalog_visibility'] = [CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE]
|
||||
|
||||
return exclude_dictionary
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import Mock, patch
|
||||
from lms.lib.courseware_search.lms_filter_generator import LmsSearchFilterGenerator
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.course_block import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
@@ -139,3 +140,9 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase):
|
||||
assert 'org' not in exclude_dictionary
|
||||
assert 'org' in field_dictionary
|
||||
assert ['TestSite3'] == field_dictionary['org']
|
||||
|
||||
@patch('django.conf.settings.SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING', False)
|
||||
def test_excludes_catalog_visibility(self):
|
||||
_, _, exclude_dictionary = LmsSearchFilterGenerator.generate_field_filters(user=self.user)
|
||||
assert 'catalog_visibility' in exclude_dictionary
|
||||
assert exclude_dictionary['catalog_visibility'] == [CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE]
|
||||
|
||||
@@ -97,7 +97,7 @@ def create_email_template_context(username):
|
||||
'channel': 'email',
|
||||
'value': False
|
||||
}
|
||||
account_base_url = settings.ACCOUNT_MICROFRONTEND_URL.rstrip('/')
|
||||
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
|
||||
return {
|
||||
"platform_name": settings.PLATFORM_NAME,
|
||||
"mailing_address": settings.CONTACT_MAILING_ADDRESS,
|
||||
|
||||
@@ -548,6 +548,8 @@ class CourseNextSectionUpdate(PrefixedDebugLoggerMixin, RecipientResolver):
|
||||
'course_id': str(course.id),
|
||||
'course_ids': [str(course.id)],
|
||||
'unsubscribe_url': unsubscribe_url,
|
||||
'self_paced_banner_url': settings.SELF_PACED_BANNER_URL,
|
||||
'self_paced_cloud_url': settings.SELF_PACED_CLOUD_URL,
|
||||
})
|
||||
template_context.update(_get_upsell_information_for_schedule(user, schedule))
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{% load django_markup %}
|
||||
{% load i18n %}
|
||||
<!-- These tags come from the ace_common djangoapp in edx ace -->
|
||||
{% load ace %}
|
||||
<!-- These tags come from the edx_ace app within the edx_ace repository -->
|
||||
{% load acetags %}
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||
|
||||
{# This is preview text that is visible in the inbox view of many email clients but not visible in the actual #}
|
||||
{# email itself. #}
|
||||
|
||||
<div lang="{{ LANGUAGE_CODE|default:"en" }}" style="
|
||||
display:none;
|
||||
font-size:1px;
|
||||
line-height:1px;
|
||||
max-height:0px;
|
||||
max-width:0px;
|
||||
opacity:0;
|
||||
overflow:hidden;
|
||||
visibility:hidden;
|
||||
">
|
||||
{% block preview_text %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% for image_src in channel.tracker_image_sources %}
|
||||
<img src="{image_src}" alt="" role="presentation" aria-hidden="true" />
|
||||
{% endfor %}
|
||||
|
||||
{% google_analytics_tracking_pixel %}
|
||||
|
||||
<div bgcolor="#F2F0EF" lang="{{ LANGUAGE_CODE|default:"en" }}" dir="{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}" style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 100%;
|
||||
">
|
||||
<!-- Hack for outlook 2010, which wants to render everything in Times New Roman -->
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td {font-family: Arial, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table role="presentation" width="600" align="center" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
|
||||
<!-- CONTENT -->
|
||||
<table class="content" role="presentation" align="center" cellpadding="0" cellspacing="0" border="0" bgcolor="#fbfaf9" width="100%"
|
||||
{% block table_style %}
|
||||
style="
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
max-width: 600px;
|
||||
"
|
||||
{% endblock %}
|
||||
>
|
||||
<tr>
|
||||
<!-- HEADER -->
|
||||
<td class="header" style="background-color: #F2F0EF;">
|
||||
{% block header %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<!-- MAIN -->
|
||||
<td class="main" bgcolor="#ffffff">
|
||||
{% block content %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<!-- FOOTER -->
|
||||
<td class="footer" style="padding: 30px; background-color: #F2F0EF;">
|
||||
{% block footer %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
|
||||
</div>
|
||||
@@ -1,6 +1,8 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_body.html' %}
|
||||
{% extends 'schedules/edx_ace/courseupdate/email/base_body.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load django_markup %}
|
||||
{% load static %}
|
||||
|
||||
{% block preview_text %}
|
||||
{% filter force_escape %}
|
||||
@@ -11,38 +13,123 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<table width="100%" style="margin-bottom: 30px;" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<style>
|
||||
.course-text{
|
||||
span{
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% if route_enabled %}
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
{% blocktrans trimmed asvar tmsg %}
|
||||
We hope you're enjoying {start_strong}{course_name}{end_strong}!
|
||||
We want to let you know what you can look forward to in week {week_num}:
|
||||
{% endblocktrans %}
|
||||
{% interpolate_html tmsg start_strong='<strong>'|safe end_strong='</strong>'|safe course_name=course_name|force_escape|safe week_num=week_num|force_escape|safe %}
|
||||
<ul>
|
||||
{% for highlight in week_highlights %}
|
||||
<li>{{ highlight }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
{% filter force_escape %}
|
||||
{% blocktrans trimmed %}
|
||||
With self-paced courses, you learn on your own schedule.
|
||||
We encourage you to spend time with the course each week.
|
||||
Your focused attention will pay off in the end!
|
||||
{% endblocktrans %}
|
||||
{% blocktrans %}This is a routed Account Activation email for {{ routed_profile_name }} ({{ routed_user_email }}): {{ routed_profile_name }}{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
<br />
|
||||
</p>
|
||||
|
||||
{% filter force_escape %}
|
||||
{% blocktrans asvar course_cta_text %}Resume your course now{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
{% include "ace_common/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text%}
|
||||
|
||||
{% include "ace_common/edx_ace/common/upsell_cta.html"%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="{{ self_paced_banner_url }}"
|
||||
style="margin-bottom: 16px; width: 600px;"
|
||||
width= "600"
|
||||
height="265"
|
||||
alt="{% trans 'Welcome to edX. It’s time for your next career move' as tmsg %}{{ tmsg | force_escape }}"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p style="margin-top: 16px; color: #1F453D; font-family: Arial; font-size: 30px; font-weight: 700; line-height: 36px; margin-left: 30px;">
|
||||
{% trans "We hope you’re enjoying Introduction to Data Science with Python!" as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
<p style="margin-top: 16px; color: #000000; font-family: Arial; font-size: 16px; font-weight: 400; line-height: 24px; margin-left: 30px;">
|
||||
{% trans "We want to let you know what you can look forward to in week two: " as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
<ul style="margin-bottom: 16px; list-style: disc;">
|
||||
<li>
|
||||
<p>
|
||||
{% trans "Learn kNN regression" as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
{% trans "Learn linear regression" as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
{% trans "Find out how to choose which model you want" as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height: 32px;">
|
||||
<td style="padding-top: 1rem;" class="course-text">
|
||||
{% filter force_escape %}
|
||||
{% blocktrans asvar course_cta_text %}Resume your course {% endblocktrans %}
|
||||
{% endfilter %}
|
||||
{% include "schedules/edx_ace/courseupdate/email/return_to_course_cta.html" with course_cta_text=course_cta_text%}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 32px; line-height: 32px; font-size: 1px;">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="background-color: #F3F1ED;">
|
||||
<td>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 540px; border-radius: 8px;" bgcolor="#F3F1ED" class="goals-engage-table" align="left">
|
||||
<!-- Row 1: Image and title -->
|
||||
<!--[if mso]>
|
||||
<tr>
|
||||
<td style="width: 400px; color: #1F453D; font-size: 16px; font-family: Arial, Helvetica, sans-serif; font-weight: 400; line-height: 24px; width: 100%;">
|
||||
<p style="padding: 20px 30px; margin-left: 30px">
|
||||
<strong>
|
||||
{% trans "Your focused attention will pay off in the end!" as tmsg %}{{ tmsg | force_escape }}
|
||||
</strong>
|
||||
{% trans "With self-paced courses, you learn on your own schedule. It’s a good idea to spend time with the course each week and check in with your goals often." as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
</td>
|
||||
<td width="120" style="width: 120px; padding: 0; margin: 0; vertical-align: top;">
|
||||
<img src="{{ self_paced_cloud_url }}"
|
||||
alt="Message Icon"
|
||||
width="120"
|
||||
height="158"
|
||||
style="display: block; border: 0; margin: 0; padding: 0; width: 120px; height: 158px; max-width: 120px !important; max-height: 158px !important;" />
|
||||
</td>
|
||||
</tr>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<tr>
|
||||
<td style="width: 400px; color: #1F453D; font-size: 16px; font-family: Arial, sans-serif; font-weight: 400; line-height: 24px;">
|
||||
<p style="padding: 20px 30px; margin: 0px;">
|
||||
<strong> {% trans "Your focused attention will pay off in the end! " as tmsg %}{{ tmsg | force_escape }} </strong>
|
||||
{% trans "With self-paced courses, you learn on your own schedule. It’s a good idea to spend time with the course each week and check in with your goals often." as tmsg %}{{ tmsg | force_escape }}
|
||||
</p>
|
||||
</td>
|
||||
<td style="padding: 0; margin: 0;" align="left" valign="top">
|
||||
<img src="{{ self_paced_cloud_url }}" alt="Message Icon" style="display: block; border: 0; margin: 0; padding: 0; width: 120px; height: 158px; margin-top: -40px;">
|
||||
</td>
|
||||
</tr>
|
||||
<!--<![endif]-->
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer%}
|
||||
{%include 'schedules/edx_ace/courseupdate/email/footer.html'%}
|
||||
{% endblock%}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
{% load django_markup %}
|
||||
{% load i18n %}
|
||||
{% load ace %}
|
||||
{% load acetags %}
|
||||
{% load static %}
|
||||
|
||||
<table role="presentation" width="100%" align="left" border="0" cellpadding="0" cellspacing="0">
|
||||
<style>
|
||||
.hyperlink {
|
||||
color: #00688d;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.hyperlink:hover {
|
||||
color: #004972 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
</style>
|
||||
{% if confirm_activation_link %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
<!-- SOCIAL -->
|
||||
<table role="presentation" align="{{ LANGUAGE_BIDI|yesno:"right,left" }}" border="0" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top" style="padding: 0px 15px;">
|
||||
<table cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top">
|
||||
<!-- logo -->
|
||||
<table align="left" cellpadding="0" cellspacing="0" class="width_100percent">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<a href="{% with_link_tracking homepage_url %}">
|
||||
<img src="{{ logo_url }}" width="92" height="65" alt="{% filter force_escape %}{% blocktrans %}Go to {{ platform_name }} Home Page{% endblocktrans %}{% endfilter %}"/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table align="right" cellpadding="0" cellspacing="0" class="width_100percent">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" class="paddingtop_20 height_0" height="66" valign="middle">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
{% if social_media_urls.facebook %}
|
||||
<td height="32" width="35">
|
||||
<a href="{{ social_media_urls.facebook|safe }}">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/social_1_fb.png"
|
||||
width="25" height="25" alt="{% filter force_escape %}{% blocktrans %}{{ platform_name }} on Facebook{% endblocktrans %}{% endfilter %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.instagram %}
|
||||
<td height="32" width="35">
|
||||
<a href="{{ social_media_urls.instagram|safe }}">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/social_4_insta.png"
|
||||
width="25" height="25" alt="{% filter force_escape %}{% blocktrans %}{{ platform_name }} on Facebook{% endblocktrans %}{% endfilter %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.linkedin %}
|
||||
<td height="32" width="35">
|
||||
<a href="{{ social_media_urls.linkedin|safe }}">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/social_3_linkedin.png"
|
||||
width="25" height="25" alt="{% filter force_escape %}{% blocktrans %}{{ platform_name }} on LinkedIn{% endblocktrans %}{% endfilter %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.twitter %}
|
||||
<td height="32" width="35">
|
||||
<a href="{{ social_media_urls.twitter|safe }}">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/social_2_twitter.png"
|
||||
width="25" height="25" alt="{% filter force_escape %}{% blocktrans %}{{ platform_name }} on Twitter{% endblocktrans %}{% endfilter %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.reddit %}
|
||||
<td height="32" width="35">
|
||||
<a href="{{ social_media_urls.reddit|safe }}">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/social_5_reddit.png"
|
||||
width="25" height="25" alt="{% filter force_escape %}{% blocktrans %}{{ platform_name }} on Reddit{% endblocktrans %}{% endfilter %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="20" style="line-height:1px;font-size:1px;"></td>
|
||||
</tr>
|
||||
<!-- APP BUTTONS -->
|
||||
<tr>
|
||||
<td align="left" valign="top" style="padding: 0px 15px;">
|
||||
<table cellpadding="0" cellspacing="0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top">
|
||||
<table align="left" cellpadding="0" cellspacing="0" class="width_100percent">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
{% if mobile_store_urls.apple %}
|
||||
<td align="left" valign="top">
|
||||
<a href="{{ mobile_store_urls.apple|safe }}" style="text-decoration: none">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/store_apple_229x78.jpg"
|
||||
alt="{% trans "Download the iOS app on the Apple Store" as tmsg %}{{ tmsg | force_escape }}"
|
||||
border="0" height="32" style="display:block;" width="95.135" /></a>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td width="20"></td>
|
||||
{% if mobile_store_urls.google %}
|
||||
<td align="left" valign="top">
|
||||
<a href="{{ mobile_store_urls.google|safe }}" style="text-decoration: none">
|
||||
<img src="http://email-media.s3.amazonaws.com/edX/2021/store_google_253x78.jpg"
|
||||
alt="{% trans "Download the Android app on the Google Play Store" as tmsg %}{{ tmsg | force_escape }}"
|
||||
border="0" height="32" style="display:block;" width="108.108" /></a>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="24" style="line-height:1px;font-size:1px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- COPYRIGHT -->
|
||||
<td align="left" class="fallback_font" style="font-family:Arial,sans-serif;font-size:14px;line-height:21px;color:#000000;font-style: normal;font-weight: 400;" valign="top">
|
||||
{% if disclaimer %}
|
||||
{{ disclaimer }}<br/>
|
||||
{% endif %}
|
||||
{% trans "edX is the trusted platform for education and learning" as tmsg %}{{ tmsg | force_escape }}.<br/>
|
||||
<br/>
|
||||
© {% now "Y" %} {{ platform_name }} LLC. {% trans "All rights reserved" as tmsg %}{{ tmsg | force_escape }}.<br/>
|
||||
<br/>
|
||||
{% if unsubscribe_link %}
|
||||
<a href="{% with_link_tracking unsubscribe_link %}" style="color: #000000;">
|
||||
{%if unsubscribe_text%} {{unsubscribe_text}} {%else%} {% trans "Unsubscribe from these emails." as tmsg %}{{ tmsg | force_escape }} {%endif%}
|
||||
</a><br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
{{ contact_mailing_address }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1 +1,40 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_head.html' %}
|
||||
{% load django_markup %}
|
||||
{% load i18n %}
|
||||
{% load ace %}
|
||||
{% load acetags %}
|
||||
{% load static %}
|
||||
|
||||
<table width="600" border="0" cellspacing="0" cellpadding="0" align="center" bgcolor="#F2F0EF">
|
||||
<tr>
|
||||
<td valign="top" align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" align="center" class="width_100percent">
|
||||
<tr>
|
||||
<td valign="top" align="center" style="max-width:600px;" class="min_width">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#F2F0EF">
|
||||
<tr><td height="20" style="line-height:1px;"> </td></tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" style="padding:0 60px;" class="padding_none">
|
||||
<table cellpadding="0" cellspacing="0" align="center" class="width_100percent">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<a href="{% with_link_tracking homepage_url %}">
|
||||
<img src="{{ logo_url }}" width="92" height="65" alt="{% filter force_escape %}{% blocktrans %}Go to {{ platform_name }} Home Page{% endblocktrans %}{% endfilter %}"/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td height="20" style="line-height:1px;font-size:1px;"> </td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,35 @@
|
||||
{% load i18n %}
|
||||
{% load ace %}
|
||||
|
||||
<p>
|
||||
{# email client support for style sheets is pretty spotty, so we have to inline all of these styles #}
|
||||
<a
|
||||
{% if reset_url %}
|
||||
href={{reset_url}}
|
||||
{% elif course_cta_url %}
|
||||
href="{% with_link_tracking course_cta_url %}"
|
||||
{% else %}
|
||||
{%if course_ids|length > 1 %}
|
||||
href="{% with_link_tracking dashboard_url %}"
|
||||
{% else %}
|
||||
href="{% with_link_tracking course_url %}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
style="
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background-color: #ED5C13;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
text-decoration-style: solid;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
border-radius: 30.22px;
|
||||
">
|
||||
{# old email clients require the use of the font tag :( #}
|
||||
<span color="#ffffff"><b>{{ course_cta_text }}</b></span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -271,6 +271,8 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
|
||||
@override_settings(CONTACT_MAILING_ADDRESS='123 Sesame Street')
|
||||
@override_settings(LOGO_URL_PNG='https://www.logo.png')
|
||||
@override_settings(SELF_PACED_BANNER_URL='')
|
||||
@override_settings(SELF_PACED_CLOUD_URL='')
|
||||
def test_schedule_context(self):
|
||||
resolver = self.create_resolver()
|
||||
# using this to make sure the select_related stays intact
|
||||
@@ -316,6 +318,8 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
'twitter': twitter_url},
|
||||
'template_revision': 'release',
|
||||
'unsubscribe_url': None,
|
||||
'self_paced_banner_url': '',
|
||||
'self_paced_cloud_url': '',
|
||||
'week_highlights': ['good stuff 2'],
|
||||
'week_num': 2,
|
||||
}
|
||||
|
||||
@@ -403,17 +403,18 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
|
||||
assert data['social_links'] is not None
|
||||
assert data['time_zone'] is None
|
||||
|
||||
def _verify_private_account_response(self, response, requires_parental_consent=False):
|
||||
def _verify_private_account_response(self, response, requires_parental_consent=False, has_profile_image=True):
|
||||
"""
|
||||
Verify that only the public fields are returned if a user does not want to share account fields
|
||||
"""
|
||||
data = response.data
|
||||
assert 3 == len(data)
|
||||
assert PRIVATE_VISIBILITY == data['account_privacy']
|
||||
self._verify_profile_image_data(data, not requires_parental_consent)
|
||||
self._verify_profile_image_data(data, has_profile_image)
|
||||
assert self.user.username == data['username']
|
||||
|
||||
def _verify_full_account_response(self, response, requires_parental_consent=False, year_of_birth=2000):
|
||||
def _verify_full_account_response(self, response, requires_parental_consent=False,
|
||||
has_profile_image=True, year_of_birth=2000):
|
||||
"""
|
||||
Verify that all account fields are returned (even those that are not shareable).
|
||||
"""
|
||||
@@ -426,7 +427,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
|
||||
UserPreference.get_value(self.user, 'account_privacy')
|
||||
)
|
||||
assert expected_account_privacy == data['account_privacy']
|
||||
self._verify_profile_image_data(data, not requires_parental_consent)
|
||||
self._verify_profile_image_data(data, has_profile_image)
|
||||
assert self.user.username == data['username']
|
||||
|
||||
# additional shareable fields (8)
|
||||
@@ -1271,11 +1272,11 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
|
||||
assert data['requires_parental_consent']
|
||||
assert PRIVATE_VISIBILITY == data['account_privacy']
|
||||
else:
|
||||
self._verify_private_account_response(response, requires_parental_consent=True)
|
||||
self._verify_private_account_response(response, requires_parental_consent=True, has_profile_image=False)
|
||||
|
||||
# Verify that the shared view is still private
|
||||
response = self.send_get(client, query_parameters='view=shared')
|
||||
self._verify_private_account_response(response, requires_parental_consent=True)
|
||||
self._verify_private_account_response(response, requires_parental_consent=True, has_profile_image=False)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
import json
|
||||
from pytz import UTC
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import urljoin
|
||||
from django.conf import settings
|
||||
@@ -20,6 +21,10 @@ from common.djangoapps.student.tests.factories import AnonymousUserFactory, User
|
||||
from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file
|
||||
from openedx.core.djangoapps.profile_images.images import create_profile_images
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
|
||||
|
||||
|
||||
TEST_PROFILE_IMAGE_UPLOAD_DT = datetime(2002, 1, 9, 15, 43, 1, tzinfo=UTC)
|
||||
|
||||
|
||||
class CookieTests(TestCase):
|
||||
@@ -27,6 +32,8 @@ class CookieTests(TestCase):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.user.profile = UserProfileFactory.create(user=self.user)
|
||||
self.user.profile.profile_image_uploaded_at = TEST_PROFILE_IMAGE_UPLOAD_DT
|
||||
self.user.profile.save() # lint-amnesty, pylint: disable=no-member
|
||||
self.request = RequestFactory().get('/')
|
||||
self.request.user = self.user
|
||||
self.request.session = self._get_stub_session()
|
||||
@@ -43,18 +50,6 @@ class CookieTests(TestCase):
|
||||
|
||||
return urls_obj
|
||||
|
||||
def _get_expected_image_urls(self):
|
||||
expected_image_urls = {
|
||||
'full': '/static/default_500.png',
|
||||
'large': '/static/default_120.png',
|
||||
'medium': '/static/default_50.png',
|
||||
'small': '/static/default_30.png'
|
||||
}
|
||||
|
||||
expected_image_urls = self._convert_to_absolute_uris(self.request, expected_image_urls)
|
||||
|
||||
return expected_image_urls
|
||||
|
||||
def _get_expected_header_urls(self):
|
||||
expected_header_urls = {
|
||||
'logout': reverse('logout'),
|
||||
@@ -112,7 +107,7 @@ class CookieTests(TestCase):
|
||||
'username': self.user.username,
|
||||
'email': self.user.email,
|
||||
'header_urls': self._get_expected_header_urls(),
|
||||
'user_image_urls': self._get_expected_image_urls(),
|
||||
'user_image_urls': get_profile_image_urls_for_user(self.user),
|
||||
}
|
||||
|
||||
self.assertDictEqual(actual, expected)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
JWT Token handling and signing functions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import jwt
|
||||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
from jwkest import Expired, Invalid, MissingKey, jwk
|
||||
from jwkest.jws import JWS
|
||||
from jwt.api_jwk import PyJWK, PyJWKSet
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, MissingRequiredClaimError
|
||||
|
||||
|
||||
def create_jwt(lms_user_id, expires_in_seconds, additional_token_claims, now=None):
|
||||
@@ -40,15 +40,9 @@ def _encode_and_sign(payload):
|
||||
|
||||
The signing key and algorithm are pulled from settings.
|
||||
"""
|
||||
keys = jwk.KEYS()
|
||||
|
||||
serialized_keypair = json.loads(settings.TOKEN_SIGNING['JWT_PRIVATE_SIGNING_JWK'])
|
||||
keys.add(serialized_keypair)
|
||||
private_key = PyJWK.from_json(settings.TOKEN_SIGNING['JWT_PRIVATE_SIGNING_JWK'])
|
||||
algorithm = settings.TOKEN_SIGNING['JWT_SIGNING_ALGORITHM']
|
||||
|
||||
data = json.dumps(payload)
|
||||
jws = JWS(data, alg=algorithm)
|
||||
return jws.sign_compact(keys=keys)
|
||||
return jwt.encode(payload, key=private_key.key, algorithm=algorithm)
|
||||
|
||||
|
||||
def unpack_jwt(token, lms_user_id, now=None):
|
||||
@@ -65,27 +59,40 @@ def unpack_jwt(token, lms_user_id, now=None):
|
||||
Returns a valid, decoded json payload (string).
|
||||
"""
|
||||
now = now or int(time())
|
||||
payload = _unpack_and_verify(token)
|
||||
payload = unpack_and_verify(token)
|
||||
|
||||
if "lms_user_id" not in payload:
|
||||
raise MissingKey("LMS user id is missing")
|
||||
raise MissingRequiredClaimError("LMS user id is missing")
|
||||
if "exp" not in payload:
|
||||
raise MissingKey("Expiration is missing")
|
||||
raise MissingRequiredClaimError("Expiration is missing")
|
||||
if payload["lms_user_id"] != lms_user_id:
|
||||
raise Invalid("User does not match")
|
||||
raise InvalidSignatureError("User does not match")
|
||||
if payload["exp"] < now:
|
||||
raise Expired("Token is expired")
|
||||
raise ExpiredSignatureError("Token is expired")
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _unpack_and_verify(token):
|
||||
def unpack_and_verify(token): # pylint: disable=inconsistent-return-statements
|
||||
"""
|
||||
Unpack and verify the provided token.
|
||||
|
||||
The signing key and algorithm are pulled from settings.
|
||||
"""
|
||||
keys = jwk.KEYS()
|
||||
keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
|
||||
decoded = JWS().verify_compact(token.encode('utf-8'), keys)
|
||||
return decoded
|
||||
key_set = []
|
||||
key_set.extend(
|
||||
PyJWKSet.from_json(settings.TOKEN_SIGNING["JWT_PUBLIC_SIGNING_JWK_SET"]).keys
|
||||
)
|
||||
|
||||
for i in range(len(key_set)): # pylint: disable=consider-using-enumerate
|
||||
try:
|
||||
decoded = jwt.decode(
|
||||
token,
|
||||
key=key_set[i].key,
|
||||
algorithms=["RS256", "RS512"],
|
||||
options={"verify_signature": True, "verify_aud": False},
|
||||
)
|
||||
return decoded
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
if i == len(key_set) - 1:
|
||||
raise
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
Tests for token handling
|
||||
"""
|
||||
import unittest
|
||||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
from jwkest import BadSignature, Expired, Invalid, MissingKey, jwk
|
||||
from jwkest.jws import JWS
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, MissingRequiredClaimError
|
||||
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.core.lib.jwt import _encode_and_sign, create_jwt, unpack_jwt
|
||||
from openedx.core.lib.jwt import _encode_and_sign, create_jwt, unpack_jwt, unpack_and_verify
|
||||
|
||||
|
||||
test_user_id = 121
|
||||
invalid_test_user_id = 120
|
||||
test_timeout = 60
|
||||
test_now = 1661432902
|
||||
test_timeout = 1000
|
||||
test_now = int(time())
|
||||
test_claims = {"foo": "bar", "baz": "quux", "meaning": 42}
|
||||
expected_full_token = {
|
||||
"lms_user_id": test_user_id,
|
||||
"iat": 1661432902,
|
||||
"exp": 1661432902 + 60,
|
||||
"iat": test_now,
|
||||
"exp": test_now + test_timeout,
|
||||
"iss": "token-test-issuer", # these lines from test_settings.py
|
||||
"version": "1.2.0", # these lines from test_settings.py
|
||||
}
|
||||
@@ -34,7 +33,7 @@ class TestSign(unittest.TestCase):
|
||||
def test_create_jwt(self):
|
||||
token = create_jwt(test_user_id, test_timeout, {}, test_now)
|
||||
|
||||
decoded = _verify_jwt(token)
|
||||
decoded = unpack_and_verify(token)
|
||||
self.assertEqual(expected_full_token, decoded)
|
||||
|
||||
def test_create_jwt_with_claims(self):
|
||||
@@ -43,7 +42,7 @@ class TestSign(unittest.TestCase):
|
||||
expected_token_with_claims = expected_full_token.copy()
|
||||
expected_token_with_claims.update(test_claims)
|
||||
|
||||
decoded = _verify_jwt(token)
|
||||
decoded = unpack_and_verify(token)
|
||||
self.assertEqual(expected_token_with_claims, decoded)
|
||||
|
||||
def test_malformed_token(self):
|
||||
@@ -53,19 +52,8 @@ class TestSign(unittest.TestCase):
|
||||
expected_token_with_claims = expected_full_token.copy()
|
||||
expected_token_with_claims.update(test_claims)
|
||||
|
||||
with self.assertRaises(BadSignature):
|
||||
_verify_jwt(token)
|
||||
|
||||
|
||||
def _verify_jwt(jwt_token):
|
||||
"""
|
||||
Helper function which verifies the signature and decodes the token
|
||||
from string back to claims form
|
||||
"""
|
||||
keys = jwk.KEYS()
|
||||
keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
|
||||
decoded = JWS().verify_compact(jwt_token.encode('utf-8'), keys)
|
||||
return decoded
|
||||
with self.assertRaises(InvalidSignatureError):
|
||||
unpack_and_verify(token)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@@ -97,19 +85,19 @@ class TestUnpack(unittest.TestCase):
|
||||
expected_token_with_claims = expected_full_token.copy()
|
||||
expected_token_with_claims.update(test_claims)
|
||||
|
||||
with self.assertRaises(BadSignature):
|
||||
with self.assertRaises(InvalidSignatureError):
|
||||
unpack_jwt(token, test_user_id, test_now)
|
||||
|
||||
def test_unpack_token_with_invalid_user(self):
|
||||
token = create_jwt(invalid_test_user_id, test_timeout, {}, test_now)
|
||||
|
||||
with self.assertRaises(Invalid):
|
||||
with self.assertRaises(InvalidSignatureError):
|
||||
unpack_jwt(token, test_user_id, test_now)
|
||||
|
||||
def test_unpack_expired_token(self):
|
||||
token = create_jwt(test_user_id, test_timeout, {}, test_now)
|
||||
|
||||
with self.assertRaises(Expired):
|
||||
with self.assertRaises(ExpiredSignatureError):
|
||||
unpack_jwt(token, test_user_id, test_now + test_timeout + 1)
|
||||
|
||||
def test_missing_expired_lms_user_id(self):
|
||||
@@ -117,7 +105,7 @@ class TestUnpack(unittest.TestCase):
|
||||
del payload['lms_user_id']
|
||||
token = _encode_and_sign(payload)
|
||||
|
||||
with self.assertRaises(MissingKey):
|
||||
with self.assertRaises(MissingRequiredClaimError):
|
||||
unpack_jwt(token, test_user_id, test_now)
|
||||
|
||||
def test_missing_expired_key(self):
|
||||
@@ -125,5 +113,5 @@ class TestUnpack(unittest.TestCase):
|
||||
del payload['exp']
|
||||
token = _encode_and_sign(payload)
|
||||
|
||||
with self.assertRaises(MissingKey):
|
||||
with self.assertRaises(MissingRequiredClaimError):
|
||||
unpack_jwt(token, test_user_id, test_now)
|
||||
|
||||
@@ -51,7 +51,7 @@ django-stubs<6
|
||||
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
|
||||
# This is to allow them to better control its deployment and to do it in a process that works better
|
||||
# for them.
|
||||
edx-enterprise==6.0.3
|
||||
edx-enterprise==6.2.5
|
||||
|
||||
# Date: 2023-07-26
|
||||
# Our legacy Sass code is incompatible with anything except this ancient libsass version.
|
||||
|
||||
@@ -421,7 +421,7 @@ edx-celeryutils==1.4.0
|
||||
# -r requirements/edx/kernel.in
|
||||
# edx-name-affirmation
|
||||
# super-csv
|
||||
edx-codejail==3.5.2
|
||||
edx-codejail==4.0.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
edx-completion==4.9
|
||||
# via -r requirements/edx/kernel.in
|
||||
@@ -461,7 +461,7 @@ edx-drf-extensions==10.6.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==6.0.3
|
||||
edx-enterprise==6.2.5
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/kernel.in
|
||||
|
||||
@@ -691,7 +691,7 @@ edx-celeryutils==1.4.0
|
||||
# -r requirements/edx/testing.txt
|
||||
# edx-name-affirmation
|
||||
# super-csv
|
||||
edx-codejail==3.5.2
|
||||
edx-codejail==4.0.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -740,7 +740,7 @@ edx-drf-extensions==10.6.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==6.0.3
|
||||
edx-enterprise==6.2.5
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/doc.txt
|
||||
|
||||
@@ -505,7 +505,7 @@ edx-celeryutils==1.4.0
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-name-affirmation
|
||||
# super-csv
|
||||
edx-codejail==3.5.2
|
||||
edx-codejail==4.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-completion==4.9
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -545,7 +545,7 @@ edx-drf-extensions==10.6.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==6.0.3
|
||||
edx-enterprise==6.2.5
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
|
||||
@@ -66,7 +66,8 @@ edx-celeryutils
|
||||
edx-completion
|
||||
edx-django-release-util # Release utils for the edx release pipeline
|
||||
edx-django-sites-extensions
|
||||
edx-codejail
|
||||
# Codejail 4 brings important safety improvements (no unsafe mode by default)
|
||||
edx-codejail>=4.0.0
|
||||
# edx-django-utils 5.14.1 adds FrontendMonitoringMiddleware
|
||||
edx-django-utils>=5.14.1 # Utilities for cache, monitoring, and plugins
|
||||
edx-drf-extensions
|
||||
|
||||
@@ -530,7 +530,7 @@ edx-celeryutils==1.4.0
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-name-affirmation
|
||||
# super-csv
|
||||
edx-codejail==3.5.2
|
||||
edx-codejail==4.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-completion==4.9
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -570,7 +570,7 @@ edx-drf-extensions==10.6.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==6.0.3
|
||||
edx-enterprise==6.2.5
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
|
||||
@@ -24,8 +24,10 @@ from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from xmodule.capa.safe_exec import safe_exec, update_hash
|
||||
from xmodule.capa.safe_exec.remote_exec import is_codejail_in_darklaunch, is_codejail_rest_service_enabled
|
||||
from xmodule.capa.safe_exec.safe_exec import emsg_normalizers, normalize_error_message
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class TestSafeExec(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def test_set_values(self):
|
||||
g = {}
|
||||
@@ -530,6 +532,7 @@ class DictCache(object):
|
||||
self.cache[key] = value
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class TestSafeExecCaching(unittest.TestCase):
|
||||
"""Test that caching works on safe_exec."""
|
||||
|
||||
@@ -654,6 +657,7 @@ class TestUpdateHash(unittest.TestCase):
|
||||
assert h1 == h2
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class TestRealProblems(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def test_802x(self):
|
||||
code = textwrap.dedent("""\
|
||||
|
||||
@@ -15,6 +15,7 @@ from markupsafe import Markup
|
||||
from xmodule.capa.correctmap import CorrectMap
|
||||
from xmodule.capa.responsetypes import LoncapaProblemError
|
||||
from xmodule.capa.tests.helpers import new_loncapa_problem
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
|
||||
@@ -23,6 +24,7 @@ FEATURES_WITH_GRADING_METHOD_IN_PROBLEMS['ENABLE_GRADING_METHOD_IN_PROBLEMS'] =
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@use_unsafe_codejail()
|
||||
class CAPAProblemTest(unittest.TestCase):
|
||||
""" CAPA problem related tests"""
|
||||
|
||||
@@ -424,6 +426,7 @@ class CAPAProblemTest(unittest.TestCase):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@use_unsafe_codejail()
|
||||
class CAPAMultiInputProblemTest(unittest.TestCase):
|
||||
""" TestCase for CAPA problems with multiple inputtypes """
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ from unittest import mock
|
||||
import ddt
|
||||
from lxml import etree
|
||||
from xmodule.capa.tests.helpers import new_loncapa_problem, mock_capa_system
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
from .response_xml_factory import CustomResponseXMLFactory, StringResponseXMLFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@use_unsafe_codejail()
|
||||
class CapaHtmlRenderTest(unittest.TestCase):
|
||||
"""
|
||||
CAPA HTML rendering tests class.
|
||||
|
||||
@@ -37,6 +37,7 @@ from xmodule.capa.tests.response_xml_factory import (
|
||||
SymbolicResponseXMLFactory,
|
||||
TrueFalseResponseXMLFactory
|
||||
)
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
from xmodule.capa.util import convert_files_to_filenames
|
||||
from xmodule.capa.xqueue_interface import dateformat
|
||||
|
||||
@@ -108,6 +109,7 @@ class ResponseTest(unittest.TestCase):
|
||||
return str(rand.randint(0, 1e9))
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class MultiChoiceResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = MultipleChoiceResponseXMLFactory
|
||||
|
||||
@@ -375,6 +377,7 @@ class SymbolicResponseTest(ResponseTest): # pylint: disable=missing-class-docst
|
||||
assert correct_map.get_correctness('1_2_1') == expected_correctness
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class OptionResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = OptionResponseXMLFactory
|
||||
|
||||
@@ -422,6 +425,7 @@ class OptionResponseTest(ResponseTest): # pylint: disable=missing-class-docstri
|
||||
assert correct_map.get_property('1_2_1', 'answervariable') == '$a'
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class FormulaResponseTest(ResponseTest):
|
||||
"""
|
||||
Test the FormulaResponse class
|
||||
@@ -571,6 +575,7 @@ class FormulaResponseTest(ResponseTest):
|
||||
assert not list(problem.responders.values())[0].validate_answer('3*y+2*x')
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class StringResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = StringResponseXMLFactory
|
||||
|
||||
@@ -1124,6 +1129,7 @@ class CodeResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
assert output[answer_id]['msg'] == 'Invalid grader reply. Please contact the course staff.'
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class ChoiceResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = ChoiceResponseXMLFactory
|
||||
|
||||
@@ -1292,6 +1298,7 @@ class ChoiceResponseTest(ResponseTest): # pylint: disable=missing-class-docstri
|
||||
self.assert_grade(problem, ['choice_1', 'choice_3'], 'incorrect')
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class NumericalResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = NumericalResponseXMLFactory
|
||||
|
||||
@@ -1680,6 +1687,7 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-class-docs
|
||||
assert not responder.validate_answer('fish')
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class CustomResponseTest(ResponseTest): # pylint: disable=missing-class-docstring
|
||||
xml_factory_class = CustomResponseXMLFactory
|
||||
|
||||
@@ -2399,6 +2407,7 @@ class CustomResponseTest(ResponseTest): # pylint: disable=missing-class-docstri
|
||||
assert correct_map.get_msg('1_2_11') == '11'
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class SchematicResponseTest(ResponseTest):
|
||||
"""
|
||||
Class containing setup and tests for Schematic responsetype.
|
||||
@@ -2488,6 +2497,7 @@ class AnnotationResponseTest(ResponseTest): # lint-amnesty, pylint: disable=mis
|
||||
assert expected_points == actual_points, ('%s should have %d points' % (answer_id, expected_points))
|
||||
|
||||
|
||||
@use_unsafe_codejail()
|
||||
class ChoiceTextResponseTest(ResponseTest):
|
||||
"""
|
||||
Class containing setup and tests for ChoiceText responsetype.
|
||||
|
||||
@@ -6,7 +6,9 @@ Tests capa util
|
||||
|
||||
import unittest
|
||||
|
||||
import codejail.safe_exec
|
||||
import ddt
|
||||
from django.test.utils import TestContextDecorator
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.capa.tests.helpers import mock_capa_system
|
||||
@@ -167,3 +169,28 @@ class UtilTest(unittest.TestCase):
|
||||
expected_text = '$あなたあなたあなたあなた あなたhi'
|
||||
contextual_text = contextualize_text(text, context)
|
||||
assert expected_text == contextual_text
|
||||
|
||||
|
||||
class use_unsafe_codejail(TestContextDecorator):
|
||||
"""
|
||||
Tell codejail to run in unsafe mode for the scope of the decorator.
|
||||
Use this as a decorator on Django TestCase classes or methods.
|
||||
|
||||
This is needed because codejail has significant OS-level setup requirements
|
||||
which we don't even attempt to fulfill for unit testing purposes. Running
|
||||
tests in unsafe mode (that is, running code executions in-process, with no
|
||||
sandboxing) is only safe because we control the contents of the unit tests.
|
||||
It's not a perfect replica of how safe mode operates but it's generally good
|
||||
enough for testing the integration and overall behavior.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.old_be_unsafe = None
|
||||
super().__init__()
|
||||
|
||||
def enable(self):
|
||||
self.old_be_unsafe = codejail.safe_exec.ALWAYS_BE_UNSAFE
|
||||
codejail.safe_exec.ALWAYS_BE_UNSAFE = True
|
||||
|
||||
def disable(self):
|
||||
codejail.safe_exec.ALWAYS_BE_UNSAFE = self.old_be_unsafe
|
||||
|
||||
@@ -37,6 +37,7 @@ from xmodule.capa.responsetypes import LoncapaProblemError, ResponseError, Stude
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface
|
||||
from xmodule.capa_block import ComplexEncoder, ProblemBlock
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.capa.tests.test_util import use_unsafe_codejail
|
||||
|
||||
from ..capa_block import RANDOMIZATION, SHOWANSWER
|
||||
from . import get_test_system
|
||||
@@ -3635,6 +3636,7 @@ class ComplexEncoderTest(unittest.TestCase): # lint-amnesty, pylint: disable=mi
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@use_unsafe_codejail()
|
||||
class ProblemCheckTrackingTest(unittest.TestCase):
|
||||
"""
|
||||
Ensure correct tracking information is included in events emitted during problem checks.
|
||||
|
||||
Reference in New Issue
Block a user