-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Introduced app registration system #6421
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
Introduced app registration system #6421
Conversation
App registration - configuring apps
App registration start up
…se cms_config.py filenames instead of cms_apps.py
App registration: improvements after Paulo's comments
@evildmp From what I understand you will be creating documentation for this. Please let me know if you need more than what I've put in the PR description to create this. Also, on the topic of docs there's a comment from Noel - it might be better to use a differently named app for the example so that nobody gets confused that these docs describe how to actually integrate with the actual versioning app. |
@czpython I ran the description in this PR by Matus and Dami who are planning on designing the generic (i.e. configurable) part of moderation imminently and gave me detailed feedback with that very much in mind. There's some stuff I probably need to add to the description of this PR to make a few things clearer based on that discussion (@evildmp I'll try to do that on Monday), but also there are two suggestions for changing how this would work:
IMO it would be good to implement both of those :) But obviously want to consult with @czpython Some other questions also arose:
|
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.
Did a quick scan through the pr.
Will do a full review after addressing the open questions.
CHANGELOG.txt
Outdated
@@ -1,3 +1,8 @@ | |||
=== 4.0.0 (unreleased) === |
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 know the PR guidelines suggest to do this but for 4.0.x, let's leave the changelog as is.
Once we're closer to release, then we can use the pr descriptions to compose the changelog.
The reason for this is that otherwise there will be conflicts all the time with changelog file and will need constant rebasing, etc..
cms/app_base.py
Outdated
|
||
:param app: django app that has defined the feature as enabled | ||
""" | ||
raise NotImplementedError() |
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.
when not passing arguments, raise NotImplementedError
, vs NotImplementedError()
cms/utils/setup.py
Outdated
@@ -2,6 +2,9 @@ | |||
from django.core.exceptions import ImproperlyConfigured | |||
|
|||
from cms.utils.compat.dj import is_installed as app_is_installed | |||
from cms.app_registration import ( | |||
autodiscover_cms_configs, get_cms_apps, | |||
configure_cms_apps) |
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.
please change to:
from cms.app_registration import (
autodiscover_cms_configs,
configure_cms_apps,
get_cms_apps,
)
@@ -0,0 +1 @@ | |||
raise KeyError |
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.
Is there a reason for the KeyError? maybe use a RuntimeError
instead.
Also some comments here would be good.
cms/app_registration.py
Outdated
# function if a cms_config.py file with a suitable class is found. | ||
if hasattr(app_config, 'cms_app'): | ||
cms_apps.append(app_config) | ||
|
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.
remove newline
cms/app_registration.py
Outdated
""" | ||
Returns django app configs of apps with a cms config | ||
""" | ||
cms_apps = [] |
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 this can be a list comprehension
cms/app_registration.py
Outdated
"class which inherits from CMSAppConfig") | ||
|
||
|
||
def get_cms_apps(): |
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.
decorate with lru_cache(max_size=None)
@monikasulik
djangocms_moderation_enabled = settings.ENABLE_MODERATION_FOR_BLOG We can achieve the same with list/tuple approach but IMHO is less readable.
|
@czpython, re point 1 in your answer. I thought that the purpose of |
App registration: Paulo's comments
@czpython I think I've acted on all comments now. |
@evildmp I have now improved the PR description. Let me know if you need any extra info to write the actual docs. |
cms/utils/setup.py
Outdated
@@ -2,6 +2,11 @@ | |||
from django.core.exceptions import ImproperlyConfigured | |||
|
|||
from cms.utils.compat.dj import is_installed as app_is_installed | |||
from cms.app_registration import ( |
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.
import order.
move this one before utils import
cms/app_base.py
Outdated
# by checking for classes which are subclasses of CMSAppConfig. | ||
|
||
|
||
class CMSAppExtension(object): |
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.
because we've now split the config from extension, we should use ABC class here and mark the configure_app
method as abstract.
app_with_cms_feature_enabled = True | ||
# a test attr that should get changed to True by | ||
# app_with_cms_feature on successful configuration | ||
configured = False |
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 know this is for testing purposes but we should avoid apps that alter another app config in any way.
In this case I suggest to use something like app_with_cms_feature_strings = ['something']
then have the configure_app
method on app_with_cms_feature hold all strings for all apps that extend it.
|
||
class CMSSomeFeatureConfig(CMSAppExtension): | ||
|
||
num_configured_apps = 0 |
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 suggest declaring this in the __init__
raise Exception('some_function called') | ||
|
||
|
||
class SomeClass(): |
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.
because of python2 support, subclass object
cms/tests/test_app_registration.py
Outdated
'cms.test_utils.project.app_with_feature_not_implemented', | ||
'cms.test_utils.project.app_using_non_feature' | ||
]) | ||
def test_raises_not_implemented_exception_when_feature_app_doesnt_implement_configure_method(self): |
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.
can be shortened to test_raises_not_implemented_exception and the rest explained in comment
cms/app_registration.py
Outdated
enabled_property = "{}_enabled".format(app_with_feature.label) | ||
configure_app = app_with_feature.cms_extension.configure_app | ||
|
||
for app_config in apps.get_app_configs(): |
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.
there should be a function that returns all apps with cms configs.
this can be wrapped with lru_cache for performance
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.
In the spirit of YAGNI, wouldn't it be better to add that when we actually need it rather than before? It's a simple thing to add later if it is needed.
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.
We would use it here, and will be good because then we can compute it once at runtime
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.
You want one with all cms apps (both extensions and configs) or one with just the cms apps that define a config? Cause actually I think in this case it makes more sense if it's just the apps that have a cms_config attribute.
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.
Correct, just the ones with cms config
cms/app_registration.py
Outdated
continue | ||
if getattr(app_config.cms_config, enabled_property, False): | ||
# Feature enabled for this app so configure | ||
configure_app(app_config) |
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.
We should pass the cms_config
directly here.
It will cover most cases, otherwise everyone would have to do app.cms_config
all the time
in their configure_app
method and I consider cms_config
an internal that we shouldn't expose.
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, that will mean that I'll also need to override the __init__
to take the django app config as in your original design. People might need things like app label, verbose_name etc. for things.
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.
Yup
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.
Now that I've just done this and looked at how the code is now, it has occurred to me that it would probably be neater to do def configure_app(cms_config, app_config)
rather than override init and do cms_config.app_config
😉 Thoughts?
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.
Don't like it because it implies that they're different while they're not, most cases will not need to use app_config.
cms/tests/test_app_registration.py
Outdated
@override_settings(INSTALLED_APPS=[ | ||
'cms.test_utils.project.app_with_bad_cms_file', | ||
]) | ||
def test_raises_exception_raised_in_cms_file(self): |
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.
rename to test_exception_propagates_from_cms_file
cms/app_registration.py
Outdated
""" | ||
cms_config_classes = [] | ||
# Find all classes that inherit from CMSAppConfig | ||
for name, obj in inspect.getmembers(cms_module): |
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.
this logic should be a function because is used by the _find_extension
above.
cms/app_base.py
Outdated
cms config classes, the method app A has defined will run twice, | ||
once for app B and once for app C. | ||
|
||
:param app: django app that has defined the feature as enabled |
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.
param is now cms_config
cms/app_base.py
Outdated
@@ -1,4 +1,5 @@ | |||
# -*- coding: utf-8 -*- | |||
from abc import ABC, abstractmethod |
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.
Have a look at https://stackoverflow.com/a/35673504/389453 for python2 and 3 compat
from cms.app_base import CMSAppExtension | ||
|
||
|
||
def some_function(*args, **kwargs): |
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.
this is a bit odd. what does this test?
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.
As the comment in the next line says - that autodiscover doesn't pick this up :) Just wanted to add some random functions and classes in there, so that we know that autodiscover only ever picks up classes that inherit from CMSAppConfig and CMSAppExtension. Possibly now a bit redundant because the separation into two classes means that the code would have to check the parent class anyway, which wasn't true before.
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.
Yeah I think is fine to remove this
cms/app_registration.py
Outdated
from cms.app_base import CMSAppConfig, CMSAppExtension | ||
|
||
|
||
CMS_CONFIG_NAME = 'cms_config' |
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 this can go into the constants.py
cms/app_registration.py
Outdated
classes = [] | ||
# Find all classes that inherit from klass | ||
for name, obj in inspect.getmembers(cms_module): | ||
is_cms_config = ( |
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.
rename to is_subclass
if config: | ||
app_config.cms_config = config(app_config) | ||
if extension: | ||
app_config.cms_extension = extension() |
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.
should we also pass app_config here for consistency?
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.
IMO no because nobody should really be accessing app_config.cms_extension
anyway, so there's no need for app_config.cms_extension.django_app
. They are two different classes so don't think they need the init to be the same.
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
cms/tests/test_app_registration.py
Outdated
app_registration.get_cms_config_apps.cache_clear() | ||
|
||
@patch.object(apps, 'get_app_configs') | ||
def test_runs_configure_app_method_for_app_with_enabled_config( |
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.
one line is fine here
cms/tests/test_app_registration.py
Outdated
config_app.cms_config) | ||
|
||
@patch.object(apps, 'get_app_configs') | ||
def test_doesnt_run_configure_app_method_for_disabled_app( |
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.
one line
cms/tests/test_app_registration.py
Outdated
self.assertFalse(feature_app.cms_extension.configure_app.called) | ||
|
||
@patch.object(apps, 'get_app_configs') | ||
def test_doesnt_raise_exception_if_not_cms_app( |
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.
one line
cms/tests/test_app_registration.py
Outdated
self.fail("Exception raised when cms app config not defined") | ||
|
||
@patch.object(apps, 'get_app_configs') | ||
def test_runs_configure_app_method_for_correct_apps_when_multiple_apps( |
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.
here is ok
Summary
This is the new app registration system. It allows django apps that extend the cms to register their functionality with the cms and introduces a way to set configuration for other apps that use this additional functionality.
Proposed changes in this pull request
To understand how to use the app registration system, lets use an example. Lets say our
INSTALLED_APPS
are defined like this:The
pink_cms_admin
is an app that extends the cms by making apps, that are so configured, to have a pink admin. To do that, it would define apink_cms_admin/cms_config.py
file, which would look like this:The
blog_posts
app wants to be pink and wants to have pony icons everywhere. So it would defineblog_posts/cms_config.py
like this:The pony_cms_icons app lets other apps have pony icons everywhere, but also wants to have a pink admin. So it would define
pony_cms_icons/cms_config.py
like this:The
configure_app
method, as is already apparent, takes one param -cms_config
.cms_config
is an instance of an app's CMSAppConfig class. In addition to that you can also access the django app object (as defined in the app's apps.py) by usingcms_config.app_config
. In this way you can access attributes that django provides (such aslabel
,verbose_name
etc.).The
configure_app
method is run once for every django cms app that declares a feature as enabled.Documentation checklist