Skip to content

Commit a4fbd29

Browse files
committed
Ensure channel version database exists
1 parent 93f2810 commit a4fbd29

2 files changed

Lines changed: 132 additions & 37 deletions

File tree

contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ def setUp(self):
6666
self.channel_id = uuid.UUID(int=42).hex
6767
self.channel_version = 1
6868

69-
db_path = os.path.join(
69+
self.versioned_db_path = os.path.join(
7070
test_db_root_dir,
7171
settings.DB_ROOT,
7272
f"{self.channel_id}-{self.channel_version}.sqlite3",
7373
)
74-
open(db_path, "w").close()
74+
open(self.versioned_db_path, "w").close()
7575

76-
with using_content_database(db_path):
76+
with using_content_database(self.versioned_db_path):
7777
call_command(
7878
"migrate",
7979
app_label=KolibriContentConfig.label,
@@ -87,10 +87,10 @@ def setUp(self):
8787
version=self.channel_version,
8888
)
8989

90-
self.db_path_without_version = os.path.join(
90+
self.unversioned_db_path = os.path.join(
9191
test_db_root_dir, settings.DB_ROOT, f"{self.channel_id}.sqlite3"
9292
)
93-
shutil.copyfile(db_path, self.db_path_without_version)
93+
shutil.copyfile(self.versioned_db_path, self.unversioned_db_path)
9494

9595
def tearDown(self):
9696
self._temp_directory_ctx.__exit__(None, None, None)
@@ -99,7 +99,7 @@ def tearDown(self):
9999
super().tearDown()
100100

101101
@mock.patch("kolibri_public.utils.export_channel_to_kolibri_public.ChannelMapper")
102-
def test_export_channel_to_kolibri_public__existing_version(
102+
def test_export_channel_to_kolibri_public__existing_version__versioned(
103103
self, mock_channel_mapper
104104
):
105105
categories = {
@@ -127,6 +127,29 @@ def test_export_channel_to_kolibri_public__existing_version(
127127
)
128128
mock_channel_mapper.return_value.run.assert_called_once_with()
129129

130+
@mock.patch("kolibri_public.utils.export_channel_to_kolibri_public.ChannelMapper")
131+
def test_export_channel_to_kolibri_public__existing_version__unversioned(
132+
self, mock_channel_mapper
133+
):
134+
os.remove(self.versioned_db_path)
135+
136+
export_channel_to_kolibri_public(
137+
channel_id=self.channel_id,
138+
channel_version=1,
139+
public=True,
140+
categories=None,
141+
countries=None,
142+
)
143+
144+
mock_channel_mapper.assert_called_once_with(
145+
channel=self.exported_channel_metadata,
146+
channel_version=1,
147+
public=True,
148+
categories=None,
149+
countries=None,
150+
)
151+
mock_channel_mapper.return_value.run.assert_called_once_with()
152+
130153
@mock.patch("kolibri_public.utils.export_channel_to_kolibri_public.ChannelMapper")
131154
def test_export_channel_to_kolibri_public__without_version(
132155
self, mock_channel_mapper
@@ -144,6 +167,13 @@ def test_export_channel_to_kolibri_public__without_version(
144167
)
145168
mock_channel_mapper.return_value.run.assert_called_once_with()
146169

170+
def test_export_channel_to_kolibri_public__bad_channel(self):
171+
with self.assertRaises(FileNotFoundError):
172+
export_channel_to_kolibri_public(
173+
channel_id="dummy_id",
174+
channel_version=1,
175+
)
176+
147177
def test_export_channel_to_kolibri_public__bad_version(self):
148178
with self.assertRaises(FileNotFoundError):
149179
export_channel_to_kolibri_public(

contentcuration/kolibri_public/utils/export_channel_to_kolibri_public.py

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@
1616
logger = logging.getLogger(__file__)
1717

1818

19+
class using_temp_migrated_database:
20+
"""
21+
A wrapper context manager for read-only access to a content database
22+
that might not have all current migrations applied. Works by copying
23+
the database to a temporary file, applying migrations to this temporary
24+
database and then using this temporary database.
25+
"""
26+
27+
def __init__(self, database_path):
28+
self.database_path = database_path
29+
self._inner_mgr = None
30+
31+
def __enter__(self):
32+
self._named_temporary_file_mgr = tempfile.NamedTemporaryFile(suffix=".sqlite3")
33+
self.temp_database_file = self._named_temporary_file_mgr.__enter__()
34+
35+
shutil.copy(self.database_path, self.temp_database_file.name)
36+
self.temp_database_file.seek(0)
37+
38+
with using_content_database(self.temp_database_file.name):
39+
# Run migration to handle old content databases published prior to current fields being added.
40+
call_command(
41+
"migrate",
42+
app_label=KolibriContentConfig.label,
43+
database=get_active_content_database(),
44+
)
45+
46+
self._inner_mgr = using_content_database(self.temp_database_file.name)
47+
self._inner_mgr.__enter__()
48+
49+
def __exit__(self, exc_type, exc_val, exc_tb):
50+
self._inner_mgr.__exit__(exc_type, exc_val, exc_tb)
51+
self._named_temporary_file_mgr.__exit__(exc_type, exc_val, exc_tb)
52+
53+
1954
def export_channel_to_kolibri_public(
2055
channel_id,
2156
channel_version=None,
@@ -25,36 +60,66 @@ def export_channel_to_kolibri_public(
2560
):
2661
logger.info("Putting channel {} into kolibri_public".format(channel_id))
2762

28-
if channel_version is not None:
29-
db_filename = "{id}-{version}.sqlite3".format(
63+
versioned_db_filename = "{id}-{version}.sqlite3".format(
64+
id=channel_id, version=channel_version
65+
)
66+
unversioned_db_filename = "{id}.sqlite3".format(id=channel_id)
67+
68+
versioned_db_path = storage.path(
69+
os.path.join(settings.DB_ROOT, versioned_db_filename)
70+
)
71+
unversioned_db_path = storage.path(
72+
os.path.join(settings.DB_ROOT, unversioned_db_filename)
73+
)
74+
75+
if channel_version is None:
76+
db_path = unversioned_db_path
77+
else:
78+
db_path = versioned_db_path
79+
_possibly_migrate_unversioned_database(
80+
channel_id=channel_id,
81+
channel_version=channel_version,
82+
unversioned_db_path=unversioned_db_path,
83+
versioned_db_path=versioned_db_path,
84+
)
85+
86+
with using_temp_migrated_database(db_path):
87+
channel = ExportedChannelMetadata.objects.get(id=channel_id)
88+
logger.info(
89+
"Found channel {} for id: {} mapping now".format(channel.name, channel_id)
90+
)
91+
mapper = ChannelMapper(
92+
channel=channel,
93+
channel_version=channel_version,
94+
public=public,
95+
categories=categories,
96+
countries=countries,
97+
)
98+
mapper.run()
99+
100+
101+
def _possibly_migrate_unversioned_database(
102+
channel_id,
103+
channel_version,
104+
unversioned_db_path,
105+
versioned_db_path,
106+
):
107+
"""
108+
Older channels may only have a single database file and not
109+
versioned databases. If this is the case and the requested channel version
110+
is present in the single database file, this function copies the database to a file
111+
containing the version in the filename.
112+
"""
113+
if os.path.exists(versioned_db_path) or not os.path.exists(unversioned_db_path):
114+
return
115+
116+
with using_temp_migrated_database(unversioned_db_path):
117+
contains_requested_version = ExportedChannelMetadata.objects.filter(
30118
id=channel_id, version=channel_version
119+
).exists()
120+
121+
if contains_requested_version:
122+
logger.info(
123+
f"Migrating unversioned database {unversioned_db_path} to versioned database {versioned_db_path}"
31124
)
32-
else:
33-
db_filename = "{id}.sqlite3".format(id=channel_id)
34-
db_location = os.path.join(settings.DB_ROOT, db_filename)
35-
36-
with storage.open(db_location) as storage_file:
37-
with tempfile.NamedTemporaryFile(suffix=".sqlite3") as db_file:
38-
shutil.copyfileobj(storage_file, db_file)
39-
db_file.seek(0)
40-
with using_content_database(db_file.name):
41-
# Run migration to handle old content databases published prior to current fields being added.
42-
call_command(
43-
"migrate",
44-
app_label=KolibriContentConfig.label,
45-
database=get_active_content_database(),
46-
)
47-
channel = ExportedChannelMetadata.objects.get(id=channel_id)
48-
logger.info(
49-
"Found channel {} for id: {} mapping now".format(
50-
channel.name, channel_id
51-
)
52-
)
53-
mapper = ChannelMapper(
54-
channel=channel,
55-
channel_version=channel_version,
56-
public=public,
57-
categories=categories,
58-
countries=countries,
59-
)
60-
mapper.run()
125+
shutil.copy(unversioned_db_path, versioned_db_path)

0 commit comments

Comments
 (0)