Merge branch 'openedx:master' into edx-depr31

This commit is contained in:
Yagnesh1998
2023-07-17 15:54:46 +05:30
committed by GitHub
305 changed files with 6718 additions and 1881 deletions

View File

@@ -8,7 +8,6 @@ from cms.djangoapps.contentstore.api.views import course_import, course_quality,
app_name = 'contentstore'
helper = "{0,1}"
urlpatterns = [
re_path(fr'^v0/import/{settings.COURSE_ID_PATTERN}/$',

View File

@@ -0,0 +1,125 @@
"""A Command to Copy or uncopy V1 Content Libraries entires to be stored as v2 content libraries."""
import logging
from textwrap import dedent
from django.core.management import BaseCommand, CommandError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from xmodule.modulestore.django import modulestore
from celery import group
from cms.djangoapps.contentstore.tasks import create_v2_library_from_v1_library, delete_v2_library_from_v1_library
from .prompt import query_yes_no
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Copy or uncopy V1 Content Libraries (default all) entires to be stored as v2 content libraries.
First Specify the uuid for the collection to store the content libraries in.
Specfiy --all for all libraries, library ids for specific libraries,
and -- file followed by the path for a list of libraries from a file.
Example usage:
$ ./manage.py cms copy_libraries_from_v1_to_v2 'collection_uuid' --all
$ ./manage.py cms copy_libraries_from_v1_to_v2
library-v1:edX+DemoX+Demo_Library' 'library-v1:edX+DemoX+Better_Library' -c 'collection_uuid'
$ ./manage.py cms copy_libraries_from_v1_to_v2 --all --uncopy
$ ./manage.py cms copy_libraries_from_v1_to_v2 'library-v1:edX+DemoX+Better_Library' --uncopy
$ ./manage.py cms copy_libraries_from_v1_to_v2
'11111111-2111-4111-8111-111111111111'
'./list_of--library-locators- --file
Note:
This Command Also produces an "output file" which contains the mapping of locators and the status of the copy.
"""
help = dedent(__doc__)
CONFIRMATION_PROMPT = "Reindexing all libraries might be a time consuming operation. Do you want to continue?"
def add_arguments(self, parser):
"""arguements for command"""
parser.add_argument(
'-collection_uuid',
'-c',
nargs=1,
type=str,
help='the uuid for the collection to create the content library in.'
)
parser.add_argument(
'library_ids',
nargs='*',
help='a space-seperated list of v1 library ids to copy'
)
parser.add_argument(
'--all',
action='store_true',
dest='all',
help='Copy all libraries'
)
parser.add_argument(
'--uncopy',
action='store_true',
dest='uncopy',
help='Delete libraries specified'
)
parser.add_argument(
'output_csv',
nargs='?',
default=None,
help='a file path to write the tasks output to. Without this the result is simply logged.'
)
def _parse_library_key(self, raw_value):
""" Parses library key from string """
result = CourseKey.from_string(raw_value)
if not isinstance(result, LibraryLocator):
raise CommandError(f"Argument {raw_value} is not a library key")
return result
def handle(self, *args, **options): # lint-amnesty, pylint: disable=unused-argument
"""Parse args and generate tasks for copying content."""
print(options)
if (not options['library_ids'] and not options['all']) or (options['library_ids'] and options['all']):
raise CommandError("copy_libraries_from_v1_to_v2 requires one or more <library_id>s or the --all flag.")
if (not options['library_ids'] and not options['all']) or (options['library_ids'] and options['all']):
raise CommandError("copy_libraries_from_v1_to_v2 requires one or more <library_id>s or the --all flag.")
if options['all']:
store = modulestore()
if query_yes_no(self.CONFIRMATION_PROMPT, default="no"):
v1_library_keys = [
library.location.library_key.replace(branch=None) for library in store.get_libraries()
]
else:
return
else:
v1_library_keys = list(map(self._parse_library_key, options['library_ids']))
create_library_task_group = group([
delete_v2_library_from_v1_library.s(str(v1_library_key), options['collection_uuid'][0])
if options['uncopy']
else create_v2_library_from_v1_library.s(str(v1_library_key), options['collection_uuid'][0])
for v1_library_key in v1_library_keys
])
group_result = create_library_task_group.apply_async().get()
if options['output_csv']:
with open(options['output_csv'][0], 'w', encoding='utf-8', newline='') as output_writer:
output_writer.writerow("v1_library_id", "v2_library_id", "status", "error_msg")
for result in group_result:
output_writer.write(result.keys())
log.info(group_result)

View File

@@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from cms.djangoapps.contentstore.xblock_services.xblock_service import delete_orphans
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import delete_orphans
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order

View File

@@ -9,12 +9,12 @@ from common.djangoapps.util.json_request import expect_json_in_class_view
from ....api import course_author_access_required
from cms.djangoapps.contentstore.xblock_services import xblock_service
from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers
import cms.djangoapps.contentstore.toggles as contentstore_toggles
log = logging.getLogger(__name__)
toggles = contentstore_toggles
handle_xblock = xblock_service.handle_xblock
handle_xblock = view_handlers.handle_xblock
@view_auth_classes()

View File

@@ -177,8 +177,9 @@ def listen_for_xblock_published(sender, signal, **kwargs):
Publish XBLOCK_PUBLISHED signals onto the event bus.
"""
if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"):
topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle")
get_producer().send(
signal=XBLOCK_PUBLISHED, topic='xblock-published',
signal=XBLOCK_PUBLISHED, topic=topic,
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
event_metadata=kwargs['metadata'],
)
@@ -190,8 +191,9 @@ def listen_for_xblock_deleted(sender, signal, **kwargs):
Publish XBLOCK_DELETED signals onto the event bus.
"""
if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"):
topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle")
get_producer().send(
signal=XBLOCK_DELETED, topic='xblock-deleted',
signal=XBLOCK_DELETED, topic=topic,
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
event_metadata=kwargs['metadata'],
)
@@ -203,8 +205,9 @@ def listen_for_xblock_duplicated(sender, signal, **kwargs):
Publish XBLOCK_DUPLICATED signals onto the event bus.
"""
if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"):
topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle")
get_producer().send(
signal=XBLOCK_DUPLICATED, topic='xblock-duplicated',
signal=XBLOCK_DUPLICATED, topic=topic,
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
event_metadata=kwargs['metadata'],
)

View File

@@ -19,6 +19,7 @@ from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import SuspiciousOperation
from django.core.files import File
from django.db.transaction import atomic
from django.test import RequestFactory
from django.utils.text import get_valid_filename
from edx_django_utils.monitoring import (
@@ -30,9 +31,10 @@ from edx_django_utils.monitoring import (
from olxcleaner.exceptions import ErrorLevel
from olxcleaner.reporting import report_error_summary, report_errors
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
from organizations.api import add_organization_course, ensure_organization
from organizations.models import OrganizationCourse
from organizations.exceptions import InvalidOrganizationException
from organizations.models import Organization, OrganizationCourse
from path import Path as path
from pytz import UTC
from user_tasks.models import UserTaskArtifact, UserTaskStatus
@@ -47,13 +49,17 @@ from cms.djangoapps.contentstore.courseware_index import (
from cms.djangoapps.contentstore.storage import course_import_export_storage
from cms.djangoapps.contentstore.utils import initialize_permissions, reverse_usage_url, translation_language
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from common.djangoapps.course_action_state.models import CourseRerunState
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
from common.djangoapps.util.monitoring import monitor_import_failure
from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines
from openedx.core.djangoapps.content_libraries import api as v2contentlib_api
from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled
from openedx.core.djangoapps.discussions.tasks import update_unit_discussion_state_from_discussion_blocks
from openedx.core.djangoapps.embargo.models import CountryAccessRule, RestrictedCourse
from openedx.core.lib.blockstore_api import get_collection
from openedx.core.lib.extract_tar import safetar_extractall
from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.course_block import CourseFields # lint-amnesty, pylint: disable=wrong-import-order
@@ -790,7 +796,6 @@ def log_errors_to_artifact(errorstore, status):
def handle_course_import_exception(courselike_key, exception, status, known=True):
"""
Handle course import exception and fail task status.
Arguments:
courselike_key: A locator identifies a course resource.
exception: Exception object
@@ -808,3 +813,159 @@ def handle_course_import_exception(courselike_key, exception, status, known=True
if status.state != UserTaskStatus.FAILED:
status.fail(task_fail_message)
def _parse_organization(org_name):
"""Find a matching organization name, if one does not exist, specify that this is the *unspecfied* organization"""
try:
ensure_organization(org_name)
except InvalidOrganizationException:
return 'None'
return Organization.objects.get(short_name=org_name)
def copy_v1_user_roles_into_v2_library(v2_library_key, v1_library_key):
"""
write the access and edit permissions of a v1 library into a v2 library.
"""
def _get_users_by_access_level(v1_library_key):
"""
Get a permissions object for a library which contains a list of user IDs for every V2 permissions level,
based on V1 library roles.
The following mapping exists for a library:
V1 Library Role -> V2 Permission Level
LibraryUserRole -> READ_LEVEL
CourseStaffRole -> AUTHOR_LEVEL
CourseInstructorRole -> ADMIN_LEVEL
"""
permissions = {}
permissions[v2contentlib_api.AccessLevel.READ_LEVEL] = list(LibraryUserRole(v1_library_key).users_with_role())
permissions[v2contentlib_api.AccessLevel.AUTHOR_LEVEL] = list(CourseStaffRole(v1_library_key).users_with_role())
permissions[v2contentlib_api.AccessLevel.ADMIN_LEVEL] = list(
CourseInstructorRole(v1_library_key).users_with_role()
)
return permissions
permissions = _get_users_by_access_level(v1_library_key)
for access_level in permissions.keys(): # lint-amnesty, pylint: disable=consider-iterating-dictionary
for user in permissions[access_level]:
v2contentlib_api.set_library_user_permissions(v2_library_key, user, access_level)
def _create_copy_content_task(v2_library_key, v1_library_key):
"""
spin up a celery task to import the V1 Library's content into the V2 library.
This utalizes the fact that course and v1 library content is stored almost identically.
"""
return v2contentlib_api.import_blocks_create_task(v2_library_key, v1_library_key)
def _create_metadata(v1_library_key, collection_uuid):
"""instansiate an index for the V2 lib in the collection"""
store = modulestore()
v1_library = store.get_library(v1_library_key)
collection = get_collection(collection_uuid).uuid
# To make it easy, all converted libs are complex, meaning they can contain problems, videos, and text
library_type = 'complex'
org = _parse_organization(v1_library.location.library_key.org)
slug = v1_library.location.library_key.library
title = v1_library.display_name
# V1 libraries do not have descriptions.
description = ''
# permssions & license are most restrictive.
allow_public_learning = False
allow_public_read = False
library_license = '' # '' = ALL_RIGHTS_RESERVED
with atomic():
return v2contentlib_api.create_library(
collection,
library_type,
org,
slug,
title,
description,
allow_public_learning,
allow_public_read,
library_license
)
@shared_task(time_limit=30)
@set_code_owner_attribute
def delete_v2_library_from_v1_library(v1_library_key_string, collection_uuid):
"""
For a V1 Library, delete the matching v2 library, where the library is the result of the copy operation
This method relys on _create_metadata failling for LibraryAlreadyExists in order to obtain the v2 slug.
"""
v1_library_key = CourseKey.from_string(v1_library_key_string)
v2_library_key = LibraryLocatorV2.from_string('lib:' + v1_library_key.org + ':' + v1_library_key.course)
try:
v2contentlib_api.delete_library(v2_library_key)
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": v2_library_key,
"status": "SUCCESS",
"msg": None
}
except Exception as error: # lint-amnesty, pylint: disable=broad-except
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": v2_library_key,
"status": "FAILED",
"msg": f"Exception: {v2_library_key} did not delete: {error}"
}
@shared_task(time_limit=30)
@set_code_owner_attribute
def create_v2_library_from_v1_library(v1_library_key_string, collection_uuid):
"""
write the metadata, permissions, and content of a v1 library into a v2 library in the given collection.
"""
v1_library_key = CourseKey.from_string(v1_library_key_string)
LOGGER.info(f"Copy Library task created for library: {v1_library_key}")
try:
v2_library_metadata = _create_metadata(v1_library_key, collection_uuid)
except v2contentlib_api.LibraryAlreadyExists:
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": None,
"status": "FAILED",
"msg": f"Exception: LibraryAlreadyExists {v1_library_key_string} aleady exists"
}
try:
_create_copy_content_task(v2_library_metadata.key, v1_library_key)
except Exception as error: # lint-amnesty, pylint: disable=broad-except
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": str(v2_library_metadata.key),
"status": "FAILED",
"msg":
f"Could not import content from {v1_library_key_string} into {str(v2_library_metadata.key)}: {str(error)}"
}
try:
copy_v1_user_roles_into_v2_library(v2_library_metadata.key, v1_library_key)
except Exception as error: # lint-amnesty, pylint: disable=broad-except
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": str(v2_library_metadata.key),
"status": "FAILED",
"msg":
f"Could not copy permissions from {v1_library_key_string} into {str(v2_library_metadata.key)}: {str(error)}"
}
return {
"v1_library_id": v1_library_key_string,
"v2_library_id": str(v2_library_metadata.key),
"status": "SUCCESS",
"msg": None
}

View File

@@ -1080,6 +1080,36 @@ class ContentStoreTest(ContentStoreTestCase):
"""Test new course creation - happy path"""
self.assert_created_course()
@ddt.data(True, False)
@mock.patch(
'cms.djangoapps.contentstore.views.course.default_enable_flexible_peer_openassessments'
)
def test_create_course__default_enable_flexible_peer_openassessments(
self,
mock_toggle_state,
mock_default_enable_flexible_peer_openassessments
):
"""
Test that flex peer grading is forced on, when enabled
"""
# Given a new course run
test_course_data = {}
test_course_data.update(self.course_data)
course_key = _get_course_id(self.store, test_course_data)
# ... with org configured to / not to enable flex grading
mock_default_enable_flexible_peer_openassessments.return_value = mock_toggle_state
# When I create a new course
new_course_data = _create_course(self, course_key, test_course_data)
# Then the process completes successfully
new_course_key = CourseKey.from_string(new_course_data['course_key'])
new_course = self.store.get_course(new_course_key)
# ... and our setting got toggled appropriately on the course
self.assertEqual(new_course.force_on_flexible_peer_openassessments, mock_toggle_state)
@override_settings(DEFAULT_COURSE_LANGUAGE='hr')
def test_create_course_default_language(self):
"""Test new course creation and verify default language"""
@@ -2104,6 +2134,8 @@ class EntryPageTestCase(TestCase):
def _create_course(test, course_key, course_data):
"""
Creates a course via an AJAX request and verifies the URL returned in the response.
Returns the data of the POST response
"""
course_url = get_url('course_handler', course_key, 'course_key_string')
response = test.client.ajax_post(course_url, course_data)
@@ -2112,6 +2144,8 @@ def _create_course(test, course_key, course_data):
test.assertNotIn('ErrMsg', data)
test.assertEqual(data['url'], course_url)
return data
def _get_course_id(store, course_data):
"""Returns the course ID."""

View File

@@ -4,6 +4,7 @@ Test view handler for rerun (and eventually create)
import datetime
from itertools import product
from unittest import mock
import ddt
@@ -317,3 +318,54 @@ class TestCourseListing(ModuleStoreTestCase):
'run': '2021_T1'
})
self.assertEqual(response.status_code, 403)
@ddt.data(*product([True, False], [True, False]))
@ddt.unpack
@mock.patch(
'cms.djangoapps.contentstore.views.course.default_enable_flexible_peer_openassessments'
)
def test_default_enable_flexible_peer_openassessments_on_rerun(
self,
mock_toggle_state,
mock_original_course_setting,
mock_default_enable_flexible_peer_openassessments
):
"""
Test that flex peer grading is forced on, when enabled
"""
# Given a valid course to rerun
add_organization({
'name': 'Test Flex Grading',
'short_name': self.source_course_key.org,
'description': 'Test roll-forward of flex grading setting',
})
source_course = self.store.get_course(self.source_course_key)
source_course.force_on_flexible_peer_openassessments = mock_original_course_setting
self.store.update_item(source_course, self.user.id)
mock_default_enable_flexible_peer_openassessments.return_value = mock_toggle_state
# When I create a new course
response = self.client.ajax_post(self.course_create_rerun_url, {
'source_course_key': str(self.source_course_key),
'org': self.source_course_key.org,
'course': self.source_course_key.course,
'run': 'copy',
'display_name': 'New, exciting course!',
})
# Then the process completes successfully
self.assertEqual(response.status_code, 200)
data = parse_json(response)
dest_course_key = CourseKey.from_string(data['destination_course_key'])
dest_course = self.store.get_course(dest_course_key)
# ... and our setting got enabled appropriately on our new course
if mock_toggle_state:
self.assertTrue(dest_course.force_on_flexible_peer_openassessments)
# ... or preserved if the default enable setting is not on
else:
self.assertEqual(
source_course.force_on_flexible_peer_openassessments,
dest_course.force_on_flexible_peer_openassessments
)

View File

@@ -1,8 +1,6 @@
"""
Tests for validate Internationalization and XBlock i18n service.
"""
import gettext
from unittest import mock, skip
@@ -17,7 +15,6 @@ from xmodule.tests.test_export import PureXBlock
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from cms.djangoapps.contentstore.views.preview import _prepare_runtime_for_preview
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.lib.edx_six import get_gettext
class FakeTranslations(XBlockI18nService):
@@ -68,8 +65,13 @@ class TestXBlockI18nService(ModuleStoreTestCase):
self.test_language = 'dummy language'
self.request = mock.Mock()
self.course = CourseFactory.create()
self.field_data = mock.Mock()
self.block = BlockFactory(category="pure", parent=self.course)
_prepare_runtime_for_preview(self.request, self.block)
_prepare_runtime_for_preview(
self.request,
self.block,
self.field_data,
)
self.addCleanup(translation.deactivate)
def get_block_i18n_service(self, block):
@@ -94,7 +96,7 @@ class TestXBlockI18nService(ModuleStoreTestCase):
def __init__(self, module):
self.module = module
self.old_ugettext = get_gettext(module)
self.old_ugettext = module.gettext
def __enter__(self):
def new_ugettext(*args, **kwargs):
@@ -152,9 +154,9 @@ class TestXBlockI18nService(ModuleStoreTestCase):
with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir,
languages=[get_language()])):
i18n_service = self.get_block_i18n_service(self.block)
self.assertEqual(get_gettext(i18n_service)('Hello'), 'Hello')
self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'fr-hello-world')
self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'es-hello-world')
self.assertEqual(i18n_service.gettext('Hello'), 'Hello')
self.assertNotEqual(i18n_service.gettext('Hello'), 'fr-hello-world')
self.assertNotEqual(i18n_service.gettext('Hello'), 'es-hello-world')
translation.activate("fr")
with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir,

View File

@@ -238,7 +238,7 @@ def use_new_home_page():
return ENABLE_NEW_STUDIO_HOME_PAGE.is_enabled()
# .. toggle_name: new_studio_mfe.use_new_custom_pages
# .. toggle_name: contentstore.new_studio_mfe.use_new_custom_pages
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio custom pages mfe
@@ -258,7 +258,7 @@ def use_new_custom_pages(course_key):
return ENABLE_NEW_STUDIO_CUSTOM_PAGES.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_schedule_details_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_schedule_details_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio schedule and details mfe
@@ -278,7 +278,7 @@ def use_new_schedule_details_page(course_key):
return ENABLE_NEW_STUDIO_SCHEDULE_DETAILS_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_advanced_settings_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_advanced_settings_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio advanced settings page mfe
@@ -298,7 +298,7 @@ def use_new_advanced_settings_page(course_key):
return ENABLE_NEW_STUDIO_ADVANCED_SETTINGS_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_grading_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_grading_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio grading page mfe
@@ -318,7 +318,7 @@ def use_new_grading_page(course_key):
return ENABLE_NEW_STUDIO_GRADING_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_updates_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_updates_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio updates page mfe
@@ -338,7 +338,7 @@ def use_new_updates_page(course_key):
return ENABLE_NEW_STUDIO_UPDATES_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_import_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_import_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio import page mfe
@@ -358,7 +358,7 @@ def use_new_import_page(course_key):
return ENABLE_NEW_STUDIO_IMPORT_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_export_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_export_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio export page mfe
@@ -378,7 +378,7 @@ def use_new_export_page(course_key):
return ENABLE_NEW_STUDIO_EXPORT_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_files_uploads_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_files_uploads_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio files and uploads page mfe
@@ -398,7 +398,7 @@ def use_new_files_uploads_page(course_key):
return ENABLE_NEW_STUDIO_FILES_UPLOADS_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_video_uploads_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_video_uploads_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new video uploads page mfe
@@ -418,7 +418,7 @@ def use_new_video_uploads_page(course_key):
return ENABLE_NEW_STUDIO_VIDEO_UPLOADS_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_course_outline_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_course_outline_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio course outline page mfe
@@ -438,7 +438,7 @@ def use_new_course_outline_page(course_key):
return ENABLE_NEW_STUDIO_COURSE_OUTLINE_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_unit_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_unit_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio course outline page mfe
@@ -458,7 +458,7 @@ def use_new_unit_page(course_key):
return ENABLE_NEW_STUDIO_UNIT_PAGE.is_enabled(course_key)
# .. toggle_name: new_studio_mfe.use_new_course_team_page
# .. toggle_name: contentstore.new_studio_mfe.use_new_course_team_page
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables the use of the new studio course team page mfe
@@ -476,3 +476,26 @@ def use_new_course_team_page(course_key):
Returns a boolean if new studio course team mfe is enabled
"""
return ENABLE_NEW_STUDIO_COURSE_TEAM_PAGE.is_enabled(course_key)
# .. toggle_name: contentstore.default_enable_flexible_peer_openassessments
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag turns on the force_on_flexible_peer_openassessments
# setting for course reruns or new courses, where enabled.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2023-06-27
# .. toggle_target_removal_date: 2024-01-27
# .. toggle_tickets: AU-1289
# .. toggle_warning:
DEFAULT_ENABLE_FLEXIBLE_PEER_OPENASSESSMENTS = CourseWaffleFlag(
f'{CONTENTSTORE_NAMESPACE}.default_enable_flexible_peer_openassessments', __name__)
def default_enable_flexible_peer_openassessments(course_key):
"""
Returns a boolean if ORA flexible peer grading should be toggled on for a
course rerun or new course. We expect this to be set at the organization
level to opt in/out of rolling forward this feature.
"""
return DEFAULT_ENABLE_FLEXIBLE_PEER_OPENASSESSMENTS.is_enabled(course_key)

View File

@@ -70,6 +70,7 @@ from cms.djangoapps.contentstore.toggles import (
use_new_unit_page,
use_new_updates_page,
use_new_video_uploads_page,
use_new_custom_pages,
)
from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_video_editor
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
@@ -402,19 +403,32 @@ def get_course_outline_url(course_locator) -> str:
return course_outline_url
def get_unit_url(course_locator) -> str:
def get_unit_url(course_locator, unit_locator) -> str:
"""
Gets course authoring microfrontend URL for unit page view.
"""
unit_url = None
if use_new_unit_page(course_locator):
mfe_base_url = get_course_authoring_url(course_locator)
course_mfe_url = f'{mfe_base_url}/container/'
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/container/{unit_locator}'
if mfe_base_url:
unit_url = course_mfe_url
return unit_url
def get_custom_pages_url(course_locator) -> str:
"""
Gets course authoring microfrontend URL for custom pages view.
"""
custom_pages_url = None
if use_new_custom_pages(course_locator):
mfe_base_url = get_course_authoring_url(course_locator)
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/custom-pages'
if mfe_base_url:
custom_pages_url = course_mfe_url
return custom_pages_url
def course_import_olx_validation_is_enabled():
"""
Check if course olx validation is enabled on course import.

View File

@@ -12,6 +12,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST
@@ -31,7 +32,8 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disa
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from ..exceptions import AssetNotFoundException, AssetSizeTooLargeException
from ..utils import reverse_course_url
from ..toggles import use_new_files_uploads_page
from ..utils import reverse_course_url, get_files_uploads_url
__all__ = ['assets_handler']
@@ -104,6 +106,9 @@ def _asset_index(request, course_key):
'''
course_block = modulestore().get_course(course_key)
if use_new_files_uploads_page(course_key):
return redirect(get_files_uploads_url(course_key))
return render_to_response('asset_index.html', {
'language_code': request.LANGUAGE_CODE,
'context_course': course_block,

View File

@@ -43,15 +43,16 @@ from ..helpers import (
)
from .preview import get_preview_fragment
from cms.djangoapps.contentstore.xblock_services import (
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
handle_xblock,
create_xblock_info,
load_services_for_studio,
get_block_info,
get_xblock,
delete_orphans,
usage_key_with_run,
)
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import usage_key_with_run
__all__ = [
"orphan_handler",

View File

@@ -10,6 +10,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseBadRequest
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET
from opaque_keys import InvalidKeyError
@@ -34,10 +35,10 @@ except ImportError:
content_staging_api = None
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url
from ..toggles import use_new_unit_page
from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url, get_unit_url
from ..helpers import get_parent_xblock, is_unit, xblock_type_display_name
from cms.djangoapps.contentstore.xblock_services.xblock_service import (
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
add_container_page_publishing_info,
create_xblock_info,
load_services_for_studio,
@@ -131,7 +132,6 @@ def container_handler(request, usage_key_string):
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
except ItemNotFoundError:
return HttpResponseBadRequest()
component_templates = get_component_templates(course)
ancestor_xblocks = []
parent = get_parent_xblock(xblock)
@@ -140,6 +140,9 @@ def container_handler(request, usage_key_string):
is_unit_page = is_unit(xblock)
unit = xblock if is_unit_page else None
if is_unit_page and use_new_unit_page(course.id):
return redirect(get_unit_url(course.id, unit.location))
is_first = True
block = xblock
@@ -199,7 +202,6 @@ def container_handler(request, usage_key_string):
user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request)
else:
user_clipboard = {"content": None}
return render_to_response('container.html', {
'language_code': request.LANGUAGE_CODE,
'context_course': course, # Needed only for display of menus at top of page.

View File

@@ -85,13 +85,28 @@ from ..course_group_config import (
from ..course_info_model import delete_course_update, get_course_updates, update_course_updates
from ..courseware_index import CoursewareSearchIndexer, SearchIndexingError
from ..tasks import rerun_course as rerun_course_task
from ..toggles import split_library_view_on_dashboard
from ..toggles import (
default_enable_flexible_peer_openassessments,
split_library_view_on_dashboard,
use_new_course_outline_page,
use_new_home_page,
use_new_updates_page,
use_new_advanced_settings_page,
use_new_grading_page,
use_new_schedule_details_page
)
from ..utils import (
add_instructor,
get_course_settings,
get_course_grading,
get_lms_link_for_item,
get_proctored_exam_settings_url,
get_course_outline_url,
get_studio_home_url,
get_updates_url,
get_advanced_settings_url,
get_grading_url,
get_schedule_details_url,
initialize_permissions,
remove_all_instructors,
reverse_course_url,
@@ -103,7 +118,7 @@ from ..utils import (
)
from .component import ADVANCED_COMPONENT_TYPES
from ..helpers import is_content_creator
from cms.djangoapps.contentstore.xblock_services.xblock_service import (
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
create_xblock_info,
)
from .library import (
@@ -533,6 +548,8 @@ def course_listing(request):
"""
List all courses and libraries available to the logged in user
"""
if use_new_home_page():
return redirect(get_studio_home_url())
optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled()
@@ -691,6 +708,8 @@ def course_index(request, course_key):
course_block = get_course_and_check_access(course_key, request.user, depth=None)
if not course_block:
raise Http404
if use_new_course_outline_page(course_key):
return redirect(get_course_outline_url(course_key))
lms_link = get_lms_link_for_item(course_block.location)
reindex_link = None
if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False):
@@ -975,6 +994,12 @@ def create_new_course(user, org, number, run, fields):
new_course = create_new_course_in_store(store_for_new_course, user, org, number, run, fields)
add_organization_course(org_data, new_course.id)
update_course_discussions_settings(new_course.id)
# Enable certain fields rolling forward, where configured
if default_enable_flexible_peer_openassessments(new_course.id):
new_course.force_on_flexible_peer_openassessments = True
modulestore().update_item(new_course, new_course.published_by)
return new_course
@@ -1038,6 +1063,10 @@ def rerun_course(user, source_course_key, org, number, run, fields, background=T
fields['enrollment_end'] = None
fields['video_upload_pipeline'] = {}
# Enable certain fields rolling forward, where configured
if default_enable_flexible_peer_openassessments(source_course_key):
fields['force_on_flexible_peer_openassessments'] = True
json_fields = json.dumps(fields, cls=EdxJSONEncoder)
args = [str(source_course_key), str(destination_course_key), user.id, json_fields]
@@ -1066,6 +1095,8 @@ def course_info_handler(request, course_key_string):
course_block = get_course_and_check_access(course_key, request.user)
if not course_block:
raise Http404
if use_new_updates_page(course_key):
return redirect(get_updates_url(course_key))
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
return render_to_response(
'course_info.html',
@@ -1150,6 +1181,8 @@ def settings_handler(request, course_key_string): # lint-amnesty, pylint: disab
with modulestore().bulk_operations(course_key):
course_block = get_course_and_check_access(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
if use_new_schedule_details_page(course_key):
return redirect(get_schedule_details_url(course_key))
settings_context = get_course_settings(request, course_key, course_block)
return render_to_response('settings.html', settings_context)
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): # pylint: disable=too-many-nested-blocks
@@ -1191,6 +1224,8 @@ def grading_handler(request, course_key_string, grader_index=None):
raise PermissionDenied()
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
if use_new_grading_page(course_key):
return redirect(get_grading_url(course_key))
grading_context = get_course_grading(course_key)
return render_to_response('settings_graders.html', grading_context)
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
@@ -1286,6 +1321,8 @@ def advanced_settings_handler(request, course_key_string):
advanced_dict.get('mobile_available')['deprecated'] = True
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
if use_new_advanced_settings_page(course_key):
return redirect(get_advanced_settings_url(course_key))
publisher_enabled = configuration_helpers.get_value_for_org(
course_block.location.org,
'ENABLE_PUBLISHER',

View File

@@ -24,8 +24,8 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disa
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from ..helpers import remove_entrance_exam_graders
from ..xblock_services.create_xblock import create_xblock
from cms.djangoapps.contentstore.xblock_services.xblock_service import delete_item
from cms.djangoapps.contentstore.xblock_storage_handlers.create_xblock import create_xblock
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import delete_item
__all__ = ['entrance_exam', ]

View File

@@ -19,6 +19,7 @@ from django.core.files import File
from django.core.files.storage import FileSystemStorage
from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseNotFound, StreamingHttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie
@@ -40,7 +41,8 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disa
from ..storage import course_import_export_storage
from ..tasks import CourseExportTask, CourseImportTask, export_olx, import_olx
from ..utils import reverse_course_url, reverse_library_url
from ..toggles import use_new_export_page, use_new_import_page
from ..utils import reverse_course_url, reverse_library_url, get_export_url, get_import_url
__all__ = [
'import_handler', 'import_status_handler',
@@ -89,6 +91,8 @@ def import_handler(request, course_key_string):
else:
return _write_chunk(request, courselike_key)
elif request.method == 'GET': # assume html
if use_new_import_page(courselike_key):
return redirect(get_import_url(courselike_key))
status_url = reverse_course_url(
"import_status_handler", courselike_key, kwargs={'filename': "fillerName"}
)
@@ -336,6 +340,8 @@ def export_handler(request, course_key_string):
export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE)
return JsonResponse({'ExportStatus': 1})
elif 'text/html' in requested_format:
if use_new_export_page(course_key):
return redirect(get_export_url(course_key))
return render_to_response('export.html', context)
else:
# Only HTML request format is supported (no JSON).

View File

@@ -43,7 +43,7 @@ from ..config.waffle import REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND
from ..utils import add_instructor, reverse_library_url
from .component import CONTAINER_TEMPLATES, get_component_templates
from ..helpers import is_content_creator
from cms.djangoapps.contentstore.xblock_services.xblock_service import create_xblock_info
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import create_xblock_info
from .user import user_with_role
__all__ = ['library_handler', 'manage_library_users']

View File

@@ -24,7 +24,7 @@ from xmodule.partitions.partitions_service import PartitionService
from xmodule.services import SettingsService, TeamsConfigurationService
from xmodule.studio_editable import has_author_view
from xmodule.util.sandboxing import SandboxService
from xmodule.util.xmodule_django import add_webpack_to_fragment
from xmodule.util.builtin_assets import add_webpack_js_to_fragment
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, XModuleMixin
from cms.djangoapps.xblock_config.models import StudioConfig
from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id, ENABLE_COPY_PASTE_FEATURE
@@ -149,13 +149,14 @@ def preview_layout_asides(block, context, frag, view_name, aside_frag_fns, wrap_
return result
def _prepare_runtime_for_preview(request, block):
def _prepare_runtime_for_preview(request, block, field_data):
"""
Sets properties in the runtime of the specified block that is
required for rendering block previews.
request: The active django request
block: An XBlock
field_data: Wrapped field data for previews
"""
course_id = block.location.course_key
@@ -198,6 +199,7 @@ def _prepare_runtime_for_preview(request, block):
deprecated_anonymous_user_id = anonymous_id_for_user(request.user, None)
services = {
"field-data": field_data,
"i18n": XBlockI18nService,
'mako': mako_service,
"settings": SettingsService(),
@@ -220,7 +222,7 @@ def _prepare_runtime_for_preview(request, block):
# Set up functions to modify the fragment produced by student_view
block.runtime.wrappers = wrappers
block.runtime.wrappers_asides = wrappers_asides
block.runtime._services.update(services) # pylint: disable=protected-access
block.runtime._runtime_services.update(services) # lint-amnesty, pylint: disable=protected-access
# xmodules can check for this attribute during rendering to determine if
# they are being rendered for preview (i.e. in Studio)
@@ -264,7 +266,9 @@ def _load_preview_block(request: Request, block: XModuleMixin):
else:
wrapper = partial(LmsFieldData, student_data=student_data)
_prepare_runtime_for_preview(request, block)
# wrap the _field_data upfront to pass to _prepare_runtime_for_preview
wrapped_field_data = wrapper(block._field_data) # pylint: disable=protected-access
_prepare_runtime_for_preview(request, block, wrapped_field_data)
block.bind_for_student(
request.user.id,
@@ -319,7 +323,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'language': getattr(course, 'language', None)
}
add_webpack_to_fragment(frag, "js/factories/xblock_validation")
add_webpack_js_to_fragment(frag, "js/factories/xblock_validation")
html = render_to_string('studio_xblock_wrapper.html', template_context)
frag = wrap_fragment(frag, html)

View File

@@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey, UsageKey
@@ -19,7 +20,8 @@ from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException, StaticT
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from ..utils import get_lms_link_for_item, get_pages_and_resources_url
from ..toggles import use_new_custom_pages
from ..utils import get_lms_link_for_item, get_pages_and_resources_url, get_custom_pages_url
__all__ = ["tabs_handler", "update_tabs_handler"]
@@ -63,7 +65,8 @@ def tabs_handler(request, course_key_string):
elif request.method == "GET": # assume html
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
# present in the same order they are displayed in LMS
if use_new_custom_pages(course_key):
return redirect(get_custom_pages_url(course_key))
tabs_to_render = list(get_course_tabs(course_item, request.user))
return render_to_response(

View File

@@ -62,7 +62,7 @@ from cms.djangoapps.contentstore.utils import (
duplicate_block,
update_from_source,
)
from cms.djangoapps.contentstore.xblock_services import xblock_service as item_module
from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers as item_module
from common.djangoapps.student.tests.factories import StaffFactory, UserFactory
from common.djangoapps.xblock_django.models import (
XBlockConfiguration,
@@ -74,7 +74,7 @@ from lms.djangoapps.lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
from ..component import component_handler, get_component_templates
from cms.djangoapps.contentstore.xblock_services.xblock_service import (
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
ALWAYS,
VisibilityState,
get_block_info,
@@ -1509,7 +1509,7 @@ class TestMoveItem(ItemTest):
validation = html.validate()
self.assertEqual(len(validation.messages), 0)
@patch("cms.djangoapps.contentstore.xblock_services.xblock_service.log")
@patch("cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.log")
def test_move_logging(self, mock_logger):
"""
Test logging when an item is successfully moved.

View File

@@ -39,7 +39,7 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, LibraryFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
from ..course import _deprecated_blocks_info, course_outline_initial_state, reindex_course_and_check_access
from cms.djangoapps.contentstore.xblock_services.xblock_service import VisibilityState, create_xblock_info
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import VisibilityState, create_xblock_info
class TestCourseIndex(CourseTestCase):

View File

@@ -27,7 +27,7 @@ from ..entrance_exam import (
update_entrance_exam
)
from cms.djangoapps.contentstore.helpers import GRADER_TYPES
from cms.djangoapps.contentstore.xblock_services.create_xblock import create_xblock
from cms.djangoapps.contentstore.xblock_storage_handlers.create_xblock import create_xblock
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})

View File

@@ -14,7 +14,7 @@ from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import reverse_usage_url
from openedx.core.lib.gating.api import GATING_NAMESPACE_QUALIFIER
from cms.djangoapps.contentstore.xblock_services.xblock_service import VisibilityState
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import VisibilityState
@ddt.ddt
@@ -57,7 +57,7 @@ class TestSubsectionGating(CourseTestCase):
)
self.seq2_url = reverse_usage_url('xblock_handler', self.seq2.location)
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.add_prerequisite')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.add_prerequisite')
def test_add_prerequisite(self, mock_add_prereq):
"""
Test adding a subsection as a prerequisite
@@ -69,7 +69,7 @@ class TestSubsectionGating(CourseTestCase):
)
mock_add_prereq.assert_called_with(self.course.id, self.seq1.location)
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.remove_prerequisite')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.remove_prerequisite')
def test_remove_prerequisite(self, mock_remove_prereq):
"""
Test removing a subsection as a prerequisite
@@ -81,7 +81,7 @@ class TestSubsectionGating(CourseTestCase):
)
mock_remove_prereq.assert_called_with(self.seq1.location)
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.set_required_content')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.set_required_content')
def test_add_gate(self, mock_set_required_content):
"""
Test adding a gated subsection
@@ -100,7 +100,7 @@ class TestSubsectionGating(CourseTestCase):
'100'
)
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.set_required_content')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.set_required_content')
def test_remove_gate(self, mock_set_required_content):
"""
Test removing a gated subsection
@@ -118,9 +118,9 @@ class TestSubsectionGating(CourseTestCase):
''
)
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.get_prerequisites')
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.get_required_content')
@patch('cms.djangoapps.contentstore.xblock_services.xblock_service.gating_api.is_prerequisite')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.get_prerequisites')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.get_required_content')
@patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.gating_api.is_prerequisite')
@ddt.data(
(90, None),
(None, 90),

View File

@@ -172,6 +172,7 @@ class GetPreviewHtmlTestCase(ModuleStoreTestCase):
self.assertFalse(modulestore().has_changes(modulestore().get_item(block.location)))
@XBlock.needs("field-data")
@XBlock.needs("i18n")
@XBlock.needs("mako")
@XBlock.needs("replace_urls")
@@ -203,6 +204,7 @@ class StudioXBlockServiceBindingTest(ModuleStoreTestCase):
self.user = UserFactory()
self.course = CourseFactory.create()
self.request = mock.Mock()
self.field_data = mock.Mock()
@XBlock.register_temp_plugin(PureXBlock, identifier='pure')
@ddt.data("user", "i18n", "field-data", "teams_configuration", "replace_urls")
@@ -211,7 +213,11 @@ class StudioXBlockServiceBindingTest(ModuleStoreTestCase):
Tests that the 'user' and 'i18n' services are provided by the Studio runtime.
"""
block = BlockFactory(category="pure", parent=self.course)
_prepare_runtime_for_preview(self.request, block)
_prepare_runtime_for_preview(
self.request,
block,
self.field_data,
)
service = block.runtime.service(block, expected_service)
self.assertIsNotNone(service)
@@ -235,9 +241,14 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
self.request = RequestFactory().get('/dummy-url')
self.request.user = self.user
self.request.session = {}
self.field_data = mock.Mock()
self.contentstore = contentstore()
self.block = BlockFactory(category="problem", parent=course)
_prepare_runtime_for_preview(self.request, block=self.block)
_prepare_runtime_for_preview(
self.request,
block=self.block,
field_data=mock.Mock(),
)
self.course = self.store.get_item(course.location)
def test_get_user_role(self):
@@ -292,7 +303,11 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
"""Test anonymous_user_id on a block which uses per-student anonymous IDs"""
# Create the runtime with the flag turned on.
block = BlockFactory(category="problem", parent=self.course)
_prepare_runtime_for_preview(self.request, block=block)
_prepare_runtime_for_preview(
self.request,
block=block,
field_data=mock.Mock(),
)
deprecated_anonymous_user_id = (
block.runtime.service(block, 'user').get_current_user().opt_attrs.get(ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID)
)
@@ -303,7 +318,11 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
"""Test anonymous_user_id on a block which uses per-course anonymous IDs"""
# Create the runtime with the flag turned on.
block = BlockFactory(category="lti", parent=self.course)
_prepare_runtime_for_preview(self.request, block=block)
_prepare_runtime_for_preview(
self.request,
block=block,
field_data=mock.Mock(),
)
anonymous_user_id = (
block.runtime.service(block, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID)

View File

@@ -5,6 +5,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST
@@ -20,6 +21,9 @@ from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRol
from common.djangoapps.util.json_request import JsonResponse, expect_json
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from ..toggles import use_new_course_team_page
from ..utils import get_course_team_url
__all__ = ['request_course_creator', 'course_team_handler']
@@ -55,6 +59,8 @@ def course_team_handler(request, course_key_string=None, email=None):
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
return _course_team_user(request, course_key, email)
elif request.method == 'GET': # assume html
if use_new_course_team_page(course_key):
return redirect(get_course_team_url(course_key))
return _manage_users(request, course_key)
else:
return HttpResponseNotFound()

View File

@@ -17,6 +17,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import FileResponse, HttpResponseNotFound
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_noop
@@ -57,7 +58,8 @@ from openedx.core.lib.api.view_utils import view_auth_classes
from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order
from ..models import VideoUploadConfig
from ..utils import reverse_course_url
from ..toggles import use_new_video_uploads_page
from ..utils import reverse_course_url, get_video_uploads_url
from ..video_utils import validate_video_image
from .course import get_course_and_check_access
@@ -700,7 +702,8 @@ def videos_index_html(course, pagination_conf=None):
context['active_transcript_preferences'] = get_transcript_preferences(str(course.id))
# Cached state for transcript providers' credentials (org-specific)
context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org)
if use_new_video_uploads_page(course.id):
return redirect(get_video_uploads_url(course.id))
return render_to_response('videos_index.html', context)

View File

@@ -1,6 +0,0 @@
"""
Xblock services that contain the business logic for xblock views.
"""
from .create_xblock import *
from .xblock_helpers import *
from .xblock_service import *

View File

@@ -0,0 +1,9 @@
"""
The xblock_storage_handlers folder contains service methods that implement the business logic for view endpoints
located in contentstore/views/block.py. It is renamed to xblock_storage_handlers to reflect its responsibility
of handling storage-related operations of xblocks, such as creation, retrieval, and deletion.
The view_handlers.py file includes business methods called by the view endpoints.
These methods, such as handle_xblock, delete_orphans, etc., interact with the required modulestore methods,
handle any errors, and aggregate and serialize data for the response.
"""

View File

@@ -116,15 +116,6 @@ CREATE_IF_NOT_FOUND = ["course_info"]
NEVER = lambda x: False
ALWAYS = lambda x: True
__all__ = [
"handle_xblock",
"create_xblock_info",
"load_services_for_studio",
"get_block_info",
"get_xblock",
"delete_orphans",
]
def _filter_entrance_exam_grader(graders):
"""

View File

@@ -32,7 +32,10 @@ define(
['\\[', '\\]'],
['[mathjax]', '[/mathjax]']
]
}
},
CommonHTML: { linebreaks: { automatic: true } },
SVG: { linebreaks: { automatic: true } },
"HTML-CSS": { linebreaks: { automatic: true } },
});
// In order to eliminate all flashing during interactive
@@ -42,6 +45,25 @@ define(
// the fast preview setting as shown in the context menu.
window.MathJax.Hub.processSectionDelay = 0;
window.MathJax.Hub.Configured();
window.addEventListener('resize', MJrenderer);
let t = -1;
let delay = 1000;
let oldWidth = document.documentElement.scrollWidth;
function MJrenderer() {
// don't rerender if the window is the same size as before
if (t >= 0) {
window.clearTimeout(t);
}
if (oldWidth !== document.documentElement.scrollWidth) {
t = window.setTimeout(function() {
oldWidth = document.documentElement.scrollWidth;
MathJax.Hub.Queue(["Rerender", MathJax.Hub]);
t = -1;
}, delay);
}
};
}
);
window.CodeMirror = CodeMirror;

View File

@@ -1437,7 +1437,6 @@ WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'bundles/',
'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-stats.json'),
'LOADER_CLASS': 'xmodule.util.xmodule_django.XModuleWebpackLoader',
},
'WORKERS': {
'BUNDLE_DIR_NAME': 'bundles/',
@@ -2704,6 +2703,7 @@ TEXTBOOKS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-
WIKI_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/course_wiki.html"
CUSTOM_PAGES_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html#adding-custom-pages"
COURSE_LIVE_HELP_URL = "https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/course_assets/course_live.html"
ORA_SETTINGS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/ora_settings.html"
# keys for big blue button live provider
COURSE_LIVE_GLOBAL_CREDENTIALS = {}
@@ -2726,11 +2726,3 @@ BRAZE_COURSE_ENROLLMENT_CANVAS_ID = ''
DISCUSSIONS_INCONTEXT_FEEDBACK_URL = ''
DISCUSSIONS_INCONTEXT_LEARNMORE_URL = ''
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.content_authoring.staged_content.static_filter_source.v1": {
"pipeline": [
"openedx.core.djangoapps.content_staging.filters.IgnoreLargeFiles",
]
}
}

View File

@@ -326,7 +326,6 @@ JWT_AUTH:
JWT_AUDIENCE: lms-key
JWT_AUTH_COOKIE_HEADER_PAYLOAD: edx-jwt-cookie-header-payload
JWT_AUTH_COOKIE_SIGNATURE: edx-jwt-cookie-signature
JWT_AUTH_REFRESH_COOKIE: edx-jwt-refresh-cookie
JWT_ISSUER: http://edx.devstack.lms:18000/oauth2
JWT_ISSUERS:
- AUDIENCE: lms-key

View File

@@ -49,6 +49,11 @@ LMS_ROOT_URL = f'http://{LMS_BASE}'
FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE
FRONTEND_REGISTER_URL = LMS_ROOT_URL + '/register'
################################## Video Pipeline Settings #########################
FEATURES['ENABLE_VIDEO_UPLOAD_PIPELINE'] = True
########################### PIPELINE #################################
# Skip packaging and optimization in development
@@ -99,7 +104,6 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.logging.LoggingPanel',
'debug_toolbar.panels.profiling.ProfilingPanel',
'debug_toolbar.panels.history.HistoryPanel',
)
@@ -300,6 +304,7 @@ EVENT_BUS_PRODUCER = 'edx_event_bus_redis.create_producer'
EVENT_BUS_REDIS_CONNECTION_URL = 'redis://:password@edx.devstack.redis:6379/'
EVENT_BUS_TOPIC_PREFIX = 'dev'
EVENT_BUS_CONSUMER = 'edx_event_bus_redis.RedisEventConsumer'
EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC = 'course-authoring-xblock-lifecycle'
################# New settings must go ABOVE this line #################
########################################################################

View File

@@ -601,6 +601,7 @@
"Copy Email To Editor": "Copiar el correo al editor",
"Copy Exam Code": "Copia el C\u00f3digo de el Examen",
"Copy row": "Copiar la fila",
"Copying": "Copiando",
"Correct failed component": "Corregir componente fallido",
"Cost": "Costo",
"Could not find Certificate Exception in the allowlist. Please refresh the page and try again": "No se pudo hallar Invalidaci\u00f3n del Certificado en la lista. Por favor, actualiza la p\u00e1gina e intenta nuevamente.",
@@ -1155,6 +1156,7 @@
"Last published {lastPublishedStart}{publishedOn}{lastPublishedEnd} by {publishedByStart}{publishedBy}{publishedByEnd}": "Last published {lastPublishedStart}{publishedOn}{lastPublishedEnd} por {publishedByStart}{publishedBy}{publishedByEnd}",
"Last updated": "\u00daltima actualizaci\u00f3n",
"Learn More": "Aprender m\u00e1s",
"Learn more": "Aprender m\u00e1s",
"Learn more about {license_name}": "Saber m\u00e1s sobre {license_name}",
"Learners are added to this cohort automatically.": "Los estudiantes son agregados autom\u00e1ticamente a esta cohorte.",
"Learners are added to this cohort only when you provide their email addresses or usernames on this page.": "Los estudiantes son agregados a esta cohorte solamente cuando se incluye la direcci\u00f3n de correo electr\u00f3nico o el nombre de usuario en esta p\u00e1gina.",
@@ -1285,6 +1287,7 @@
"New Password": "Nueva Contrase\u00f1a",
"New document": "Documento nuevo",
"New enrollment mode:": "Nuevo modo de inscripcion:",
"New files were added to this course's Files & Uploads": "Se agregaron nuevos archivos a la secci\u00f3n Archivos y Cargas de este curso",
"New window": "Nueva ventana",
"New {component_type}": "Nuevo {component_type}",
"Next": "Siguiente",
@@ -1408,6 +1411,7 @@
"Paste row after": "Pegar fila despu\u00e9s",
"Paste row before": "Pegar fila antes",
"Paste your embed code below:": "Pegar el c\u00f3digo para incrustar debajo:",
"Pasting": "Pegando",
"Path to Signature Image": "Ruta a la imagen de la firma",
"Pause": "Pausar",
"Pay {subscriptionPrice} after {trialLength}-day free trial": "Paga {subscriptionPrice} cuando termine la prueba gratuita de {trialLength} d\u00edas",
@@ -1755,6 +1759,7 @@
"Skip": "Omitir",
"Social Media Links": "Enlaces de redes sociales",
"Some Rights Reserved": "Algunos Derechos Reservados",
"Some errors occurred": "Se produjeron algunos errores",
"Some images in this post have been omitted": "Algunas im\u00e1genes en esta publicaci\u00f3n han sido omitidas",
"Something went wrong changing this enrollment. Please try again.": "Ocurri\u00f3 un error al cambiar esta inscripci\u00f3n. Por favor intenta nuevamente.",
"Something went wrong. Please try again later.": "Algo sali\u00f3 mal. Por favor intente de nuevo m\u00e1s tarde.",
@@ -1926,9 +1931,12 @@
"The following email addresses and/or usernames are invalid:": "El correo electr\u00f3nico y/o el nombre de usuario no son v\u00e1lidos:",
"The following errors were generated:": "Se generaron los siguientes errores:",
"The following file types are not allowed: ": "Los siguientes tipos de archivos son soportados:",
"The following files already exist in this course but don't match the version used by the component you pasted:": "Los siguientes archivos ya existen en este curso, pero no coinciden con la versi\u00f3n utilizada por el componente que peg\u00f3:",
"The following information is already a part of your {platform} profile. We've included it here for your application.": "La siguiente informaci\u00f3n ya es parte de su perfil en {platform} . La hemos incluido aqu\u00ed para su aplicaci\u00f3n",
"The following message will be displayed at the bottom of the courseware pages within your course:": "El siguiente mensaje ser\u00e1 mostrado al final de las p\u00e1ginas de los cursos. ",
"The following options are available for the {license_name} license.": "Las siguientes opciones est\u00e1n disponibles para {license_name} licencia",
"The following required files could not be added to the course:": "Los siguientes archivos obligatorios no se pudieron agregar al curso:",
"The following required files were imported to this course:": "Los siguientes archivos obligatorios no se pudieron agregar al curso:",
"The following users are no longer enrolled in the course:": "Los siguientes usuarios ya no est\u00e1n inscritos en el curso:",
"The following warnings were generated:": "Se generaron las siguientes advertencias:",
"The general category for this type of assignment, for example, Homework or Midterm Exam. This name is visible to learners.": "La categor\u00eda general para este tipo de asignaci\u00f3n, por ejemplo, Tareas o Examen trimestral. Este nombre es visible a los estudiantes.",
@@ -1945,6 +1953,8 @@
"The name that is used for ID verification and that appears on your certificates.": "El nombre que es usado para la verificaci\u00f3n de identidad y aparece en sus certificados.",
"The number of assignments of this type that will be dropped. The lowest scoring assignments are dropped first.": "El n\u00famero de asignaciones de este tipo que ser\u00e1n descartados. Las asignaciones con calificaciones m\u00e1s bajas ser\u00e1n las primeras en ser descartadas.",
"The number of subsections in the course that contain problems of this assignment type.": "El n\u00famero de subdivisiones del curso que contiene problemas de este tipo de asignaci\u00f3n.",
"The number of {type} assignments defined here does not match the current number of {type} assignments in the course:": "El n\u00famero de tareas {type} definido aqu\u00ed no coincide con el n\u00famero actual de tareas {type} en el curso:",
"The number of {type} assignments in the course matches the number defined here.": "El n\u00famero de asignaciones {type} en el curso coincide con el n\u00famero definido aqu\u00ed.",
"The onboarding service is temporarily unavailable. Please try again later.": "El servicio de inducci\u00f3n se temporalmente fuera de servicio. Por favor intenta de nuevo.",
"The organization that this signatory belongs to, as it should appear on certificates.": "La organizaci\u00f3n a la que pertenece el firmante, como debe aparecer en los certificados. ",
"The page \"{route}\" could not be found.": "La p\u00e1gina \"{route}\" no pudo ser encontrada.",
@@ -1967,6 +1977,7 @@
"The {cohortGroupName} cohort has been created. You can manually add students to this cohort below.": "El cohorte {cohortGroupName} ha sido creado. Puedes manualmente a\u00f1adir estudiantes a este cohorte.",
"There are currently {stuck_learners} learners in the waiting state, meaning they have not yet met all requirements for Peer Assessment. ": "Actualmente se encuentran {stuck_learners} estudiantes en el estado de espera, lo cual significa que ellos a\u00fan no cumplen con todos los requerimientos para el examen en parejas. ",
"There are invalid keywords in your email. Check the following keywords and try again.": "Hay palabras clave inv\u00e1lidas en tu correo. Por favor, comprueba las siguientes claves e int\u00e9ntalo de nuevo:",
"There are no assignments of this type in the course.": "No hay tareas de este tipo en el curso.",
"There are no posts in this topic yet.": "Todav\u00eda no hay publicaciones en este tema.",
"There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages.": "Ha habido una falla para exportar al XML al menos un componente. Se recomienda ir a la p\u00e1gina de edici\u00f3n y reparar el error antes de intentar otra exportaci\u00f3n. Por favor, verifique que todos los componentes en la p\u00e1gina son validos y no exhiben ninguna mensaje de error. ",
"There has been an error processing your survey.": "Ocurri\u00f3 un error al procesar tu encuesta.",
@@ -2268,6 +2279,7 @@
"Very low": "Muy bajo",
"Video Capture Error": "Error en la captura de v\u00eddeo",
"Video ID": "ID del video",
"Video Sharing": "Compartir videos",
"Video Source Language": "Idioma de la fuente del video",
"Video Status": "Estado del v\u00eddeo",
"Video duration is {humanizeDuration}": "La duraci\u00f3n del v\u00eddeo es {humanizeDuration}",
@@ -2309,6 +2321,7 @@
"Waiting": "Esperando",
"Want to make edX better for everyone?": "\u00bfQuieres hacer que edX sea mejor para todos?",
"Warning": "Advertencia",
"Warning: ": "Advertencia:",
"Warnings": "Advertencias",
"We ask you to activate your account to ensure it is really you creating the account and to prevent fraud.": "Necesitamos que active su cuenta para asegurarnos que es usted realmente el que est\u00e1 creando la cuenta y para prevenir fraude.",
"We couldn't create your account.": "No pudimos crear tu cuenta.",
@@ -2419,8 +2432,10 @@
"You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.": "Puede que tambi\u00e9n pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminaci\u00f3n, sigue las instrucciones para {htmlStart}imprimir o descargar un certificado{htmlEnd}.",
"You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. ": "Es posible que pueda completar el procedimiento de captura de im\u00e1genes sin ayuda, pero puede tomar un par de intentos de env\u00edo para obtener la posici\u00f3n correcta de la c\u00e1mara. El posicionamiento \u00f3ptimo de la c\u00e1mara var\u00eda con cada computadora, pero generalmente la mejor posici\u00f3n para la doma de la cabeza es de aproximadamente 12-18 pulgadas (30-45 cent\u00edmetros) de la c\u00e1mara, con la cabeza centrada en relaci\u00f3n con la pantalla de la computadora.",
"You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. ": "Es posible que pueda completar el procedimiento de captura de im\u00e1genes sin ayuda, pero puede tomar un par de intentos de env\u00edo para obtener la posici\u00f3n correcta de la c\u00e1mara. El posicionamiento \u00f3ptimo de la c\u00e1mara var\u00eda con cada computadora, pero en general, la mejor posici\u00f3n para una foto de una tarjeta de identificaci\u00f3n es de 8 a 12 pulgadas (20-30 cent\u00edmetros) de la c\u00e1mara, con la tarjeta de identificaci\u00f3n centrada en relaci\u00f3n con la c\u00e1mara.",
"You may need to update a file(s) manually": "Es posible que deba actualizar un archivo (s) manualmente",
"You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}": "Debes tener 13 a\u00f1os o m\u00e1s para compartir un perfil completo. Si tienes m\u00e1s de esta edad, aseg\u00farate que has especificado un a\u00f1o de nacimiento en {account_settings_page_link}",
"You must enter a valid email address in order to add a new team member": "Se debe introducir un email valido para adicionar un nuevo miembro en el equipo. ",
"You must have at least one undroppable <%- types %> assignment.": "Debes tener al menos una actividad no prescindible de <%- types %>.",
"You must provide a learner name.": "Debe ingresar un nombre.",
"You must select a session by {expiration_date} to access the course.": "Debes seleccionar una edici\u00f3n antes de {expiration_date} para acceder al curso.",
"You must select a session to access the course.": "Debe seleccionar una sesi\u00f3n para acceder al curso.",
@@ -2630,6 +2645,7 @@
"your course": "su curso",
"{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}": "{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}",
"{ReqTextSpanStart}Requirements{spanEnd}{lineBreak}{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}": "{ReqTextSpanStart}Requirements{spanEnd}{lineBreak}{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}",
"{assignment_count} {type} assignment(s) found:": "{assignment_count} {type} tarea(s) encontrada(s):",
"{browse_span_start}Browse teams in other topics{span_end} or {search_span_start}search teams{span_end} in this topic. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.": "Intenta {browse_span_start}explorar equipos en otros temas{span_end} o {search_span_start}busca equipos{span_end} en este tema. S\u00ed a\u00fan no puedes encontrar un equipo para unirte, {create_span_start}crea un nuevo equipo en este tema{span_end}.",
"{categoryText} in {parentDisplayname}": "{categoryText} en {parentDisplayname}",
"{currentCountOpeningTag}{currentCharacterCount}{currentCountClosingTag} of {maxCharacters}": "{currentCountOpeningTag}{currentCharacterCount}{currentCountClosingTag} de {maxCharacters}",

View File

@@ -23,9 +23,10 @@ function($, _, ViewUtils, BaseView, XBlock, HtmlUtils) {
var self = this,
view = this.view,
xblockInfo = this.model,
xblockUrl = xblockInfo.url();
xblockUrl = xblockInfo.url(),
querystring = window.location.search; // pass any querystring down to child views
return $.ajax({
url: decodeURIComponent(xblockUrl) + '/' + view,
url: decodeURIComponent(xblockUrl) + '/' + view + querystring,
type: 'GET',
cache: false,
headers: {Accept: 'application/json'},

View File

@@ -81,6 +81,12 @@
<!-- Configure and load MathJax -->
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
styles: {
'.MathJax_SVG>svg': { 'max-width': '100%' },
},
CommonHTML: { linebreaks: { automatic: true } },
SVG: { linebreaks: { automatic: true } },
"HTML-CSS": { linebreaks: { automatic: true } },
tex2jax: {
inlineMath: [
["\\(","\\)"],
@@ -92,6 +98,24 @@
]
}
});
window.addEventListener('resize', MJrenderer);
let t = -1;
let delay = 1000;
let oldWidth = document.documentElement.scrollWidth;
function MJrenderer() {
// don't rerender if the window is the same size as before
if (t >= 0) {
window.clearTimeout(t);
}
if (oldWidth !== document.documentElement.scrollWidth) {
t = window.setTimeout(function() {
oldWidth = document.documentElement.scrollWidth;
MathJax.Hub.Queue(["Rerender", MathJax.Hub]);
t = -1;
}, delay);
}
};
</script>
<script type="text/x-mathjax-config">
MathJax.Hub.signal.Interest(function(message) {