Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/actions/verify-tests-count/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ runs:
- name: collect tests from all modules
shell: bash
run: |
echo "root_cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test cms/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "root_lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test lms/ openedx/ common/djangoapps/ common/lib/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "root_cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test -p no:warnings cms/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "root_lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test -p no:warnings lms/ openedx/ common/djangoapps/ common/lib/ -q | head -n -2 | wc -l)" >> $GITHUB_ENV

- name: get GHA unit test paths
shell: bash
Expand All @@ -19,8 +19,8 @@ runs:
- name: collect tests from GHA unit test shards
shell: bash
run: |
echo "cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test ${{ env.cms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test ${{ env.lms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "cms_unit_tests_count=$(pytest --collect-only --ds=cms.envs.test -p no:warnings ${{ env.cms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV
echo "lms_unit_tests_count=$(pytest --collect-only --ds=lms.envs.test -p no:warnings ${{ env.lms_unit_test_paths }} -q | head -n -2 | wc -l)" >> $GITHUB_ENV


- name: add unit tests count
Expand Down
26 changes: 26 additions & 0 deletions cms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
isort:skip_file
"""

# FAL-2248: Monkey patch django's get_storage_engine to work around long migrations times.
# This fixes a performance issue with database migrations in Ocim. We will need to keep
# this patch in our opencraft-release/* branches until edx-platform upgrades to Django 4.*
# which will include this commit:
# https://github.com/django/django/commit/518ce7a51f994fc0585d31c4553e2072bf816f76
import django.db.backends.mysql.introspection

# We monkey patch Kombu's entrypoints listing because scanning through this
# accounts for the majority of LMS/Studio startup time for tests, and we don't
Expand All @@ -22,3 +28,23 @@
# that shared_task will use this app, and also ensures that the celery
# singleton is always configured for the CMS.
from .celery import APP as CELERY_APP # lint-amnesty, pylint: disable=wrong-import-position


def get_storage_engine(self, cursor, table_name):
"""
This is a patched version of `get_storage_engine` that fixes a
performance issue with migrations. For more info see FAL-2248 and
https://github.com/django/django/pull/14766
"""
cursor.execute("""
SELECT engine
FROM information_schema.tables
WHERE table_name = %s
AND table_schema = DATABASE()""", [table_name])
result = cursor.fetchone()
if not result:
return self.connection.features._mysql_storage_engine # pylint: disable=protected-access
return result[0]


django.db.backends.mysql.introspection.DatabaseIntrospection.get_storage_engine = get_storage_engine
13 changes: 12 additions & 1 deletion cms/djangoapps/contentstore/config/waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"""


from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace, LegacyWaffleSwitchNamespace
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace, LegacyWaffleSwitchNamespace, WaffleFlag

from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

# Namespace
WAFFLE_NAMESPACE = 'studio'
LOG_PREFIX = 'Studio: '

# Switches
# TODO: Replace with WaffleSwitch(). See waffle() docstring.
Expand Down Expand Up @@ -81,3 +82,13 @@ def waffle_flags():
# .. toggle_warnings: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
CUSTOM_RELATIVE_DATES = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_relative_dates', module_name=__name__,)

# .. toggle_name: studio.prevent_staff_structure_deletion
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Prevents staff from deleting course structures
# .. toggle_use_cases: opt_in
# .. toggle_creation_date: 2021-06-25
PREVENT_STAFF_STRUCTURE_DELETION = WaffleFlag(
f'{WAFFLE_NAMESPACE}.prevent_staff_structure_deletion', __name__, LOG_PREFIX
)
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Permission definitions for the contentstore djangoapp
"""

from bridgekeeper import perms

from lms.djangoapps.courseware.rules import HasRolesRule

DELETE_COURSE_CONTENT = 'contentstore.delete_course_content'
perms[DELETE_COURSE_CONTENT] = HasRolesRule('instructor')
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/views/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ def serialize_certificate(certificate):
# Some keys are not required, such as the title override...
if certificate_data.get('course_title'):
certificate_response["course_title"] = certificate_data['course_title']
if certificate_data.get('course_description'):
certificate_response['course_description'] = certificate_data['course_description']

return certificate_response

Expand Down
15 changes: 15 additions & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.core.lib.courses import course_image_url
Expand Down Expand Up @@ -1156,6 +1157,13 @@ def settings_handler(request, course_key_string): # lint-amnesty, pylint: disab
verified_mode = CourseMode.verified_mode_for_course(course_key, include_expired=True)
upgrade_deadline = (verified_mode and verified_mode.expiration_datetime and
verified_mode.expiration_datetime.isoformat())

date_placeholder_format = configuration_helpers.get_value_for_org(
course_module.location.org,
'SCHEDULE_DETAIL_FORMAT',
settings.SCHEDULE_DETAIL_FORMAT
).upper()

settings_context = {
'context_course': course_module,
'course_locator': course_key,
Expand All @@ -1180,6 +1188,7 @@ def settings_handler(request, course_key_string): # lint-amnesty, pylint: disab
'enable_extended_course_details': enable_extended_course_details,
'upgrade_deadline': upgrade_deadline,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_module.id),
'date_placeholder_format': date_placeholder_format,
}
if is_prerequisite_courses_enabled():
courses, in_process_course_actions = get_courses_accessible_to_user(request)
Expand Down Expand Up @@ -1214,6 +1223,12 @@ def settings_handler(request, course_key_string): # lint-amnesty, pylint: disab
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
course_details = CourseDetails.fetch(course_key)

# Fetch the prefered timezone setup by the user
# and pass it as part of Json response
user_timezone = UserPreference.get_value(request.user, 'time_zone')
course_details.user_timezone = user_timezone

return JsonResponse(
course_details,
# encoder serializes dates, old locations, and instances
Expand Down
9 changes: 8 additions & 1 deletion cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from xblock.core import XBlock
from xblock.fields import Scope

from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG
from cms.djangoapps.contentstore.config.waffle import PREVENT_STAFF_STRUCTURE_DELETION, SHOW_REVIEW_RULES_FLAG
from cms.djangoapps.contentstore.permissions import DELETE_COURSE_CONTENT
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
from common.djangoapps.edxmako.services import MakoService
Expand Down Expand Up @@ -1356,6 +1357,12 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
else:
xblock_info['staff_only_message'] = False

xblock_info['show_delete_button'] = True
if PREVENT_STAFF_STRUCTURE_DELETION.is_enabled():
xblock_info['show_delete_button'] = (
user.has_perm(DELETE_COURSE_CONTENT, xblock) if user is not None else False
)

xblock_info['has_partition_group_components'] = has_children_visible_to_specific_partition_groups(
xblock
)
Expand Down
147 changes: 147 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.test.client import RequestFactory
from django.urls import reverse
from edx_proctoring.exceptions import ProctoredExamNotFoundException
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys import InvalidKeyError
from opaque_keys.edx.asides import AsideUsageKeyV2
from opaque_keys.edx.keys import CourseKey, UsageKey
Expand Down Expand Up @@ -46,6 +47,8 @@
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
from cms.djangoapps.contentstore.views import item as item_module
from cms.djangoapps.contentstore.config.waffle import PREVENT_STAFF_STRUCTURE_DELETION
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.xblock_django.models import (
XBlockConfiguration,
Expand Down Expand Up @@ -3430,3 +3433,147 @@ def test_self_paced_item_visibility_state(self, store_type):
# Check that in self paced course content has live state now
xblock_info = self._get_xblock_info(chapter.location)
self._verify_visibility_state(xblock_info, VisibilityState.live)

def test_staff_show_delete_button(self):
"""
Test delete button is *not visible* to user with CourseStaffRole
"""
# Add user as course staff
CourseStaffRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_staff_show_delete_button_with_waffle(self):
"""
Test delete button is *not visible* to user with CourseStaffRole and
PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course staff
CourseStaffRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertFalse(xblock_info['show_delete_button'])

def test_no_user_show_delete_button(self):
"""
Test delete button is *visible* when user attribute is not set on
xblock. This happens with ajax requests.
"""
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=None
)
self.assertTrue(xblock_info['show_delete_button'])

def test_no_user_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* when user attribute is not set on
xblock (this happens with ajax requests) and PREVENT_STAFF_STRUCTURE_DELETION waffle set.
"""

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=None
)

self.assertFalse(xblock_info['show_delete_button'])

def test_instructor_show_delete_button(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
"""
# Add user as course instructor
CourseInstructorRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_instructor_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course instructor
CourseInstructorRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertTrue(xblock_info['show_delete_button'])

def test_creator_show_delete_button(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
"""
# Add user as course creator
CourseCreatorRole(self.course_key).add_users(self.user)

# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)
self.assertTrue(xblock_info['show_delete_button'])

def test_creator_show_delete_button_with_waffle(self):
"""
Test delete button is *visible* to user with CourseInstructorRole only
and PREVENT_STAFF_STRUCTURE_DELETION waffle set
"""
# Add user as course creator
CourseCreatorRole(self.course_key).add_users(self.user)

with override_waffle_flag(PREVENT_STAFF_STRUCTURE_DELETION, active=True):
# Get xblock outline
xblock_info = create_xblock_info(
self.course,
include_child_info=True,
course_outline=True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical',
user=self.user
)

self.assertFalse(xblock_info['show_delete_button'])
38 changes: 38 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@
# templates.
STUDIO_NAME = _("Your Platform Studio")
STUDIO_SHORT_NAME = _("Studio")

# .. setting_name: SCHEDULE_DETAIL_FORMAT
# .. setting_default: MM/DD/YYYY'
# .. setting_description: Settings to configure the date format in Schedule & Details page
SCHEDULE_DETAIL_FORMAT = 'MM/DD/YYYY'
FEATURES = {
'GITHUB_PUSH': False,

Expand Down Expand Up @@ -501,6 +506,29 @@
# .. toggle_warnings: For consistency in user-experience, keep the value in sync with the setting of the same name
# in the LMS and CMS.
'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': False,

# .. toggle_name: FEATURES['DISABLE_UNENROLLMENT']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Set to True to disable self-unenrollments via REST API.
# This also hides the "Unenroll" button on the Learner Dashboard.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2021-10-11
# .. toggle_warnings: For consistency in user experience, keep the value in sync with the setting of the same name
# in the LMS and CMS.
# .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/429'
'DISABLE_UNENROLLMENT': False,

# .. toggle_name: FEATURES['ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Whether to enable the legacy MD5 hashing algorithm to generate anonymous user id
# instead of the newer SHAKE128 hashing algorithm
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2022-08-08
# .. toggle_target_removal_date: None
# .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/30832'
'ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID': False,
}

# .. toggle_name: ENABLE_COPPA_COMPLIANCE
Expand Down Expand Up @@ -2105,6 +2133,16 @@
# .. toggle_creation_date: 2016-06-30
ENABLE_COMPREHENSIVE_THEMING = False

# .. setting_name: CUSTOM_RESOURCE_TEMPLATES_DIRECTORY
# .. setting_default: None
# .. setting_description: Path to an existing directory of YAML files containing
# html content to be used with the subclasses of xmodule.x_module.ResourceTemplates.
# Default example templates can be found in xmodule/templates/html.
# Note that the extension used is ".yaml" and not ".yml".
# See xmodule.x_module.ResourceTemplates for usage.
# "CUSTOM_RESOURCE_TEMPLATES_DIRECTORY" : null
CUSTOM_RESOURCE_TEMPLATES_DIRECTORY = None

############################ Global Database Configuration #####################

DATABASE_ROUTERS = [
Expand Down
Loading