Merge branch 'openedx:master' into edx-depr31
This commit is contained in:
@@ -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}/$',
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', ]
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 *
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 #################
|
||||
########################################################################
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user