This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git


The following commit(s) were added to refs/heads/master by this push:
     new 750cbe7  AIRAVATA-2934 Integrate custom Django apps in navigation
750cbe7 is described below

commit 750cbe71a885402f381ac47196430f4794c8bfeb
Author: Marcus Christie <[email protected]>
AuthorDate: Wed Mar 20 13:21:08 2019 -0400

    AIRAVATA-2934 Integrate custom Django apps in navigation
---
 django_airavata/app_config.py                      | 69 +++++++++++++++++++++-
 django_airavata/apps/admin/apps.py                 |  3 +-
 django_airavata/apps/dataparsers/apps.py           |  1 -
 django_airavata/apps/groups/apps.py                |  3 +-
 django_airavata/apps/workspace/apps.py             |  1 -
 django_airavata/context_processors.py              | 48 ++++++++++++---
 django_airavata/settings.py                        |  5 +-
 django_airavata/templates/base.html                | 14 +++++
 .../templates/django_airavata/home.html            | 22 ++++++-
 docs/dev/new_django_app.md                         |  2 -
 10 files changed, 147 insertions(+), 21 deletions(-)

diff --git a/django_airavata/app_config.py b/django_airavata/app_config.py
index 57fdde2..f747b15 100644
--- a/django_airavata/app_config.py
+++ b/django_airavata/app_config.py
@@ -1,17 +1,20 @@
 
+import logging
 from abc import ABC, abstractmethod
+from importlib import import_module
 
 from django.apps import AppConfig
 
+logger = logging.getLogger(__name__)
+
 
 class AiravataAppConfig(AppConfig, ABC):
     """Custom AppConfig for Django Airavata apps."""
 
     @property
-    @abstractmethod
     def url_app_name(self):
-        """Return the urls application namespace (typically, same as label)."""
-        pass
+        """Return the urls application namespace."""
+        return get_url_app_name(self)
 
     @property
     @abstractmethod
@@ -36,3 +39,63 @@ class AiravataAppConfig(AppConfig, ABC):
     def app_description(self):
         """Some user friendly text to briefly describe the application."""
         pass
+
+
+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("{} 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)
diff --git a/django_airavata/apps/admin/apps.py 
b/django_airavata/apps/admin/apps.py
index 4e0591c..d7640f4 100644
--- a/django_airavata/apps/admin/apps.py
+++ b/django_airavata/apps/admin/apps.py
@@ -6,9 +6,8 @@ class AdminConfig(AiravataAppConfig):
     name = 'django_airavata.apps.admin'
     label = 'django_airavata_admin'
     verbose_name = 'Admin'
-    url_app_name = label
     app_order = 100
-    url_home = url_app_name + ':home'
+    url_home = 'django_airavata_admin:home'
     fa_icon_class = 'fa-cog'
     app_description = """
         Configure and share resources with other users.
diff --git a/django_airavata/apps/dataparsers/apps.py 
b/django_airavata/apps/dataparsers/apps.py
index e93f1db..02d3793 100644
--- a/django_airavata/apps/dataparsers/apps.py
+++ b/django_airavata/apps/dataparsers/apps.py
@@ -5,7 +5,6 @@ class DataParsersConfig(AiravataAppConfig):
     name = 'django_airavata.apps.dataparsers'
     label = 'django_airavata_dataparsers'
     verbose_name = 'Data Parsers'
-    url_app_name = label
     app_order = 20
     url_home = 'django_airavata_dataparsers:home'
     fa_icon_class = 'fa-copy'
diff --git a/django_airavata/apps/groups/apps.py 
b/django_airavata/apps/groups/apps.py
index 7abdfba..b0c8744 100755
--- a/django_airavata/apps/groups/apps.py
+++ b/django_airavata/apps/groups/apps.py
@@ -5,9 +5,8 @@ class GroupsConfig(AiravataAppConfig):
     name = 'django_airavata.apps.groups'
     label = 'django_airavata_groups'
     verbose_name = 'Groups'
-    url_app_name = label
     app_order = 10
-    url_home = url_app_name + ':manage'
+    url_home = 'django_airavata_groups:manage'
     fa_icon_class = 'fa-users'
     app_description = """
         Create and manage user groups.
diff --git a/django_airavata/apps/workspace/apps.py 
b/django_airavata/apps/workspace/apps.py
index f3dc5a9..ff14232 100644
--- a/django_airavata/apps/workspace/apps.py
+++ b/django_airavata/apps/workspace/apps.py
@@ -5,7 +5,6 @@ class WorkspaceConfig(AiravataAppConfig):
     name = 'django_airavata.apps.workspace'
     label = 'django_airavata_workspace'
     verbose_name = 'Workspace'
-    url_app_name = label
     app_order = 0
     url_home = 'django_airavata_workspace:dashboard'
     fa_icon_class = 'fa-flask'
diff --git a/django_airavata/context_processors.py 
b/django_airavata/context_processors.py
index 117cafe..6970cf5 100644
--- a/django_airavata/context_processors.py
+++ b/django_airavata/context_processors.py
@@ -1,7 +1,12 @@
+import logging
+
 from django.apps import apps
+from django.conf import settings
 
 from django_airavata.app_config import AiravataAppConfig
 
+logger = logging.getLogger(__name__)
+
 
 def airavata_app_registry(request):
     """Put airavata django apps into the context."""
@@ -11,18 +16,47 @@ def airavata_app_registry(request):
     airavata_apps.sort(
         key=lambda app: "{:09}-{}".format(app.app_order,
                                           app.verbose_name.lower()))
-    current_airavata_app = [
-        app for app in airavata_apps
-        if request.resolver_match and
-        app.url_app_name == request.resolver_match.app_name]
-    current_airavata_app = current_airavata_app[0]\
-        if len(current_airavata_app) > 0 else None
     return {
         'airavata_apps': airavata_apps,
-        'current_airavata_app': current_airavata_app,
+        'current_airavata_app': _get_current_app(request, airavata_apps),
+    }
+
+
+def custom_app_registry(request):
+    """Put custom Django apps into the context."""
+    custom_apps = settings.CUSTOM_DJANGO_APPS.copy()
+    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)),
+        'current_custom_app': _app_to_dict(current_custom_app)
     }
 
 
+def _app_to_dict(app):
+    # For some reason adding the AppConfig instance directly to the context
+    # doesn't allow its properties to be read. This code converts it into a
+    # simple dict.
+    if not app:
+        return None
+    return {
+        'name': app.name,
+        'label': app.label,
+        'verbose_name': app.verbose_name,
+        'url_home': app.url_home,
+        'fa_icon_class': app.fa_icon_class,
+        'app_description': app.app_description,
+    }
+
+
+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 resolver_match(request):
     """Put resolver_match (ResolverMatch instance) into the context."""
     return {'resolver_match': request.resolver_match}
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index d8f4dd9..96d23e2 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -14,6 +14,8 @@ import os
 
 from pkg_resources import iter_entry_points
 
+from django_airavata.app_config import enhance_custom_app_config
+
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
@@ -89,7 +91,7 @@ CUSTOM_DJANGO_APPS = []
 #    )
 #
 for entry_point in iter_entry_points(group='airavata.djangoapp'):
-    CUSTOM_DJANGO_APPS.append(entry_point.load())
+    CUSTOM_DJANGO_APPS.append(enhance_custom_app_config(entry_point.load()))
     INSTALLED_APPS.append(entry_point.name)
 
 MIDDLEWARE = [
@@ -126,6 +128,7 @@ TEMPLATES = [
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
                 'django_airavata.context_processors.airavata_app_registry',
+                'django_airavata.context_processors.custom_app_registry',
                 # 'django_airavata.context_processors.resolver_match',
             ],
         },
diff --git a/django_airavata/templates/base.html 
b/django_airavata/templates/base.html
index 9b32096..507eb7e 100644
--- a/django_airavata/templates/base.html
+++ b/django_airavata/templates/base.html
@@ -172,6 +172,20 @@
             </a>
             {% endif %}
             {% endfor %}
+            {% if custom_apps|length > 0 %}
+              <div class="dropdown-divider"></div>
+              {% for app in custom_apps %}
+                {% if current_custom_app and app.label == 
current_custom_app.label %}
+                <a class="dropdown-item active" href="{% url app.url_home %}">
+                  <i class="fa {{ app.fa_icon_class }} mr-2"></i>{{ 
app.verbose_name }}
+                </a>
+                {% else %}
+                <a class="dropdown-item" href="{% url app.url_home %}">
+                  <i class="fa {{ app.fa_icon_class }} mr-2"></i>{{ 
app.verbose_name }}
+                </a>
+                {% endif %}
+              {% endfor %}
+            {% endif %}
           </div>
         </div>
       </div>
diff --git a/django_airavata/templates/django_airavata/home.html 
b/django_airavata/templates/django_airavata/home.html
index 048df50..99e48ad 100644
--- a/django_airavata/templates/django_airavata/home.html
+++ b/django_airavata/templates/django_airavata/home.html
@@ -15,8 +15,8 @@
     {% if not forloop.counter|divisibleby:"2" %}
     <div class="row">
     {% endif %}
-        <div class="col-sm-6">
-            <div class="card">
+        <div class="col-sm-6 d-flex">
+            <div class="card flex-grow-1">
                 <div class="card-body">
                     <h5 class="card-title"><i class="fa {{ app.fa_icon_class 
}} mr-2"></i>{{ app.verbose_name }}</h5>
                     <p class="card-text">{{ app.app_description }}</p>
@@ -28,5 +28,23 @@
     </div>
     {% endif %}
     {% endfor %}
+
+    {% for app in custom_apps %}
+    {% if not forloop.counter|divisibleby:"2" %}
+    <div class="row">
+    {% endif %}
+        <div class="col-sm-6 d-flex">
+            <div class="card flex-grow-1">
+                <div class="card-body">
+                    <h5 class="card-title"><i class="fa {{ app.fa_icon_class 
}} mr-2"></i>{{ app.verbose_name }}</h5>
+                    <p class="card-text">{{ app.app_description|default:"" 
}}</p>
+                    <a href="{% url app.url_home %}" class="btn 
btn-primary">Go to {{ app.verbose_name }} ยป</a>
+                </div>
+            </div>
+        </div>
+    {% if forloop.counter|divisibleby:"2" %}
+    </div>
+    {% endif %}
+    {% endfor %}
 {% endif %}
 {% endblock content %}
diff --git a/docs/dev/new_django_app.md b/docs/dev/new_django_app.md
index 2340c99..6f38e9b 100644
--- a/docs/dev/new_django_app.md
+++ b/docs/dev/new_django_app.md
@@ -32,7 +32,6 @@ class MyAppConfig(AiravataAppConfig):
     name = 'django_airavata.apps.myapp'
     label = 'django_airavata_myapp'
     verbose_name = 'My App'
-    url_app_name = label
     app_order = 10
     url_home = 'django_airavata_myapp:home'
     fa_icon_class = 'fa-bolt'
@@ -48,7 +47,6 @@ properties:
 - _label_ - this needs to be unique across all installed Django apps. I just
   make this match the _app_name_ in `urls.py`.
 - _verbose_name_ - display name of app
-- _url_app_name_ - this needs to match the _app_name_ in `urls.py`
 - _app_order_ - order of app in the menu listing. Range is 0 - 100. See the
   other Django apps for their values to figure out how to order this app
   relative to them.

Reply via email to