Skip to content

Commit 7617fd4

Browse files
committed
Merge branch 'master' into rpenido/fal-4033-ignore-changes-on-user-state
2 parents 2d547e4 + 2598084 commit 7617fd4

File tree

113 files changed

+4308
-7892
lines changed

Some content is hidden

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

113 files changed

+4308
-7892
lines changed

cms/djangoapps/contentstore/admin.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from cms.djangoapps.contentstore.models import (
1414
BackfillCourseTabsConfig,
1515
CleanStaleCertificateAvailabilityDatesConfig,
16+
LearningContextLinksStatus,
17+
PublishableEntityLink,
1618
VideoUploadConfig
1719
)
1820
from cms.djangoapps.contentstore.outlines_regenerate import CourseOutlineRegenerate
@@ -86,6 +88,71 @@ class CleanStaleCertificateAvailabilityDatesConfigAdmin(ConfigurationModelAdmin)
8688
pass
8789

8890

91+
@admin.register(PublishableEntityLink)
92+
class PublishableEntityLinkAdmin(admin.ModelAdmin):
93+
"""
94+
PublishableEntityLink admin.
95+
"""
96+
fields = (
97+
"uuid",
98+
"upstream_block",
99+
"upstream_usage_key",
100+
"upstream_context_key",
101+
"downstream_usage_key",
102+
"downstream_context_key",
103+
"version_synced",
104+
"version_declined",
105+
"created",
106+
"updated",
107+
)
108+
readonly_fields = fields
109+
list_display = [
110+
"upstream_block",
111+
"upstream_usage_key",
112+
"downstream_usage_key",
113+
"version_synced",
114+
"updated",
115+
]
116+
search_fields = [
117+
"upstream_usage_key",
118+
"upstream_context_key",
119+
"downstream_usage_key",
120+
"downstream_context_key",
121+
]
122+
123+
def has_add_permission(self, request):
124+
return False
125+
126+
def has_change_permission(self, request, obj=None):
127+
return False
128+
129+
130+
@admin.register(LearningContextLinksStatus)
131+
class LearningContextLinksStatusAdmin(admin.ModelAdmin):
132+
"""
133+
LearningContextLinksStatus admin.
134+
"""
135+
fields = (
136+
"context_key",
137+
"status",
138+
"created",
139+
"updated",
140+
)
141+
readonly_fields = ("created", "updated")
142+
list_display = (
143+
"context_key",
144+
"status",
145+
"created",
146+
"updated",
147+
)
148+
149+
def has_add_permission(self, request):
150+
return False
151+
152+
def has_change_permission(self, request, obj=None):
153+
return False
154+
155+
89156
admin.site.register(BackfillCourseTabsConfig, ConfigurationModelAdmin)
90157
admin.site.register(VideoUploadConfig, ConfigurationModelAdmin)
91158
admin.site.register(CourseOutlineRegenerate, CourseOutlineRegenerateAdmin)

cms/djangoapps/contentstore/helpers.py

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
3030
import openedx.core.djangoapps.content_staging.api as content_staging_api
3131
import openedx.core.djangoapps.content_tagging.api as content_tagging_api
32+
from openedx.core.djangoapps.content_staging.data import LIBRARY_SYNC_PURPOSE
3233

3334
from .utils import reverse_course_url, reverse_library_url, reverse_usage_url
3435

@@ -262,6 +263,37 @@ class StaticFileNotices:
262263
error_files: list[str] = Factory(list)
263264

264265

266+
def _insert_static_files_into_downstream_xblock(
267+
downstream_xblock: XBlock, staged_content_id: int, request
268+
) -> StaticFileNotices:
269+
"""
270+
Gets static files from staged content, and inserts them into the downstream XBlock.
271+
"""
272+
static_files = content_staging_api.get_staged_content_static_files(staged_content_id)
273+
notices, substitutions = _import_files_into_course(
274+
course_key=downstream_xblock.context_key,
275+
staged_content_id=staged_content_id,
276+
static_files=static_files,
277+
usage_key=downstream_xblock.scope_ids.usage_id,
278+
)
279+
280+
# Rewrite the OLX's static asset references to point to the new
281+
# locations for those assets. See _import_files_into_course for more
282+
# info on why this is necessary.
283+
store = modulestore()
284+
if hasattr(downstream_xblock, "data") and substitutions:
285+
data_with_substitutions = downstream_xblock.data
286+
for old_static_ref, new_static_ref in substitutions.items():
287+
data_with_substitutions = data_with_substitutions.replace(
288+
old_static_ref,
289+
new_static_ref,
290+
)
291+
downstream_xblock.data = data_with_substitutions
292+
if store is not None:
293+
store.update_item(downstream_xblock, request.user.id)
294+
return notices
295+
296+
265297
def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) -> tuple[XBlock | None, StaticFileNotices]:
266298
"""
267299
Import a block (along with its children and any required static assets) from
@@ -299,31 +331,43 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) ->
299331
tags=user_clipboard.content.tags,
300332
)
301333

302-
# Now handle static files that need to go into Files & Uploads.
303-
static_files = content_staging_api.get_staged_content_static_files(user_clipboard.content.id)
304-
notices, substitutions = _import_files_into_course(
305-
course_key=parent_key.context_key,
306-
staged_content_id=user_clipboard.content.id,
307-
static_files=static_files,
308-
usage_key=new_xblock.scope_ids.usage_id,
309-
)
310-
311-
# Rewrite the OLX's static asset references to point to the new
312-
# locations for those assets. See _import_files_into_course for more
313-
# info on why this is necessary.
314-
if hasattr(new_xblock, 'data') and substitutions:
315-
data_with_substitutions = new_xblock.data
316-
for old_static_ref, new_static_ref in substitutions.items():
317-
data_with_substitutions = data_with_substitutions.replace(
318-
old_static_ref,
319-
new_static_ref,
320-
)
321-
new_xblock.data = data_with_substitutions
322-
store.update_item(new_xblock, request.user.id)
334+
notices = _insert_static_files_into_downstream_xblock(new_xblock, user_clipboard.content.id, request)
323335

324336
return new_xblock, notices
325337

326338

339+
def import_static_assets_for_library_sync(downstream_xblock: XBlock, lib_block: XBlock, request) -> StaticFileNotices:
340+
"""
341+
Import the static assets from the library xblock to the downstream xblock
342+
through staged content. Also updates the OLX references to point to the new
343+
locations of those assets in the downstream course.
344+
345+
Does not deal with permissions or REST stuff - do that before calling this.
346+
347+
Returns a summary of changes made to static files in the destination
348+
course.
349+
"""
350+
if not lib_block.runtime.get_block_assets(lib_block, fetch_asset_data=False):
351+
return StaticFileNotices()
352+
if not content_staging_api:
353+
raise RuntimeError("The required content_staging app is not installed")
354+
staged_content = content_staging_api.stage_xblock_temporarily(lib_block, request.user.id, LIBRARY_SYNC_PURPOSE)
355+
if not staged_content:
356+
# expired/error/loading
357+
return StaticFileNotices()
358+
359+
store = modulestore()
360+
try:
361+
with store.bulk_operations(downstream_xblock.context_key):
362+
# Now handle static files that need to go into Files & Uploads.
363+
# If the required files already exist, nothing will happen besides updating the olx.
364+
notices = _insert_static_files_into_downstream_xblock(downstream_xblock, staged_content.id, request)
365+
finally:
366+
staged_content.delete()
367+
368+
return notices
369+
370+
327371
def _fetch_and_set_upstream_link(
328372
copied_from_block: str,
329373
copied_from_version_num: int,
@@ -548,6 +592,9 @@ def _import_files_into_course(
548592
if result is True:
549593
new_files.append(file_data_obj.filename)
550594
substitutions.update(substitution_for_file)
595+
elif substitution_for_file:
596+
# substitutions need to be made because OLX references to these files need to be updated
597+
substitutions.update(substitution_for_file)
551598
elif result is None:
552599
pass # This file already exists; no action needed.
553600
else:
@@ -618,8 +665,8 @@ def _import_file_into_course(
618665
contentstore().save(content)
619666
return True, {clipboard_file_path: f"static/{import_path}"}
620667
elif current_file.content_digest == file_data_obj.md5_hash:
621-
# The file already exists and matches exactly, so no action is needed
622-
return None, {}
668+
# The file already exists and matches exactly, so no action is needed except substitutions
669+
return None, {clipboard_file_path: f"static/{import_path}"}
623670
else:
624671
# There is a conflict with some other file that has the same name.
625672
return False, {}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Management command to recreate upstream-dowstream links in PublishableEntityLink for course(s).
3+
4+
This command can be run for all the courses or for given list of courses.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import logging
10+
from datetime import datetime, timezone
11+
12+
from django.core.management.base import BaseCommand, CommandError
13+
from django.utils.translation import gettext as _
14+
from opaque_keys import InvalidKeyError
15+
from opaque_keys.edx.keys import CourseKey
16+
17+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
18+
19+
from ...tasks import create_or_update_upstream_links
20+
21+
log = logging.getLogger(__name__)
22+
23+
24+
class Command(BaseCommand):
25+
"""
26+
Recreate links for course(s) in PublishableEntityLink table.
27+
28+
Examples:
29+
# Recreate upstream links for two courses.
30+
$ ./manage.py cms recreate_upstream_links --course course-v1:edX+DemoX.1+2014 \
31+
--course course-v1:edX+DemoX.2+2015
32+
# Force recreate upstream links for one or more courses including processed ones.
33+
$ ./manage.py cms recreate_upstream_links --course course-v1:edX+DemoX.1+2014 \
34+
--course course-v1:edX+DemoX.2+2015 --force
35+
# Recreate upstream links for all courses.
36+
$ ./manage.py cms recreate_upstream_links --all
37+
# Force recreate links for all courses including completely processed ones.
38+
$ ./manage.py cms recreate_upstream_links --all --force
39+
# Delete all links and force recreate links for all courses
40+
$ ./manage.py cms recreate_upstream_links --all --force --replace
41+
"""
42+
43+
def add_arguments(self, parser):
44+
parser.add_argument(
45+
'--course',
46+
metavar=_('COURSE_KEY'),
47+
action='append',
48+
help=_('Recreate links for xblocks under given course keys. For eg. course-v1:edX+DemoX.1+2014'),
49+
default=[],
50+
)
51+
parser.add_argument(
52+
'--all',
53+
action='store_true',
54+
help=_(
55+
'Recreate links for xblocks under all courses. NOTE: this can take long time depending'
56+
' on number of course and xblocks'
57+
),
58+
)
59+
parser.add_argument(
60+
'--force',
61+
action='store_true',
62+
help=_('Recreate links even for completely processed courses.'),
63+
)
64+
parser.add_argument(
65+
'--replace',
66+
action='store_true',
67+
help=_('Delete all and create links for given course(s).'),
68+
)
69+
70+
def handle(self, *args, **options):
71+
"""
72+
Handle command
73+
"""
74+
courses = options['course']
75+
should_process_all = options['all']
76+
force = options['force']
77+
replace = options['replace']
78+
time_now = datetime.now(tz=timezone.utc)
79+
if not courses and not should_process_all:
80+
raise CommandError('Either --course or --all argument should be provided.')
81+
82+
if should_process_all and courses:
83+
raise CommandError('Only one of --course or --all argument should be provided.')
84+
85+
if should_process_all:
86+
courses = CourseOverview.get_all_course_keys()
87+
for course in courses:
88+
log.info(f"Start processing upstream->dowstream links in course: {course}")
89+
try:
90+
CourseKey.from_string(str(course))
91+
except InvalidKeyError:
92+
log.error(f"Invalid course key: {course}, skipping..")
93+
continue
94+
create_or_update_upstream_links.delay(str(course), force=force, replace=replace, created=time_now)

0 commit comments

Comments
 (0)