-
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
base: main
Are you sure you want to change the base?
Conversation
authoring app
|
After this transition, I think I'd want to eventually refactor the |
|
I'm killing this auto-discovery code from the PR because I don't think the convenience is worth the loss of code introspection in editors and CI, but I want to preserve it here in case we need to write some applet-traversing code again: """
This was an attempt to make cute and clever code to dynamically discover and
import all applet modules of a certain type for aggregation purposes (e.g. the
authoring/models.py file importing from all applet models.py files). This code
actually does work, but I only realized after buildling it that it breaks
editor introspection/autocomplete, which makes the cost far too high for this
convenience.
"""
import functools
import importlib
import inspect
import pathlib
def auto_import(module_name):
"""Auto-import all modules with a given name in subdirs of applets."""
caller_frame_info = inspect.stack()[1]
caller_module = inspect.getmodule(caller_frame_info[0])
caller_module_name = caller_module.__name__
# converts openedx_learning.authoring.models -> openedx_learning.authoring
import_base = ".".join(caller_module_name.split(".")[:-1])
caller_filepath = caller_frame_info.filename
caller_dir = pathlib.Path(caller_filepath).resolve().parent
applets_dir = caller_dir / "applets"
module_paths = applets_dir.rglob(f"{module_name}.py")
relative_paths = [
module_path.relative_to(caller_dir) for module_path in module_paths
]
all_modules_dict = {}
for relative_path in sorted(relative_paths):
module_import_name = f"{import_base}." + ".".join(relative_path.parts)[:-3]
module = importlib.import_module(module_import_name)
module_dict = vars(module)
if '__all__' in module_dict:
module_dict_to_add = {
key: module_dict[key]
for key in module_dict['__all__']
}
else:
module_dict_to_add = {
key: val
for (key, val) in module_dict.items()
if not key.startswith('_')
}
all_modules_dict.update(module_dict_to_add)
return all_modules_dict
auto_import_models = functools.partial(auto_import, "models")
auto_import_api = functools.partial(auto_import, "api")
auto_import_admin = functools.partial(auto_import, "admin") |
Could this be done using Django's "squashed migration" feature instead of custom logic?
I'm not a fan of "applet", so I'd suggest "[sub]modules". Or perhaps better, continue to call them "apps" and just rename the larger thing - "umbrella app", "super-app", "domain", etc. |
My first stab at this was to shove them into a For the same reason, I don't want to call the small things "apps" because "apps" really means something in Django, and I don't want to overload that term. One of the reasons I went for "applet" was because it did not collide with any existing terminology that I know of in the Python/Django sphere. (I mean yes, there's the Java connotation, but Java applets have been dead for almost a decade now, and they had waned in popularity years before that.) In our meeting, "subapps" was suggested, which I'm okay with. I honestly still prefer "applets" though, because I think it more clearly conveys that these things are not actually real Django apps, but instead are more diminutive, app-like things. If I see a |
|
@bradenmacdonald: I may be missing something, but I don't really see how to make squashed migrations work without keeping the shells of the old apps around in their old locations (at least enough for a apps.py and migrations folder), and that seems like it would be more confusing than it's worth. If the squashed migration happens only in the authoring app, it also doesn't work because deciding whether something is migrated or not doesn't depend on the state of the schema but on the entries in the FWIW, the schema changes were auto-generated in the sense that I generated them from looking at the current state of the model files and then indented them in one level to wrap them in my |
I see. I guess that's the price that we'd have to pay to allow migrations from any point in time rather than Ulmo exactly. |
Actually, I may have an idea around this. The two important things are the labels and getting all the apps into edx-platform's INSTALLED_APPS. I think I can shove the migration remnants into a corner somewhere, make a function to return the app locations that edx-platform needs, and hook up the migrations to be sequential so this all works out okay. |
authoring appauthoring app
|
@kdmccormick, @bradenmacdonald: This should be ready for review. The corresponding edx-platform changes are in openedx/openedx-platform#37924 if you want to run it locally. After thinking about it more, I'd still like to keep the term "applet" because:
If "applet" is a no-go for you folks, would something like "micro-app" or "mini-app" work? |
| We're introducing a lower-level publishing concept of a dependency that will be | ||
| used by Containers, but this means we have to backfill that dependency info for | ||
| existing Containers in the system. | ||
| """ |
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 had to make a complete copy of the API code we currently use to do this migration, bur re-orient it to use the historical models rather than live ones. This is really what I should have done in the first place, to make the migration more robust—but now I simply have to, in order to keep this from breaking.
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 that's worth explaining in the docstring at the top of this migration file.
kdmccormick
left a comment
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.
Code looks great! I'm fine with applets, you make a good case.
I checked this out with your edx-platform branch to test. Even after re-pip-installing openedx-learning, running DJANGO_SETTINGS_MODULE=cms.envs.tutor.development ./manage.py cms migrate keeps giving me:
ValueError: The field content_libraries.ContentLibrary.learning_package was declared with a lazy reference to 'oel_authoring.learningpackage', but app 'oel_authoring' isn't installed.
The field contentstore.ComponentLink.upstream_block was declared with a lazy reference to 'oel_authoring.component', but app 'oel_authoring' isn't installed.
The field contentstore.ContainerLink.upstream_container was declared with a lazy reference to 'oel_authoring.container', but app 'oel_authoring' isn't installed.
The field modulestore_migrator.ModulestoreBlockMigration.change_log_record was declared with a lazy reference to 'oel_authoring.draftchangelogrecord', but app 'oel_authoring' isn't installed.
The field modulestore_migrator.ModulestoreBlockMigration.target was declared with a lazy reference to 'oel_authoring.publishableentity', but app 'oel_authoring' isn't installed.
The field modulestore_migrator.ModulestoreMigration.change_log was declared with a lazy reference to 'oel_authoring.draftchangelog', but app 'oel_authoring' isn't installed.
The field modulestore_migrator.ModulestoreMigration.target was declared with a lazy reference to 'oel_authoring.learningpackage', but app 'oel_authoring' isn't installed.
The field modulestore_migrator.ModulestoreMigration.target_collection was declared with a lazy reference to 'oel_authoring.collection', but app 'oel_authoring' isn't installed.
I can poke at it more later, but let me know if this is something you ran into while testing.
projects/urls.py
Outdated
| path("media_server/", include("openedx_learning.contrib.media_server.urls")), | ||
| path("tagging/rest_api/", include("openedx_tagging.core.tagging.urls")), | ||
| path('__debug__/', include('debug_toolbar.urls')), | ||
| # path('__debug__/', include('debug_toolbar.urls')), |
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.
| # path('__debug__/', include('debug_toolbar.urls')), | |
| path('__debug__/', include('debug_toolbar.urls')), |
openedx_learning/api/django.py
Outdated
| Module for parts of the Learning Core API that exist to make it easier to use in | ||
| Django projects. | ||
| """ | ||
|
|
||
| def learning_core_apps_to_install(): |
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.
| Module for parts of the Learning Core API that exist to make it easier to use in | |
| Django projects. | |
| """ | |
| def learning_core_apps_to_install(): | |
| 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(): |
we don't call it learning_core anywhere else in the code, so I'd air towards consistency here.
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.
Though I have to point out, with your proposed name, "learning apps" sounds to me like "not authoring apps", and the only apps in here are for authoring, not learning. So I kinda wish we'd go the other way and use learning_core more in the code where we refer to this package. But maybe that ship has sailed.
In other words, openedx_learning_apps_to_install can be read as "openedx_learning apps to install", which makes sense, but also read as "Open edX learning apps to install" which doesn't.
Huh. Yeah, I see it now as well. I ran into a variant of this last week, and I thought I had fixed it. I did somehow manage to get it to the fully migrated state. But now that I try starting from scratch again, I get the same error. I'll look into it. Thank you. |
|
Oh, I get it. The migration adjustments I made on the edx-platform side were before I kept all the old apps around, so the references need to be moved back to |
bradenmacdonald
left a comment
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.
Code looks good. Let me know when you've made those changes to fix the migrations, and I can test it on my devstack.
..._content/backcompat/collections/migrations/0006_remove_all_field_state_for_move_to_applet.py
Show resolved
Hide resolved
| We're introducing a lower-level publishing concept of a dependency that will be | ||
| used by Containers, but this means we have to backfill that dependency info for | ||
| existing Containers in the system. | ||
| """ |
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 that's worth explaining in the docstring at the top of this migration file.
authoring appopenedx_content app
68b305b to
68b105f
Compare
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.
68b105f to
b5b6038
Compare
|
@bradenmacdonald, @kdmccormick: This is ready for another review round. I'll update the openedx-platform PR that points to it later tonight. |
| #. 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*. |
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_content migration because it detects that the foreign keys in its State tracking will be out of sync with the database?
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.LearningPackage to openedx_content.LearningPackage, but when it comes to compute what that actually means, it will detect that oel_publishing.LearningPackage doesn't exist because oel_publishing : 0011_remove_all_field_state_for_move_to_applet has 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:
- The final backcompat app migrations are destructive in terms of model state, as they all get deleted.
- We want to force the platform migrations to point to the
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.
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 like oel_publishing.LearningPackage as 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.
If people always run CMS migrations before LMS migrations
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 --fake the migration and move on.
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 the oel_publishing app model LearningPackage, which happens to map to that table. But there's no way that I know of to make openedx_content take over the app namespaces of all the apps it's replacing.
Alternately, just don't drop those old logical tables or another year or so.
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:
- New
openedx_contentmodels are created. - Underlying
oel_*tables are renamed toopenedx_content - Platform models switch their references over. I think this can be a zero-database-operation migration, but in the worst case we re-build some foreign key constraint indexes.
So 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.
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 --fake the migration and move on.
It looks like LMS runs first.
|
|
||
| In one pull request, we are going to: | ||
|
|
||
| #. Rename the ``openedx_learning.apps.authoring`` package to be ``openedx_learning.apps.openedx_content``. |
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.
If we later move openedx_learning.apps.openedx_content -> openedx_core.apps.openedx_content or openedx_content (top level package) as discussed in the arch sync, will that be a trivial change or complex like this?
Edit: See #468 which tracks this follow-up work.
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 should be trivial. The references in openedx-platform need to be updated, and in particular things like the field definitions (like this example).
But those field definitions don't mess up the migrations, because they're not really recorded in the migration table. So just updating all the references should be fine as long as the code remains the same.
This is a wacky proposal that I've been kicking around in my head since I started working seriously on #452, and ran into issues with renaming the app and/or moving models around.
What is this?
This PR refactors the repo to combine all authoring apps (publishing, components, content, collections, etc.) into a single
openedx_contentapp from Django's point of view. But the boundaries previously set up by the apps still exist inopenedx_learning.apps.openedx_content.applets.*which has different packages forcomponents,collections, and all the rest.Each of these sub-apps still have their own
models.pyandadmin.pyfiles, though these are all stitched together by the higher levelopenedx_contentfiles. So for instance,openedx.apps.openedx_content.modelsimports everything from the sub-apps.We could make utility wrappers to make this more convenient later.(Edit: Introspection magic breaks code autocomplete.)It would look like:
Why do this?
I still believe that having small, discrete app-like things is useful for controlling complexity, and I don't want to give that up. That being said, refactoring is made much harder when we want to try to either change the names of real Django apps (e.g.
contentstomedia) or if we want to move models around (e.g.Container/ContainerVersionleaving thepublishingapp to go to a newcontainersapp). Having all the models be in one namespace will make shifting the boundaries between them much easier.This will have a few other minor benefits. We can do a top level
api.pyfile foropenedx_contentin a way that's consistent with other apps in the system. It sort of sets up the umbrellaopenedx_contentapp as the holder of the public interface. It also makes it less cumbersome to enter in the list of apps.On the downside, there's less consistency in terms of what goes where. Management commands, migrations, and app initialization code must all go in the root
openedx_contentapp.How does this work?
See the details at: https://github.com/ormsbee/openedx-learning/blob/big-authoring/docs/decisions/0020-authoring-as-one-app.rst