Skip to content

Commit 3905bd5

Browse files
Merge branch 'master' into ktyagi/primaryemail
2 parents d168dc4 + 272718e commit 3905bd5

200 files changed

Lines changed: 3707 additions & 14578 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/renovate.json5

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,50 @@
11
{
22
extends: [
3-
'config:recommended',
4-
'schedule:weekly',
5-
':automergeLinters',
6-
':automergeMinor',
7-
':automergeTesters',
8-
':enableVulnerabilityAlerts',
9-
':semanticCommits',
10-
':updateNotScheduled',
3+
'config:recommended', // Renovate base defaults: dependency dashboard, monorepo grouping, sane PR limits
4+
'schedule:weekly', // Only open new PRs once a week
5+
':automergeLinters', // Automerge linter updates (eslint, prettier, etc.)
6+
':automergeTesters', // Automerge test runner updates (jest, mocha, etc.)
7+
':enableVulnerabilityAlerts', // Open security PRs immediately, ignoring the weekly schedule
8+
':semanticCommits', // Use conventional commit format (fix(deps):, chore(deps):)
9+
':updateNotScheduled', // Allow vulnerability fixes to bypass the weekly schedule
1110
],
11+
12+
// Never auto-rebase PRs; let humans decide when to rebase
13+
rebaseWhen: 'never',
14+
15+
// Wait 3 days after a release before opening a PR, giving time for early bugs and
16+
// potentially malicious releases (e.g. supply chain attacks) to be detected
17+
minimumReleaseAge: '3 days',
18+
19+
// Only manage npm dependencies
20+
enabledManagers: [
21+
'npm',
22+
],
23+
24+
// Only create PRs during Eastern time (aligns with the weekly schedule)
25+
timezone: 'America/New_York',
26+
27+
// Cap the number of open Renovate PRs at any one time
28+
prConcurrentLimit: 3,
29+
30+
// Packages with known breaking changes or no active maintainer
31+
ignoreDeps: [
32+
'karma-spec-reporter',
33+
],
34+
1235
packageRules: [
1336
{
14-
matchDepTypes: [
15-
'devDependencies',
16-
],
17-
matchUpdateTypes: [
18-
'lockFileMaintenance',
19-
'minor',
20-
'patch',
21-
'pin',
37+
// Automerge minor and patch updates for @edx and @openedx scoped packages;
38+
// these are maintained by the same org so breakage is caught upstream
39+
matchPackageNames: [
40+
'/@edx/',
41+
'/@openedx/',
2242
],
23-
automerge: true,
24-
},
25-
{
2643
matchUpdateTypes: [
2744
'minor',
2845
'patch',
2946
],
3047
automerge: true,
31-
matchPackageNames: [
32-
'/@edx/',
33-
'/@openedx/',
34-
],
3548
},
3649
],
37-
ignoreDeps: [
38-
'karma-spec-reporter',
39-
],
40-
timezone: 'America/New_York',
41-
prConcurrentLimit: 3,
42-
enabledManagers: [
43-
'npm',
44-
],
4550
}

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
push:
99
branches:
1010
- master
11+
- release/*
1112

1213
concurrency:
1314
# We only need to be running tests for the latest commit on each PR

cms/djangoapps/contentstore/git_export_utils.py

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
from django.contrib.auth.models import User # pylint: disable=imported-auth-user
1414
from django.utils import timezone
1515
from django.utils.translation import gettext_lazy as _
16+
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
1617

18+
from openedx.core.djangoapps.content_libraries.api import extract_library_v2_zip_to_dir
1719
from xmodule.contentstore.django import contentstore
1820
from xmodule.modulestore.django import modulestore
19-
from xmodule.modulestore.xml_exporter import export_course_to_xml
21+
from xmodule.modulestore.xml_exporter import CourseLocator, export_course_to_xml, export_library_to_xml
2022

2123
log = logging.getLogger(__name__)
2224

@@ -66,10 +68,28 @@ def cmd_log(cmd, cwd):
6668
return output
6769

6870

69-
def export_to_git(course_id, repo, user='', rdir=None):
70-
"""Export a course to git."""
71+
def export_to_git(context_key, repo, user='', rdir=None):
72+
"""
73+
Export a course or library to git.
74+
75+
Args:
76+
context_key: LearningContextKey for the content to export
77+
repo (str): Git repository URL
78+
user (str): Optional username for git commit identity
79+
rdir (str): Optional custom directory name for the repository
80+
81+
Raises:
82+
GitExportError: For various git operation failures
83+
"""
7184
# pylint: disable=too-many-statements
7285

86+
# Validate context_key type and determine export function and content type label
87+
if not isinstance(context_key, (LibraryLocatorV2, LibraryLocator, CourseLocator)):
88+
raise TypeError(
89+
f"{context_key!r} for git export must be LibraryLocatorV2, LibraryLocator, "
90+
f"or CourseLocator, not {type(context_key)}"
91+
)
92+
7393
if not GIT_REPO_EXPORT_DIR:
7494
raise GitExportError(GitExportError.NO_EXPORT_DIR)
7595

@@ -128,15 +148,31 @@ def export_to_git(course_id, repo, user='', rdir=None):
128148
log.exception('Failed to pull git repository: %r', ex.output)
129149
raise GitExportError(GitExportError.CANNOT_PULL) from ex
130150

131-
# export course as xml before commiting and pushing
151+
# export content as xml (or zip for v2 libraries) before commiting and pushing
132152
root_dir = os.path.dirname(rdirp)
133-
course_dir = os.path.basename(rdirp).rsplit('.git', 1)[0]
153+
content_dir = os.path.basename(rdirp).rsplit('.git', 1)[0]
154+
155+
content_type_label = "course" if context_key.is_course else "library"
156+
157+
is_library_v2 = isinstance(context_key, LibraryLocatorV2)
158+
if is_library_v2:
159+
# V2 libraries use backup API with zip extraction
160+
content_export_func = extract_library_v2_zip_to_dir
161+
elif isinstance(context_key, LibraryLocator):
162+
content_export_func = export_library_to_xml
163+
else:
164+
content_export_func = export_course_to_xml
165+
134166
try:
135-
export_course_to_xml(modulestore(), contentstore(), course_id,
136-
root_dir, course_dir)
137-
except (OSError, AttributeError):
138-
log.exception('Failed export to xml')
139-
raise GitExportError(GitExportError.XML_EXPORT_FAIL) # pylint: disable=raise-missing-from # noqa: B904
167+
if is_library_v2:
168+
content_export_func(context_key, root_dir, content_dir, user)
169+
else:
170+
# V1 libraries and courses: use XML export (no user parameter)
171+
content_export_func(modulestore(), contentstore(), context_key,
172+
root_dir, content_dir)
173+
except (OSError, AttributeError) as ex:
174+
log.exception('Failed to export %s', content_type_label)
175+
raise GitExportError(GitExportError.XML_EXPORT_FAIL) from ex
140176

141177
# Get current branch if not already set
142178
if not branch:
@@ -160,9 +196,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
160196
ident = GIT_EXPORT_DEFAULT_IDENT
161197
time_stamp = timezone.now()
162198
cwd = os.path.abspath(rdirp)
163-
commit_msg = "Export from Studio at {time_stamp}".format( # noqa: UP032
164-
time_stamp=time_stamp,
165-
)
199+
commit_msg = f"Export {content_type_label} from Studio at {time_stamp}"
166200
try:
167201
cmd_log(['git', 'config', 'user.email', ident['email']], cwd)
168202
cmd_log(['git', 'config', 'user.name', ident['name']], cwd)
@@ -180,3 +214,10 @@ def export_to_git(course_id, repo, user='', rdir=None):
180214
except subprocess.CalledProcessError as ex:
181215
log.exception('Error running git push command: %r', ex.output)
182216
raise GitExportError(GitExportError.CANNOT_PUSH) from ex
217+
218+
log.info(
219+
'%s %s exported to git repository %s successfully',
220+
content_type_label.capitalize(),
221+
context_key,
222+
repo,
223+
)

cms/djangoapps/contentstore/management/commands/tests/test_git_export.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
import subprocess
1010
import unittest
1111
from io import StringIO
12+
from unittest.mock import patch
1213
from uuid import uuid4
1314

1415
from django.conf import settings
1516
from django.core.management import call_command
1617
from django.core.management.base import CommandError
1718
from django.test.utils import override_settings
18-
from opaque_keys.edx.locator import CourseLocator
19+
from opaque_keys.edx.locator import CourseLocator, LibraryLocator, LibraryLocatorV2
1920

2021
import cms.djangoapps.contentstore.git_export_utils as git_export_utils
2122
from cms.djangoapps.contentstore.git_export_utils import GitExportError
@@ -34,6 +35,9 @@ class TestGitExport(CourseTestCase):
3435
Excercise the git_export django management command with various inputs.
3536
"""
3637

38+
LIBRARY_V2_KEY = LibraryLocatorV2(org='TestOrg', slug='test-lib')
39+
LIBRARY_V1_KEY = LibraryLocator(org='TestOrg', library='test-lib')
40+
3741
def setUp(self):
3842
"""
3943
Create/reinitialize bare repo and folders needed
@@ -182,3 +186,52 @@ def test_no_change(self):
182186
with self.assertRaisesRegex(GitExportError, str(GitExportError.CANNOT_COMMIT)): # noqa: PT027
183187
git_export_utils.export_to_git(
184188
self.course.id, f'file://{self.bare_repo_dir}')
189+
190+
@patch('cms.djangoapps.contentstore.git_export_utils.cmd_log', return_value=b'main')
191+
@patch('cms.djangoapps.contentstore.git_export_utils.extract_library_v2_zip_to_dir')
192+
def test_library_v2_export_selects_correct_function(self, mock_extract, mock_cmd_log):
193+
"""
194+
When ``export_to_git`` is given a LibraryLocatorV2 key it must call
195+
``extract_library_v2_zip_to_dir`` and must not call the v1 XML export
196+
functions (``export_library_to_xml`` or ``export_course_to_xml``).
197+
cmd_log is mocked so no real git subprocess or repo state is needed.
198+
"""
199+
mock_extract.return_value = None
200+
repo_url = f'file://{self.bare_repo_dir}'
201+
202+
with patch('cms.djangoapps.contentstore.git_export_utils.export_course_to_xml') as mock_course_xml, \
203+
patch('cms.djangoapps.contentstore.git_export_utils.export_library_to_xml') as mock_lib_xml:
204+
git_export_utils.export_to_git(self.LIBRARY_V2_KEY, repo_url, self.user.username)
205+
206+
assert mock_extract.call_args[0][0] == self.LIBRARY_V2_KEY
207+
mock_course_xml.assert_not_called()
208+
mock_lib_xml.assert_not_called()
209+
210+
@patch('cms.djangoapps.contentstore.git_export_utils.cmd_log', return_value=b'main')
211+
@patch('cms.djangoapps.contentstore.git_export_utils.export_library_to_xml')
212+
def test_library_v1_export_selects_correct_function(self, mock_lib_xml, mock_cmd_log):
213+
"""
214+
When ``export_to_git`` is given a LibraryLocator (v1) key it must call
215+
``export_library_to_xml`` and must not call ``extract_library_v2_zip_to_dir``.
216+
cmd_log is mocked so no real git subprocess or repo state is needed.
217+
"""
218+
mock_lib_xml.return_value = None
219+
repo_url = f'file://{self.bare_repo_dir}'
220+
221+
with patch('cms.djangoapps.contentstore.git_export_utils.extract_library_v2_zip_to_dir') as mock_v2, \
222+
patch('cms.djangoapps.contentstore.git_export_utils.export_course_to_xml') as mock_course_xml:
223+
git_export_utils.export_to_git(self.LIBRARY_V1_KEY, repo_url, self.user.username)
224+
225+
assert mock_lib_xml.called
226+
mock_v2.assert_not_called()
227+
mock_course_xml.assert_not_called()
228+
229+
@patch('cms.djangoapps.contentstore.git_export_utils.extract_library_v2_zip_to_dir',
230+
side_effect=OSError('disk full'))
231+
def test_library_v2_export_failure_raises_xml_export_fail(self, mock_extract):
232+
"""
233+
If ``extract_library_v2_zip_to_dir`` raises, ``export_to_git`` should
234+
wrap it in ``GitExportError.XML_EXPORT_FAIL``.
235+
"""
236+
with self.assertRaisesRegex(GitExportError, str(GitExportError.XML_EXPORT_FAIL)): # noqa: PT027
237+
git_export_utils.export_to_git(self.LIBRARY_V2_KEY, f'file://{self.bare_repo_dir}')

cms/djangoapps/contentstore/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
from opaque_keys.edx.locator import LibraryContainerLocator
1818
from openedx_content.api import get_published_version
1919
from openedx_content.models_api import Component, Container
20-
from openedx_django_lib.fields import immutable_uuid_field, manual_date_time_field, ref_field
20+
21+
try:
22+
from openedx_django_lib.fields import immutable_uuid_field, manual_date_time_field, ref_field
23+
except ImportError: # pragma: no cover - runtime compatibility shim for different openedx_django_lib versions
24+
from openedx_django_lib.fields import immutable_uuid_field, manual_date_time_field
25+
from openedx_django_lib.fields import key_field as ref_field
2126

2227
logger = logging.getLogger(__name__)
2328

cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,33 @@ def get_use_new_custom_pages(self, obj):
6767

6868
def get_use_new_schedule_details_page(self, obj):
6969
"""
70-
Method to get the use_new_schedule_details_page switch
70+
Method to indicate whether we should use the new schedule details page.
71+
72+
This used to be based on a waffle flag but the flag is being removed so we
73+
default it to true for now until we can remove the need for it from the consumers
74+
of this serializer and the related APIs.
75+
76+
See https://github.com/openedx/edx-platform/issues/36275
7177
"""
72-
course_key = self.get_course_key()
73-
return toggles.use_new_schedule_details_page(course_key)
78+
return True
7479

7580
def get_use_new_advanced_settings_page(self, obj):
7681
"""
7782
Method to get the use_new_advanced_settings_page switch
7883
"""
79-
course_key = self.get_course_key()
80-
return toggles.use_new_advanced_settings_page(course_key)
84+
return True
8185

8286
def get_use_new_grading_page(self, obj):
8387
"""
84-
Method to get the use_new_grading_page switch
88+
Method to indicate whether we should use the new grading page.
89+
90+
This used to be based on a waffle flag but the flag is being removed so we
91+
default it to true for now until we can remove the need for it from the consumers
92+
of this serializer and the related APIs.
93+
94+
See https://github.com/openedx/edx-platform/issues/36275
8595
"""
86-
course_key = self.get_course_key()
87-
return toggles.use_new_grading_page(course_key)
96+
return True
8897

8998
def get_use_new_updates_page(self, obj):
9099
"""
@@ -102,15 +111,13 @@ def get_use_new_import_page(self, obj):
102111
"""
103112
Method to get the use_new_import_page switch
104113
"""
105-
course_key = self.get_course_key()
106-
return toggles.use_new_import_page(course_key)
114+
return True
107115

108116
def get_use_new_export_page(self, obj):
109117
"""
110118
Method to get the use_new_export_page switch
111119
"""
112-
course_key = self.get_course_key()
113-
return toggles.use_new_export_page(course_key)
120+
return True
114121

115122
def get_use_new_files_uploads_page(self, obj):
116123
"""
@@ -160,15 +167,13 @@ def get_use_new_course_team_page(self, obj):
160167
"""
161168
Method to get the use_new_course_team_page switch
162169
"""
163-
course_key = self.get_course_key()
164-
return toggles.use_new_course_team_page(course_key)
170+
return True
165171

166172
def get_use_new_certificates_page(self, obj):
167173
"""
168174
Method to get the use_new_certificates_page switch
169175
"""
170-
course_key = self.get_course_key()
171-
return toggles.use_new_certificates_page(course_key)
176+
return True
172177

173178
def get_use_new_textbooks_page(self, obj):
174179
"""
@@ -186,8 +191,7 @@ def get_use_new_group_configurations_page(self, obj):
186191
"""
187192
Method to get the use_new_group_configurations_page switch
188193
"""
189-
course_key = self.get_course_key()
190-
return toggles.use_new_group_configurations_page(course_key)
194+
return True
191195

192196
def get_enable_course_optimizer(self, obj):
193197
"""

0 commit comments

Comments
 (0)