Skip to content

Commit d547870

Browse files
committed
refactor: cleanup code and add query optimizations
1 parent 147789a commit d547870

2 files changed

Lines changed: 73 additions & 33 deletions

File tree

  • openedx_learning/apps/authoring/backup_restore
  • tests/openedx_learning/apps/authoring/backup_restore

openedx_learning/apps/authoring/backup_restore/zipper.py

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
import hashlib
66
import zipfile
77
from pathlib import Path
8-
from typing import Optional
8+
from typing import List, Optional
99

10-
from django.db.models import QuerySet
10+
from django.db.models import Prefetch, QuerySet
1111
from django.utils.text import slugify
1212

13-
from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
14-
from openedx_learning.apps.authoring.components.models import ComponentVersion, ComponentVersionContent
15-
from openedx_learning.apps.authoring.publishing import api as publishing_api
16-
from openedx_learning.apps.authoring.publishing.models import (
13+
from openedx_learning.api.authoring_models import (
14+
ComponentVersion,
15+
ComponentVersionContent,
16+
Content,
1717
LearningPackage,
1818
PublishableEntity,
1919
PublishableEntityVersion,
2020
)
21+
from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
22+
from openedx_learning.apps.authoring.publishing import api as publishing_api
2123

2224
TOML_PACKAGE_NAME = "package.toml"
2325

@@ -69,6 +71,49 @@ def create_folder(self, folder_path: Path, zip_file: zipfile.ZipFile) -> None:
6971
zip_file.writestr(zip_info, "") # Add explicit empty directory entry
7072
self.folders_already_created.add(folder_path)
7173

74+
def get_publishable_entities(self) -> QuerySet[PublishableEntity]:
75+
"""
76+
Retrieve the publishable entities associated with the learning package.
77+
Prefetches related data for efficiency.
78+
"""
79+
lp_id = self.learning_package.pk
80+
publishable_entities: QuerySet[PublishableEntity] = publishing_api.get_publishable_entities(lp_id)
81+
return (
82+
publishable_entities
83+
.select_related(
84+
"container",
85+
"component__component_type",
86+
"draft__version__componentversion",
87+
"published__version__componentversion",
88+
)
89+
.prefetch_related(
90+
Prefetch(
91+
"draft__version__componentversion__componentversioncontent_set",
92+
queryset=ComponentVersionContent.objects.select_related("content"),
93+
to_attr="prefetched_contents",
94+
),
95+
Prefetch(
96+
"published__version__componentversion__componentversioncontent_set",
97+
queryset=ComponentVersionContent.objects.select_related("content"),
98+
to_attr="prefetched_contents",
99+
),
100+
)
101+
)
102+
103+
def get_versions_to_write(self, entity: PublishableEntity):
104+
"""
105+
Get the versions of a publishable entity that should be written to the zip file.
106+
It retrieves both draft and published versions.
107+
"""
108+
draft_version: Optional[PublishableEntityVersion] = publishing_api.get_draft_version(entity)
109+
published_version: Optional[PublishableEntityVersion] = publishing_api.get_published_version(entity)
110+
111+
versions_to_write = [draft_version] if draft_version else []
112+
113+
if published_version and published_version != draft_version:
114+
versions_to_write.append(published_version)
115+
return versions_to_write
116+
72117
def create_zip(self, path: str) -> None:
73118
"""
74119
Creates a zip file containing the learning package data.
@@ -77,7 +122,6 @@ def create_zip(self, path: str) -> None:
77122
Raises:
78123
Exception: If the learning package cannot be found or if the zip creation fails.
79124
"""
80-
lp_id = self.learning_package.pk
81125

82126
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
83127
# Add the package.toml file
@@ -95,8 +139,7 @@ def create_zip(self, path: str) -> None:
95139
# ------ ENTITIES SERIALIZATION -------------
96140

97141
# get the publishable entities
98-
publishable_entities: QuerySet[PublishableEntity] = publishing_api.get_publishable_entities(lp_id)
99-
publishable_entities = publishable_entities.select_related("container", "component__component_type")
142+
publishable_entities: QuerySet[PublishableEntity] = self.get_publishable_entities()
100143

101144
for entity in publishable_entities:
102145
# entity: PublishableEntity = entity # Type hint for clarity
@@ -155,13 +198,7 @@ def create_zip(self, path: str) -> None:
155198
# Focusing on draft and published versions
156199

157200
# Get the draft and published versions
158-
draft_version: Optional[PublishableEntityVersion] = publishing_api.get_draft_version(entity)
159-
published_version: Optional[PublishableEntityVersion] = publishing_api.get_published_version(entity)
160-
161-
versions_to_write = [draft_version] if draft_version else []
162-
163-
if published_version and published_version != draft_version:
164-
versions_to_write.append(published_version)
201+
versions_to_write: List[PublishableEntityVersion] = self.get_versions_to_write(entity)
165202

166203
for version in versions_to_write:
167204
# Create a folder for the version
@@ -174,24 +211,27 @@ def create_zip(self, path: str) -> None:
174211
self.create_folder(static_folder, zipf)
175212

176213
# ------ COMPONENT STATIC CONTENT -------------
177-
# Get component version
178214
component_version: ComponentVersion = version.componentversion
179215

180216
# Get content data associated with this version
181-
# content_list: QuerySet[Content] = component_version.contents.all()
182-
content_list: QuerySet[ComponentVersionContent] = component_version.componentversioncontent_set.all() # pylint: disable=line-too-long # noqa: E501
217+
# type: ignore[attr-defined]
218+
content_list: QuerySet[ComponentVersionContent] = component_version.prefetched_contents
183219

184220
for component_version_content in content_list:
185-
content = component_version_content.content
221+
content: Content = component_version_content.content
222+
223+
# Important: The component_version_content.key contains implicitly
224+
# the file name and the file extension
225+
file_path = version_folder / component_version_content.key
186226

187227
if content.has_file and content.path:
188-
# Add the file to the static folder
189-
# file_path = static_folder / content.path
190-
file_path = static_folder / component_version_content.key
228+
# If has_file, we pull it from the file system
191229
with content.read_file() as f:
192230
file_data = f.read()
193-
zipf.writestr(str(file_path), file_data)
194231
elif not content.has_file and content.text:
195-
# Create file for the text file according to the mime_type attr
196-
text_file_path = static_folder / component_version_content.key
197-
zipf.writestr(str(text_file_path), content.text)
232+
# Otherwise, we use the text content as the file data
233+
file_data = content.text
234+
else:
235+
# If no file and no text, we skip this content
236+
continue
237+
zipf.writestr(str(file_path), file_data)

tests/openedx_learning/apps/authoring/backup_restore/test_backup.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
class LpDumpCommandTestCase(TestCase):
2121
"""
22-
Test serving static assets (Content files, via Component lookup).
22+
Test the lp_dump management command.
2323
"""
2424

2525
learning_package: LearningPackage
@@ -28,7 +28,7 @@ class LpDumpCommandTestCase(TestCase):
2828
@classmethod
2929
def setUpTestData(cls):
3030
"""
31-
Initialize our content data
31+
Initialize data for the whole TestCase
3232
"""
3333

3434
# Create a user for the test
@@ -86,7 +86,7 @@ def setUpTestData(cls):
8686
api.create_component_version_content(
8787
new_problem_version.pk,
8888
new_txt_content.pk,
89-
key="content/hello.txt",
89+
key="hello.txt",
9090
)
9191

9292
# Create a Draft component, one in each learning package
@@ -115,7 +115,7 @@ def setUpTestData(cls):
115115
api.create_component_version_content(
116116
new_html_version.pk,
117117
cls.html_asset_content.id,
118-
key="content/hello.html",
118+
key="static/hello.html",
119119
)
120120

121121
components = api.get_publishable_entities(cls.learning_package)
@@ -154,8 +154,8 @@ def check_zip_file_structure(self, zip_path: Path):
154154
"entities/xblock.v1/problem/my_published_example_386dce.toml",
155155

156156
# Entity static content files
157-
"entities/xblock.v1/html/my_draft_example_af06e1/component_versions/v2/static/content/hello.html",
158-
"entities/xblock.v1/problem/my_published_example_386dce/component_versions/v2/static/content/hello.txt",
157+
"entities/xblock.v1/html/my_draft_example_af06e1/component_versions/v2/static/hello.html",
158+
"entities/xblock.v1/problem/my_published_example_386dce/component_versions/v2/hello.txt",
159159
]
160160

161161
expected_paths = expected_directories + expected_files

0 commit comments

Comments
 (0)