Skip to content

Commit b5b6038

Browse files
committed
refactor: combine authoring apps to openedx_content
This commit makes the following major refactorings: - All apps previously under openedx_learning/apps/authoring have now effectively been merged together to one openedx_content app. - The models and logic for those apps continues to be encapsulated in what we're calling "applets", inside of openedx_content/applets/... - In order to facilitate smooth database migrations, the AppConfigs and database migrations for the old apps continue to be preserved in openedx_content/backcompat/... - A new openedx_learning.api.django.openedx_learning_apps_to_install() has been created to make it easier for openedx-platform to get all the necessary apps to install. The rationale for this change is detailed in docs/decisions/0020-authoring-as-one-app.rst, but the short version is that we hope this arrangement will allow us to keep many of the benefits of having small apps (easy to reason about), while also making it easier to refactor internally. Refactoring is currently hindered by the difficulty in moving models across apps. The name change away from "authoring" also reflects that we intend to use these APIs and models on the "learning" side of things as well, e.g. when reading content for rendering to a student. In the longer term, we will probably make openedx_content a top-level package in this repo, but that would be a later step. Most of this commit is just shuffling things around and renaming references, but the truly complicated bits are around the sequencing of database migrations, particularly the ones removing model state from the old apps, and transferring that state into the new openedx_content app without changing the actual database state. We also have some hacky logic in openedx_content/apps.py in order to properly patch migration dependencies so that we don't break existing openedx-platform migrations. This also bumps the version to 0.31.0.
1 parent 07b699d commit b5b6038

File tree

179 files changed

+2141
-433
lines changed

Some content is hidden

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

179 files changed

+2141
-433
lines changed

.annotation_safe_list.yml

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,43 @@ auth.User:
1919
".. pii_retirement": "consumer_api"
2020
contenttypes.ContentType:
2121
".. no_pii:": "This model has no PII"
22-
oel_collections.Collection:
22+
openedx_content.Collection:
2323
".. no_pii:": "This model has no PII"
24-
oel_collections.CollectionPublishableEntity:
24+
openedx_content.CollectionPublishableEntity:
2525
".. no_pii:": "This model has no PII"
26-
oel_components.Component:
26+
openedx_content.Component:
2727
".. no_pii:": "This model has no PII"
28-
oel_components.ComponentType:
28+
openedx_content.ComponentType:
2929
".. no_pii:": "This model has no PII"
30-
oel_components.ComponentVersion:
30+
openedx_content.ComponentVersion:
3131
".. no_pii:": "This model has no PII"
32-
oel_components.ComponentVersionContent:
32+
openedx_content.ComponentVersionContent:
3333
".. no_pii:": "This model has no PII"
34-
oel_contents.Content:
34+
openedx_content.Content:
3535
".. no_pii:": "This model has no PII"
36-
oel_contents.MediaType:
36+
openedx_content.MediaType:
3737
".. no_pii:": "This model has no PII"
38-
oel_publishing.Container:
38+
openedx_content.Container:
3939
".. no_pii:": "This model has no PII"
40-
oel_publishing.ContainerVersion:
40+
openedx_content.ContainerVersion:
4141
".. no_pii:": "This model has no PII"
42-
oel_publishing.Draft:
42+
openedx_content.Draft:
4343
".. no_pii:": "This model has no PII"
44-
oel_publishing.EntityList:
44+
openedx_content.EntityList:
4545
".. no_pii:": "This model has no PII"
46-
oel_publishing.EntityListRow:
46+
openedx_content.EntityListRow:
4747
".. no_pii:": "This model has no PII"
48-
oel_publishing.LearningPackage:
48+
openedx_content.LearningPackage:
4949
".. no_pii:": "This model has no PII"
50-
oel_publishing.PublishLog:
50+
openedx_content.PublishLog:
5151
".. no_pii:": "This model has no PII"
52-
oel_publishing.PublishLogRecord:
52+
openedx_content.PublishLogRecord:
5353
".. no_pii:": "This model has no PII"
54-
oel_publishing.PublishableEntity:
54+
openedx_content.PublishableEntity:
5555
".. no_pii:": "This model has no PII"
56-
oel_publishing.PublishableEntityVersion:
56+
openedx_content.PublishableEntityVersion:
5757
".. no_pii:": "This model has no PII"
58-
oel_publishing.Published:
58+
openedx_content.Published:
5959
".. no_pii:": "This model has no PII"
6060
oel_tagging.ObjectTag:
6161
".. no_pii:": "This model has no PII"
@@ -65,17 +65,17 @@ oel_tagging.TagImportTask:
6565
".. no_pii:": "This model has no PII"
6666
oel_tagging.Taxonomy:
6767
".. no_pii:": "This model has no PII"
68-
oel_sections.Section:
68+
openedx_content.Section:
6969
".. no_pii:": "This model has no PII"
70-
oel_sections.SectionVersion:
70+
openedx_content.SectionVersion:
7171
".. no_pii:": "This model has no PII"
72-
oel_subsections.Subsection:
72+
openedx_content.Subsection:
7373
".. no_pii:": "This model has no PII"
74-
oel_subsections.SubsectionVersion:
74+
openedx_content.SubsectionVersion:
7575
".. no_pii:": "This model has no PII"
76-
oel_units.Unit:
76+
openedx_content.Unit:
7777
".. no_pii:": "This model has no PII"
78-
oel_units.UnitVersion:
78+
openedx_content.UnitVersion:
7979
".. no_pii:": "This model has no PII"
8080
social_django.Association:
8181
".. no_pii:": "This model has no PII"

.gitignore

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ venv/
7575
!.vscode/settings.json.example
7676

7777
# Media files (for uploads)
78-
media/
78+
/media/
7979

8080
# Media files generated during test runs
81-
test_media/
81+
/test_media/
82+
83+
# uv stuff
84+
.lock
85+
CACHEDIR.TAG
86+
pyvenv.cfg

.importlinter

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,24 @@ layers=
3636
openedx_learning.api.authoring
3737

3838
# The "backup_restore" app handle the new export and import mechanism.
39-
openedx_learning.apps.authoring.backup_restore
39+
openedx_learning.apps.openedx_content.applets.backup_restore
4040

4141
# The "components" app is responsible for storing versioned Components,
4242
# which is Open edX Studio terminology maps to things like individual
4343
# Problems, Videos, and blocks of HTML text. This is also the type we would
4444
# associate with a single "leaf" XBlock–one that is not a container type and
4545
# has no child elements.
46-
openedx_learning.apps.authoring.components
46+
openedx_learning.apps.openedx_content.applets.components
4747

4848
# The "contents" app stores the simplest pieces of binary and text data,
4949
# without versioning information. These belong to a single Learning Package.
50-
openedx_learning.apps.authoring.contents
50+
openedx_learning.apps.openedx_content.applets.contents
5151

5252
# The "collections" app stores arbitrary groupings of PublishableEntities.
5353
# Its only dependency should be the publishing app.
54-
openedx_learning.apps.authoring.collections
54+
openedx_learning.apps.openedx_content.applets.collections
5555

5656
# The lowest layer is "publishing", which holds the basic primitives needed
5757
# to create Learning Packages and manage the draft and publish states for
5858
# various types of content.
59-
openedx_learning.apps.authoring.publishing
59+
openedx_learning.apps.openedx_content.applets.publishing
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
20. openedx_content as an Umbrella App of Smaller Applets
2+
=========================================================
3+
4+
Context
5+
-------
6+
7+
Up to this point, Learning Core has used many small apps with a narrow focus (e.g. ``components``, ``collections``, etc.) in order to make each individual app simpler to reason about. This has been useful overall, but it has made refactoring more cumbersome. For instance:
8+
9+
#. Moving models between apps is tricky, requiring the use of Django's ``SeparateDatabaseAndState`` functionality to fake a deletion in one app and a creation in another without actually altering the database. It also requires doctoring the migration files for models in other repos that might have foreign key relations to the model being moved, so that they're pointing to the new ``app_label``. This will be an issue when we try to extract container-related models and logic out of publishing and into a new ``containers`` app.
10+
#. Renaming an app is also cumbersome, because the process requires creating a new app and transitioning the models over. This came up when trying to rename the ``contents`` app to ``media``.
11+
12+
There have also been minor inconveniences, like having a long list of ``INSTALLED_APPS`` to maintain in edx-platform over time, or not having these tables easily grouped together in the Django admin interface.
13+
14+
Decisions
15+
---------
16+
17+
1. Single openedx_content App
18+
~~~~~~~~~~~~~~~~~~~~~~~
19+
20+
All existing authoring apps will be merged into one Django app (``openedx_learning.app.openedx_content``). Some consequences of this decision:
21+
22+
- The tables will be renamed to have the ``openedx_content`` label prefix.
23+
- All management commands will be moved to the ``openedx_content`` app.
24+
25+
2. Logical Separation via Applets
26+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27+
28+
We will continue to keep internal API boundaries between individual applets, and use the ``api.py`` modules. This is both to insulate applets from implementation changes in other applets, as well as to provide a set of APIs that third-party plugins can utilize. As before, we will use Import Linter to enforce dependency ordering.
29+
30+
3. Restructuring Specifics
31+
~~~~~~~~~~~~~~~~~~~~~~~~~~
32+
33+
In one pull request, we are going to:
34+
35+
#. Rename the ``openedx_learning.apps.authoring`` package to be ``openedx_learning.apps.openedx_content``.
36+
#. Create bare shells of the existing ``authoring`` apps (``backup_restore``, ``collections``, ``components``, ``contents``, ``publishing``, ``sections``, ``subsections``, ``units``), and move them to the ``openedx_learning.apps.openedx_content.backcompat`` package. These shells will have an ``apps.py`` file and the ``migrations`` package for each existing app. This will allow for a smooth schema migration to transition the models from these individual apps to ``openedx_content``.
37+
#. Move the actual models files and API logic for our existing authoring apps to the ``openedx_learning.apps.openedx_content.applets`` package.
38+
#. Convert the top level ``openedx_learning.apps.openedx_content`` package to be a Django app. The top level ``admin.py``, ``api.py``, and ``models.py`` modules will do wildcard imports from the corresponding modules across all applet packages.
39+
40+
In terms of model migrations, all existing apps will have a final migration that uses ``SeparateDatabaseAndState`` to remove all model state, but make no actual database changes. The initial ``openedx_content`` app migration will then also use ``SeparateDatabaseAndState`` to create the model state without doing any actual database operations. The next ``openedx_content`` app migration will rename all existing database tables to use the ``openedx_content`` prefix, for uniformity.
41+
42+
The ordering of these migrations is important, and existing edx-platform migrations should remain unchanged. This is important to make sure that we do not introduce ordering inconsistencies for existing installations that are upgrading.
43+
44+
Therefore, the migrations will happen in the following order:
45+
46+
#. All ``backcompat.*`` apps migrations except for the final ones that delete model state. This takes us up to where migrations would already be before we make any changes.
47+
#. The ``openedx_content`` app's ``0001_intial`` migration that adds model state without changing the database. At this point, model state exists for the same models in all the old ``backcompat.*`` apps as well as the new ``openedx_content`` app.
48+
#. edx-platform apps that had foreign keys to old ``backcompat.*`` apps models will need to be switched to point to the new ``openedx_content`` app models. This will likewise be done without a database change, because they're still pointing to the same tables and columns.
49+
#. Now that edx-platform references have been updated, we can delete the model state from the old ``backcompat.*`` apps and rename the underlying tables (in either order).
50+
51+
The tricky part is to make sure that the old ``backcompat.*`` apps models still exist when the edx-platform migrations to move over the references runs. This is problematic because the edx-platform migrations can only specify that they run *after the new openedx_content models are created*. They cannot specify that they run *before the old backcompat models are dropped*.
52+
53+
So in order to enforce this ordering, we do the following:
54+
55+
* The ``openedx_content`` migration ``0001_initial`` requires that all ``backcompat.*`` migrations except the last ones removing model state are run.
56+
* The ``openedx_content`` migration ``0002_rename_tables_to_openedx_content`` migration requires that the edx-platform migrations changing refrences over run. This is important anyway, because we want to make sure those reference changes happen before we change any table names.
57+
* The final ``backcompat.*`` migrations that remove model field state will list ``openedx_content`` app's ``0002_rename_tables_to_openedx_content`` as a dependency.
58+
59+
A further complication is that ``openedx_learning`` will often run its migrations without edx-platform present (e.g. for CI or standalone dev purposes), so we can't force ``0002_rename_tables_to_openedx_content`` in the ``openedx_content`` app to have references to edx-platform migrations. To get around this, we dynamically inject those migration dependencies only if we detect those edx-platform apps exist in the currently loaded Django project. This injection happens in the ``apps.py`` initialization for the ``openedx_content`` app.
60+
61+
The final complication is that we want these migration dependencies to be the same regardless of whether you're running edx-platform migrations with the LMS or CMS (Studio) settings, or we run the risk of getting into an inconsistent state and dropping the old models before all the edx-platform apps can run their migrations to move their references. To do this, we have to make sure that the edx-platform apps that reference Learning Core models are present in the ``INSTALLED_APPS`` for both configurations.
62+
63+
4. The Bigger Picture
64+
~~~~~~~~~~~~~~~~~~~~~
65+
66+
This practice means that the ``openedx_content`` Django app corresponds to a Subdomain in Domain Driven Design terminology, with each applet being a Bounded Context. We call these "Applets" instead of "Bounded Contexts" because we don't want it to get confused for Django's notion of Contexts and Context Processors (or Python's notion of Context Managers).

olx_importer/management/commands/load_components.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
from django.db import transaction
2929

3030
# Model references to remove
31-
from openedx_learning.apps.authoring.components import api as components_api
32-
from openedx_learning.apps.authoring.contents import api as contents_api
33-
from openedx_learning.apps.authoring.publishing import api as publishing_api
31+
from openedx_learning.apps.openedx_content.applets.components import api as components_api
32+
from openedx_learning.apps.openedx_content.applets.contents import api as contents_api
33+
from openedx_learning.apps.openedx_content.applets.publishing import api as publishing_api
3434

3535
SUPPORTED_TYPES = ["problem", "video", "html"]
3636
logger = logging.getLogger(__name__)

openedx_learning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Open edX Learning ("Learning Core").
33
"""
44

5-
__version__ = "0.30.2"
5+
__version__ = "0.31.0"

openedx_learning/api/authoring.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,14 @@
22
This is the public API for content authoring in Learning Core.
33
44
This is the single ``api`` module that code outside of the
5-
``openedx_learning.apps.authoring.*`` package should import from. It will
5+
``openedx_learning.apps.openedx_content.*`` package should import from. It will
66
re-export the public functions from all api.py modules of all authoring apps. It
77
may also implement its own convenience APIs that wrap calls to multiple app
88
APIs.
99
"""
1010
# These wildcard imports are okay because these api modules declare __all__.
1111
# pylint: disable=wildcard-import
12-
from ..apps.authoring.backup_restore.api import *
13-
from ..apps.authoring.collections.api import *
14-
from ..apps.authoring.components.api import *
15-
from ..apps.authoring.contents.api import *
16-
from ..apps.authoring.publishing.api import *
17-
from ..apps.authoring.sections.api import *
18-
from ..apps.authoring.subsections.api import *
19-
from ..apps.authoring.units.api import *
12+
from ..apps.openedx_content.api import *
2013

2114
# This was renamed after the authoring API refactoring pushed this and other
2215
# app APIs into the openedx_learning.api.authoring module. Here I'm aliasing to

openedx_learning/api/authoring_models.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
"""
88
# These wildcard imports are okay because these modules declare __all__.
99
# pylint: disable=wildcard-import
10-
from ..apps.authoring.collections.models import *
11-
from ..apps.authoring.components.models import *
12-
from ..apps.authoring.contents.models import *
13-
from ..apps.authoring.publishing.models import *
14-
from ..apps.authoring.sections.models import *
15-
from ..apps.authoring.subsections.models import *
16-
from ..apps.authoring.units.models import *
10+
from ..apps.openedx_content.applets.collections.models import *
11+
from ..apps.openedx_content.applets.components.models import *
12+
from ..apps.openedx_content.applets.contents.models import *
13+
from ..apps.openedx_content.applets.publishing.models import *
14+
from ..apps.openedx_content.applets.sections.models import *
15+
from ..apps.openedx_content.applets.subsections.models import *
16+
from ..apps.openedx_content.applets.units.models import *

openedx_learning/api/django.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Module for parts of the Learning Core API that exist to make it easier to use in
3+
Django projects.
4+
"""
5+
6+
7+
def openedx_learning_apps_to_install():
8+
"""
9+
Return all app names for appending to INSTALLED_APPS.
10+
11+
This function exists to better insulate edx-platform and potential plugins
12+
over time, as we eventually plan to remove the backcompat apps.
13+
"""
14+
return [
15+
"openedx_learning.apps.openedx_content",
16+
"openedx_learning.apps.openedx_content.backcompat.backup_restore",
17+
"openedx_learning.apps.openedx_content.backcompat.collections",
18+
"openedx_learning.apps.openedx_content.backcompat.components",
19+
"openedx_learning.apps.openedx_content.backcompat.contents",
20+
"openedx_learning.apps.openedx_content.backcompat.publishing",
21+
"openedx_learning.apps.openedx_content.backcompat.sections",
22+
"openedx_learning.apps.openedx_content.backcompat.subsections",
23+
"openedx_learning.apps.openedx_content.backcompat.units",
24+
]

openedx_learning/apps/authoring/components/apps.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)