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()

Reply via email to