This is an automated email from the ASF dual-hosted git repository. machristie pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/airavata-django-portal-commons.git
commit dc40a2fce8e0d37d4561dd0f7e78dea0c1f8b803 Author: Marcus Christie <[email protected]> AuthorDate: Mon Oct 31 17:35:43 2022 -0400 initial commit - this code comes from airavata-django-portal --- .gitignore | 7 ++ MANIFEST.in | 0 README.md | 7 ++ airavata_django_portal_commons/__init__.py | 0 .../dynamic_apps/__init__.py | 52 +++++++++ .../dynamic_apps/context_processors.py | 130 +++++++++++++++++++++ .../dynamic_apps/urls.py | 12 ++ pyproject.toml | 6 + setup.cfg | 11 ++ setup.py | 3 + 10 files changed, 228 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f6cd5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +venv +*.egg-info +*.pyc +.vscode +build +dist +*.egg diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..48cd3b0 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Airavata Django Portal Commons + +Utilities for working with dynamically loaded Django apps. + +## Getting Started + +TODO diff --git a/airavata_django_portal_commons/__init__.py b/airavata_django_portal_commons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/airavata_django_portal_commons/dynamic_apps/__init__.py b/airavata_django_portal_commons/dynamic_apps/__init__.py new file mode 100644 index 0000000..2c8692e --- /dev/null +++ b/airavata_django_portal_commons/dynamic_apps/__init__.py @@ -0,0 +1,52 @@ +import logging +from importlib import import_module + +from pkg_resources import iter_entry_points + +# AppConfig instances from custom Django apps +CUSTOM_DJANGO_APPS = [] + +logger = logging.getLogger(__name__) + + +def load(installed_apps, entry_point_group="airavata.djangoapp"): + for entry_point in iter_entry_points(group=entry_point_group): + custom_app_class = entry_point.load() + custom_app_instance = custom_app_class( + entry_point.name, import_module(entry_point.module_name) + ) + CUSTOM_DJANGO_APPS.append(custom_app_instance) + # Create path to AppConfig class (otherwise the ready() method doesn't get + # called) + logger.info(f"adding dynamic Django app {entry_point.name}") + installed_apps.append( + "{}.{}".format(entry_point.module_name, entry_point.attrs[0]) + ) + + +def merge_setting_dict(default, custom_setting): + # FIXME: only handles dict settings, doesn't handle lists + if isinstance(custom_setting, dict): + for k in custom_setting.keys(): + if k not in default: + default[k] = custom_setting[k] + else: + raise Exception( + "Custom django app setting conflicts with " + "key {} in {}".format(k, default) + ) + + +def merge_setting(settings_module, setting_name): + # Merge settings from custom Django apps + # FIXME: only handles WEBPACK_LOADER additions + for custom_django_app in CUSTOM_DJANGO_APPS: + if hasattr(custom_django_app, "settings"): + s = custom_django_app.settings + merge_setting_dict( + getattr(settings_module, setting_name), getattr(s, setting_name, {}) + ) + + +def merge_webpack_loader(settings_module): + merge_setting(settings_module, "WEBPACK_LOADER") diff --git a/airavata_django_portal_commons/dynamic_apps/context_processors.py b/airavata_django_portal_commons/dynamic_apps/context_processors.py new file mode 100644 index 0000000..4380165 --- /dev/null +++ b/airavata_django_portal_commons/dynamic_apps/context_processors.py @@ -0,0 +1,130 @@ +import copy +from importlib import import_module +import logging +import re + +from airavata_django_portal_commons import dynamic_apps + +logger = logging.getLogger(__name__) + + +def custom_app_registry(request): + """Put custom Django apps into the context.""" + custom_apps = dynamic_apps.CUSTOM_DJANGO_APPS.copy() + custom_apps = [ + _enhance_custom_app_config(app) + for app in custom_apps + if (getattr(app, "enabled", None) is None or app.enabled(request)) + ] + custom_apps.sort(key=lambda app: app.verbose_name.lower()) + current_custom_app = _get_current_app(request, custom_apps) + return { + # 'custom_apps': list(map(_app_to_dict, custom_apps)), + "custom_apps": custom_apps, + "current_custom_app": current_custom_app, + "custom_app_nav": ( + _get_app_nav(request, current_custom_app) if current_custom_app else None + ), + } + + +def _enhance_custom_app_config(app): + """As necessary add default values for properties to custom AppConfigs.""" + app.url_app_name = _get_url_app_name(app) + app.url_home = _get_url_home(app) + app.fa_icon_class = _get_fa_icon_class(app) + app.app_description = _get_app_description(app) + return app + + +def _get_url_app_name(app_config): + """Return the urls namespace for the given AppConfig instance.""" + urls = _get_app_urls(app_config) + return getattr(urls, "app_name", None) + + +def _get_url_home(app_config): + """Get named URL of home page of app.""" + if hasattr(app_config, "url_home"): + return app_config.url_home + else: + return _get_default_url_home(app_config) + + +def _get_default_url_home(app_config): + """Return first url pattern as a default.""" + urls = _get_app_urls(app_config) + app_name = _get_url_app_name(app_config) + logger.warning( + "Custom Django app {} has no URL namespace " "defined".format(app_config.label) + ) + first_named_url = None + for urlpattern in urls.urlpatterns: + if hasattr(urlpattern, "name"): + first_named_url = urlpattern.name + break + if not first_named_url: + raise Exception(f"{urls} has no named urls, can't figure out default home URL") + if app_name: + return app_name + ":" + first_named_url + else: + return first_named_url + + +def _get_fa_icon_class(app_config): + """Return Font Awesome icon class to use for app.""" + if hasattr(app_config, "fa_icon_class"): + return app_config.fa_icon_class + else: + return "fa-circle" + + +def _get_app_description(app_config): + """Return brief description of app.""" + return getattr(app_config, "app_description", None) + + +def _get_app_urls(app_config): + return import_module(".urls", app_config.name) + + +def _get_current_app(request, apps): + current_app = [ + app + for app in apps + if request.resolver_match + and app.url_app_name == request.resolver_match.app_name + ] + return current_app[0] if len(current_app) > 0 else None + + +def _get_app_nav(request, current_app): + if hasattr(current_app, "nav"): + # Copy and filter current_app's nav items + nav = [ + item + for item in copy.copy(current_app.nav) + if "enabled" not in item or item["enabled"](request) + ] + # convert "/djangoapp/path/in/app" to "path/in/app" + app_path = "/".join(request.path.split("/")[2:]) + for nav_item in nav: + if "active_prefixes" in nav_item: + if re.match("|".join(nav_item["active_prefixes"]), app_path): + nav_item["active"] = True + else: + nav_item["active"] = False + else: + # 'active_prefixes' is optional, and if not specified, assume + # current item is active + nav_item["active"] = True + else: + # Default to the home view in the app + nav = [ + { + "label": current_app.verbose_name, + "icon": "fa " + current_app.fa_icon_class, + "url": current_app.url_home, + } + ] + return nav diff --git a/airavata_django_portal_commons/dynamic_apps/urls.py b/airavata_django_portal_commons/dynamic_apps/urls.py new file mode 100644 index 0000000..256a6f8 --- /dev/null +++ b/airavata_django_portal_commons/dynamic_apps/urls.py @@ -0,0 +1,12 @@ +from airavata_django_portal_commons import dynamic_apps +from django.conf.urls import include +from django.urls import path + +urlpatterns = [] +for custom_django_app in dynamic_apps.CUSTOM_DJANGO_APPS: + # Custom Django apps may define a url_prefix, otherwise label will be used + # as url prefix + url_prefix = getattr(custom_django_app, "url_prefix", custom_django_app.label) + urlpatterns.append( + path(f"{url_prefix}/", include(custom_django_app.name + ".urls")) + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..374b58c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3eb1eb6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[metadata] +name = airavata_django_portal_commons +version = 0.1.0 +description = Utilities for working with dynamically loaded Django apps. + +[options] +packages = find: +# Include data files as specified in MANIFEST.in +include_package_data = True +install_requires = + django >= 3.2, < 4 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup()
