-
Notifications
You must be signed in to change notification settings - Fork 18
Unify smaller apps into one openedx_content app
#454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ormsbee
wants to merge
5
commits into
openedx:main
Choose a base branch
from
ormsbee:big-authoring
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,332
−433
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b5b6038
refactor: combine authoring apps to openedx_content
ormsbee 840b134
temp: single backcompat model
ormsbee 173b928
temp: thsi relies on migration squashing in openedx-platform to work
ormsbee ec30ec6
temp: add backcompat publishing models
ormsbee c5b72f3
temp: more ADR rewording
ormsbee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
docs/decisions/0020-merge-authoring-apps-into-openedx-content.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| 20. Merge authoring apps into openedx_content (using Applets) | ||
| ============================================================= | ||
|
|
||
| Context | ||
| ------- | ||
|
|
||
| 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: | ||
|
|
||
| #. 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. Moving models also introduces tricky dependencies with respect to migration ordering (described in more detail later in this document). We encountered this when considering how to extract container functionality out of the ``publishing`` app. | ||
| #. 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``. | ||
|
|
||
| There have also been minor inconveniences, like having a long list of ``INSTALLED_APPS`` to maintain in openedx-platform over time, or not having these tables easily grouped together in the Django admin interface. | ||
|
|
||
| Decisions | ||
| --------- | ||
|
|
||
| 1. Single openedx_content App | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| All existing authoring apps will be merged into one Django app named ``openedx_content``. Some consequences of this decision: | ||
|
|
||
| - The tables will be renamed to have the ``openedx_content`` label prefix. | ||
| - All management commands will be moved to the ``openedx_content`` app. | ||
|
|
||
| 2. Logical Separation via Applets | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| We will continue to keep internal API boundaries by using a new "Applets" convention. A Django applet is made to look like a miniature Django app, with its own ``models.py``, ``api.py``, and potentially other modules. The modules for the old authoring apps will be copied into various subpackages of ``openedx_content.applets``, such as ``openedx_content.applets.publishing``. Applets should respect each others' API boundaries, and never directly query models across applets. As before, we will use Import Linter to enforce dependency ordering. | ||
|
|
||
| 3. Package Restructuring | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| In one pull request, we are going to: | ||
|
|
||
| #. Rename the ``openedx_learning.apps.authoring`` package to be ``openedx_learning.apps.openedx_content``. (Note: We have discussed eventually moving this to a top level app, i.e. ``openedx_content`` instead of ``openedx_learning.apps.openedx_content``, but that will happen at a later time.) | ||
| #. 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, the ``migrations`` package for each existing app, and in some cases a minimal ``models.py`` that will hold the bare skeletons of a handful of models. This will allow for a smooth schema migration to transition the models from these individual apps to ``openedx_content``. | ||
| #. Move the actual models files and API logic for our existing authoring apps to the ``openedx_learning.apps.openedx_content.applets`` package. | ||
| #. 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. | ||
| #. Test packages will also be updated to follow the new structure. | ||
|
|
||
| 4. Database Migration Specifics | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| When Django runs migrations, it calculates both an internal model state, as well as running database operations. We are going to take advantage of the fact that these two can be separated using the ``SeparateDatabaseAndState`` operation. We will use this to remove the model state from the old authoring apps and create the model state in the new ``openedx_content`` app without having to run database operations. | ||
|
|
||
| There are a few high level constraints that we have to consider: | ||
|
|
||
| #. Existing openedx-platform migrations should not be modified. Existing openedx-platform migrations should remain unchanged. This is important to make sure that we do not introduce ordering inconsistencies for sites that have already run migrations for the old apps and are upgrading to a new release (e.g. Verawood). | ||
| #. The openedx-learning repo should not have any dependencies on openedx-platform migrations, because our dependencies strictly go in the other direction: openedx-platform calls openedx-learning, not the other way around. Furthermore, openedx-learning will often be run without openedx-platform, such as for local development or during CI. | ||
| #. Two of the openedx-platform apps that have foreign keys to openedx-learning models are only in Studio's INSTALLED_APPS (``contentstore`` and ``modulestore_migrator``), while ``content_libraries`` is installed in both Studio and LMS. Migrations may be run for LMS or Studio first, depending on the user and environment. Tutor runs LMS first, but we can't assume that will always be true. | ||
| #. We must support people who are installing from scratch, as well as those who are upgrading from the Ulmo release. | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| Therefore, the migrations will happen in the following order: | ||
|
|
||
| #. All ``backcompat.*`` apps migrations | ||
| #. The first ``openedx_content`` migration creates logical models without any database changes. | ||
| #. The second ``openedx_content`` migration renames the underlying tables. | ||
| #. Each the openedx-platform apps that had foreign keys to | ||
|
|
||
|
|
||
|
|
||
| Rejected | ||
|
|
||
| Dynamically | ||
|
|
||
|
|
||
| . | ||
| #. 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. | ||
| #. 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. | ||
| #. 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). | ||
|
|
||
| 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*. | ||
|
|
||
| So in order to enforce this ordering, we do the following: | ||
|
|
||
| * The ``openedx_content`` migration ``0001_initial`` requires that all ``backcompat.*`` migrations except the last ones removing model state are run. | ||
| * 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. | ||
| * The final ``backcompat.*`` migrations that remove model field state will list ``openedx_content`` app's ``0002_rename_tables_to_openedx_content`` as a dependency. | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| 4. The Bigger Picture | ||
| ~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| 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). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,4 @@ | |
| Open edX Learning ("Learning Core"). | ||
| """ | ||
|
|
||
| __version__ = "0.30.2" | ||
| __version__ = "0.31.0" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """ | ||
| Module for parts of the Learning Core API that exist to make it easier to use in | ||
| Django projects. | ||
| """ | ||
|
|
||
|
|
||
| def openedx_learning_apps_to_install(): | ||
| """ | ||
| Return all app names for appending to INSTALLED_APPS. | ||
|
|
||
| This function exists to better insulate edx-platform and potential plugins | ||
| over time, as we eventually plan to remove the backcompat apps. | ||
| """ | ||
| return [ | ||
| "openedx_learning.apps.openedx_content", | ||
| "openedx_learning.apps.openedx_content.backcompat.backup_restore", | ||
| "openedx_learning.apps.openedx_content.backcompat.collections", | ||
| "openedx_learning.apps.openedx_content.backcompat.components", | ||
| "openedx_learning.apps.openedx_content.backcompat.contents", | ||
| "openedx_learning.apps.openedx_content.backcompat.publishing", | ||
| "openedx_learning.apps.openedx_content.backcompat.sections", | ||
| "openedx_learning.apps.openedx_content.backcompat.subsections", | ||
| "openedx_learning.apps.openedx_content.backcompat.units", | ||
| ] |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm missing something here. None of the Platform migrations actually manipulate the database or depend on its actual state in any way. So why is the ordering so important? What happens if one runs all the Learning Core migrations first, then the platform migrations? Does Django block the
0002_rename_tables_to_openedx_contentmigration because it detects that the foreign keys in its State tracking will be out of sync with the database?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though the platform migrations don't touch database state, they do alter the tracked model state. So if openedx-learning migrations all run first, then Django gives an error when trying to later run the platform migrations because as far as it's concerned, the backcompat models no longer exist (which results in an error saying that the backcompat app itself does not exist).
Or to put in other words, the platform migration wants to switch a foreign key reference from
oel_publishing.LearningPackagetoopenedx_content.LearningPackage, but when it comes to compute what that actually means, it will detect thatoel_publishing.LearningPackagedoesn't exist becauseoel_publishing : 0011_remove_all_field_state_for_move_to_applethas already dropped those logical models (even if the tables are still there).If people always run CMS migrations before LMS migrations, this wouldn't be a problem. It hasn't been a problem in the past because the dependencies strictly went one way: openedx-learning could do whatever it wanted, and the openedx-platform apps would catch up later. It's different now because:
openedx_contentmodels before we rename the tables under those models.If it weren't for (2), we might be able to get away with leaving the backcompat app models around. I'm not sure about what kind of issues that might cause down the road though.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, this is getting way too convoluted, but could we create a final migration in
openedx_content(not backcompat) which re-creates the "old" tables likeoel_publishing.LearningPackageas an alias of the new tables, strictly in migration state? Then when the platform migration runs, Django will see both the old and the new model, see that they point to the same table, and run the migration as a no-op. We want to get rid of the old table references in the code, but I don't see a problem having them live on in the migration state indefinitely, unless we anticipate reusing those table namespaces.Alternately, just don't drop those old logical tables or another year or so.
Which one does tutor run first? If tutor runs CMS first, then I'd say it's not a huge deal, as most people won't hit the error and anyone who does can just
--fakethe migration and move on.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not really about the table, it's the logical model that's at the app level. In the Django migration model state, the platform app models don't have key references to the table
oel_publishing_learningpackage, they have have references to theoel_publishingapp modelLearningPackage, which happens to map to that table. But there's no way that I know of to makeopenedx_contenttake over the app namespaces of all the apps it's replacing.This could work. We could remove the backcompat migrations that drop all the model state. We keep the platform migrations that switch over the field references, but we make those happen after the rename of the tables. That way the ordering is always going to be consistent regardless of whether the LC migrations are run first in isolation, or if they're all mixed together. So it looks like:
openedx_contentmodels are created.oel_*tables are renamed toopenedx_contentSo then the backcompat models basically become phantoms with no backing tables. But that's the major downside to it—we'd have to keep those backcompat model modules around, with big fat warnings everywhere to not use or touch them, because any changes will prompt migrations to be created for them, and those migrations will always fail.
It looks like LMS runs first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, one complication of this is that we have to be careful about backwards relations from the user model, e.g. when various models have a "created_by" fkey to auth.User. We also can't have conflicting index names for things like unique indexes (the various Meta options we have). But since the only thing that is absolutely necessary to preserve in these are the models and primary key field of the things that are linked to from other migrations, I think that I can do the following:
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I feel like this PR fell off the migrations tree and hit every branch on the way down, but I'm playing with this locally and I think it's working now with the minimum bare models in backcompat. The ADR part describing all this needs a rewrite, and I probably want to rename the openedx-platform migrations, but this solution should mean:
lms migrateand then doingcms migratefrom either the "from scratch" state (new installs, CI), as well as Ulmo-or-later.backcompat.*apps, but they are very small shells of models that do nothing more than maintain their names and primary keys (so that foreign keys don't break).The downside is that if you start with a codebase that is not either fresh or in the Ulmo-or-later state, it may break because the real tables underlying the LC models will have been renamed by
openedx_content, and apps likemodulestore_migratorwill attempt to form keys to them.