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.