dynamicweb/cms/app_base.py

259 lines
9.5 KiB
Python

from abc import ABCMeta, abstractmethod
class CMSApp:
"""Base class for creating apphooks. Apphooks live in a file called ``cms_apps.py``.
To create an AppHook subclass ``CMSApp`` in ``cms_apps.py``
::
class MyAppHook(CMSApp):
name = "Problem solver"
"""
#: list of urlconfs: example: ``_urls = ["myapp.urls"]``
_urls = []
#: list of menu classes: example: ``_menus = [MyAppMenu]``
_menus = []
#: Human-readable name of the apphook (required). This name will be displayed
#: on the admin site.
name = None
#: Gives the system a unique way to refer to the apphook. This enables Django
#: namespaces support (optional)
app_name = None
#: configuration model (optional)
app_config = None
#: if set to true, apphook inherits permissions from the current page
permissions = True
#: list of application names to exclude from inheriting CMS permissions
exclude_permissions = []
def __new__(cls):
"""
We want to bind the CMSapp class to a specific AppHookConfig, but only one at a time
Checking for the runtime attribute should be a sane fix
"""
if cls.app_config:
if getattr(cls.app_config, 'cmsapp', None) and cls.app_config.cmsapp != cls:
raise RuntimeError(
'Only one AppHook per AppHookConfiguration must exists.\n'
'AppHook %s already defined for %s AppHookConfig' % (
cls.app_config.cmsapp.__name__, cls.app_config.__name__
)
)
cls.app_config.cmsapp = cls
return super().__new__(cls)
def get_configs(self):
"""
Returns all the apphook configuration instances.
To be implemented by apphook subclass.
"""
raise NotImplementedError('Configurable AppHooks must implement this method')
def get_config(self, namespace):
"""
Returns the apphook configuration instance linked to the given namespace
To be implemented by apphook subclass.
"""
raise NotImplementedError('Configurable AppHooks must implement this method')
def get_config_add_url(self):
"""
Returns the url to add a new apphook configuration instance
(usually the model admin add view)
To be implemented by apphook subclass.
"""
raise NotImplementedError('Configurable AppHooks must implement this method')
def get_menus(self, page=None, language=None, **kwargs):
"""
Returns the menus for the apphook instance, eventually selected according
to the given arguments.
By default, it returns the menus assigned to :py:attr:`CMSApp._menus`.
The method accepts page, language and generic keyword arguments:
you can customize this function to return different list of menu classes
according to the given arguments.
If no menus are returned, then the user will need to attach menus to pages
manually in the admin.
If no page and language are provided, this method **must** return **all the
menus used by this apphook**. Example::
if page and page.reverse_id == 'page1':
return [Menu1]
elif page and page.reverse_id == 'page2':
return [Menu2]
else:
return [Menu1, Menu2]
:param page: page the apphook is attached to
:param language: current site language
:return: list of menu classes
"""
return self._menus
def get_urls(self, page=None, language=None, **kwargs):
"""
Returns the urlconfs for the apphook instance, eventually selected
according to the given arguments.
By default, it returns the urls assigned to :py:attr:`CMSApp._urls`
The method accepts page, language and generic keyword arguments:
you can customize this function to return different urlconfs
according to the given arguments.
This method **must** return a non-empty list of urlconfs,
even if no argument is passed.
:param page: page the apphook is attached to
:param language: current site language
:return: list of urlconfs strings
"""
return self._urls
def get_root_template(self, page=None, language=None, **kwargs):
"""
.. versionadded:: 5.1
Returns the template name used by the apphook's root view.
Best-effort: walks the urlconfs from :meth:`get_urls` to locate the
pattern matching the empty path and returns ``template_name`` from its
view class (CBVs) or callback (FBVs that expose it). Returns ``None``
for views that compute the template dynamically — override this method
to return the template name in that case.
:return: template name or None
"""
from django.urls import URLPattern, URLResolver, get_resolver
def find_root(patterns):
for p in patterns:
if p.pattern.match('') is None:
continue
if isinstance(p, URLResolver):
inner = find_root(p.url_patterns)
if inner is not None:
return inner
elif isinstance(p, URLPattern):
return p
return None
for urlconf in self.get_urls(page=page, language=language, **kwargs):
try:
resolver = get_resolver(urlconf)
pattern = find_root(resolver.url_patterns)
except Exception:
continue
if pattern is None:
continue
callback = pattern.callback
view_class = getattr(callback, 'view_class', None)
view_initkwargs = getattr(callback, 'view_initkwargs', None) or {}
return (
view_initkwargs.get('template_name')
or getattr(view_class, 'template_name', None)
or getattr(callback, 'template_name', None)
)
return None
class CMSAppConfig:
"""
.. versionadded:: 4.0
Base class that all cms app configurations should inherit from.
CMSAppConfig live in a file called ``cms_config.py``.
Apps subclassing ``CMSAppConfig`` can set ``cms_enabled = True`` for their app config to
use django CMS' wizard functionality. Additional wizzwards are listed in the app config's
``cms_wizzards`` property.
The second functionality that django CMS offers is attaching Model objects to the toolbar. To use
this functionality, set list the Model classes in ``cms_toolbar_enabled_models`` and have
``cms_enabled = True``
"""
def __init__(self, django_app_config):
self.app_config = django_app_config
@staticmethod
def get_contract(contract_name: str) -> type:
"""
Retrieve a contract implementation from registered CMS extension apps.
Searches through all registered CMS extension applications for a contract
that matches the given contract name. A contract is a named interface that
allows apps to provide functionality that other apps can use.
This method enables an architecture where apps can query for
specific contracts and use the implementation provided by any registered
extension app that exports that contract.
:param contract_name: The name identifier of the contract to retrieve
:type contract_name: str
:return: The contract class if found, or None if no matching contract exists
:rtype: type or None
:raises: None
Example::
# Get a versioning contract implementation
versioning_contract = CMSAppConfig.get_contract('versioning')
if versioning_contract:
# Use the versioning functionality
version = versioning_contract.create_version(obj)
"""
from cms.app_registration import get_cms_extension_apps
for app in get_cms_extension_apps():
name, cls = getattr(app.cms_extension, "contract", (None, None))
if name == contract_name:
return cls
return None
class CMSAppExtension(metaclass=ABCMeta):
"""
.. versionadded:: 4.0
Base class that all cms app extensions should inherit from. App extensions allow
apps to offer their functionality to other apps, e.g., as done by djangocms-versioning.
CMSAppExtensions live in a file called ``cms_config.py``.
"""
@abstractmethod
def configure_app(self, cms_config):
"""
Implement this method if the app provides functionality that
other apps can use and configure.
This method will be run once for every app that defines an
attribute like ``<app_label>_enabled`` as ``True`` on its cms app
config class.
So for example, if app A with label "app_a" implements this
method and app B and app C define ``app_a_enabled = True`` on their
cms config classes, the method app A has defined will run twice,
once for app B and once for app C.
:param cms_config: the cms config class of the app registering for additional functionality
:type cms_config: :class:`CMSAppConfig` subclass
"""
pass
def ready(self):
"""Override this method to run code after all CMS extensions
have been configured.
This method will be run once, even if no cms app config sets
its ``<app_label>_enabled`` attribute to ``True``"""
pass