This is an automated email from the ASF dual-hosted git repository. smarru pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/airavata-custos-portal.git
commit ac2a89410fd6d3b2b81567a5ade60c800b2bd6fe Author: Shivam Rastogi <shivam_r...@yahoo.com> AuthorDate: Sun Mar 22 21:10:07 2020 -0400 Added navigation buttons to the application --- .../custos_admin_portal/app_config.py | 101 +++++++++++++++++++++ .../custos_admin_portal/apps/admin/apps.py | 19 +++- .../custos_admin_portal/apps/admin/urls.py | 2 +- .../custos_admin_portal/apps/admin/views.py | 3 +- .../custos_admin_portal/apps/workspace/apps.py | 28 +++++- .../custos_admin_portal/apps/workspace/urls.py | 2 +- .../custos_admin_portal/apps/workspace/views.py | 6 +- .../custos_admin_portal/context_processors.py | 87 ++++++++++++++++++ .../custos_admin_portal/settings.py | 3 +- .../static/common/dist/webpack-stats.json | 2 +- .../static/common/package-lock.json | 7 +- .../static/common/vue.config.js | 1 + .../custos_admin_portal/templates/base.html | 68 +++++++++++++- 13 files changed, 310 insertions(+), 19 deletions(-) diff --git a/custos_admin_portal/custos_admin_portal/app_config.py b/custos_admin_portal/custos_admin_portal/app_config.py new file mode 100644 index 0000000..b99cf99 --- /dev/null +++ b/custos_admin_portal/custos_admin_portal/app_config.py @@ -0,0 +1,101 @@ + +import logging +from abc import ABC, abstractmethod +from importlib import import_module + +from django.apps import AppConfig + +logger = logging.getLogger(__name__) + + +class CustosAppConfig(AppConfig, ABC): + """Custom AppConfig for Django Airavata apps.""" + + @property + def url_app_name(self): + """Return the urls application namespace.""" + return get_url_app_name(self) + + @property + @abstractmethod + def app_order(self): + """Return positive int order of app in listings, lowest sorts first.""" + pass + + @property + @abstractmethod + def url_home(self): + """Named route of home page for this application.""" + pass + + @property + @abstractmethod + def fa_icon_class(self): + """Font Awesome icon class name.""" + pass + + @property + @abstractmethod + 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/custos_admin_portal/custos_admin_portal/apps/admin/apps.py b/custos_admin_portal/custos_admin_portal/apps/admin/apps.py index 6b3d0d6..2385180 100644 --- a/custos_admin_portal/custos_admin_portal/apps/admin/apps.py +++ b/custos_admin_portal/custos_admin_portal/apps/admin/apps.py @@ -1,6 +1,21 @@ -from django.apps import AppConfig +from custos_admin_portal.app_config import CustosAppConfig -class AdminConfig(AppConfig): +class AdminConfig(CustosAppConfig): name = 'custos_admin_portal.apps.admin' label = 'custos_admin_portal_admin' + verbose_name = 'Admin' + app_order = 100 + url_home = 'custos_admin_portal_admin:list_requests' + fa_icon_class = 'fa-cog' + app_description = """ + Configure and share resources with other users. + """ + nav = [ + { + 'label': 'Application Catalog', + 'icon': 'fa fa-list', + 'url': 'custos_admin_portal_admin:list_requests', + 'active_prefixes': ['applications', 'list-requests'] + } + ] \ No newline at end of file diff --git a/custos_admin_portal/custos_admin_portal/apps/admin/urls.py b/custos_admin_portal/custos_admin_portal/apps/admin/urls.py index dd0828d..dc77d80 100644 --- a/custos_admin_portal/custos_admin_portal/apps/admin/urls.py +++ b/custos_admin_portal/custos_admin_portal/apps/admin/urls.py @@ -5,7 +5,7 @@ from . import views app_name = 'custos_admin_portal_admin' urlpatterns = [ - url(r'^list-requests', views.list_new_tenant_requests, name='list_request'), + url(r'^list-requests', views.list_new_tenant_requests, name='list_requests'), url(r'^request/(?P<tenant_request_id>[^/]+)/$', views.view_tenant_request, name="view_tenant_request"), url(r'^edit-tenant-request/(?P<tenant_request_id>[^/]+)/$', views.edit_tenant_request, name="edit_tenant_request") ] diff --git a/custos_admin_portal/custos_admin_portal/apps/admin/views.py b/custos_admin_portal/custos_admin_portal/apps/admin/views.py index 4b4428f..6be7603 100644 --- a/custos_admin_portal/custos_admin_portal/apps/admin/views.py +++ b/custos_admin_portal/custos_admin_portal/apps/admin/views.py @@ -2,7 +2,8 @@ from django.shortcuts import render def list_new_tenant_requests(request): - print("New tenant requests list is called") + request.active_nav_item = 'list-requests' + # TODO fetch all the tenant requests from airavata here. return render(request, 'workspace/list_requests.html', { 'bundle_name': 'admin-list-requests', diff --git a/custos_admin_portal/custos_admin_portal/apps/workspace/apps.py b/custos_admin_portal/custos_admin_portal/apps/workspace/apps.py index 7be3c80..4cf73f2 100644 --- a/custos_admin_portal/custos_admin_portal/apps/workspace/apps.py +++ b/custos_admin_portal/custos_admin_portal/apps/workspace/apps.py @@ -1,5 +1,27 @@ -from django.apps import AppConfig +from custos_admin_portal.app_config import CustosAppConfig -class WorkspaceConfig(AppConfig): - name = 'workspace' +class WorkspaceConfig(CustosAppConfig): + name = 'custos_admin_portal.apps.workspace' + label = 'custos_admin_portal_workspace' + verbose_name = 'Workspace' + app_order = 0 + url_home = 'custos_admin_portal_workspace:request_new_tenant' + fa_icon_class = 'fa-flask' + app_description = """ + Launch applications and manage your experiments and projects. + """ + nav = [ + { + 'label': 'Create new tenant request', + 'icon': 'fa fa-plus-square', + 'url': 'custos_admin_portal_workspace:request_new_tenant', + 'active_prefixes': ['applications', 'request-new-tenant'] + }, + { + 'label': 'List of all existing tenant requests', + 'icon': 'fa fa-list', + 'url': 'custos_admin_portal_workspace:list_requests', + 'active_prefixes': ['applications', 'list-requests'] + } + ] \ No newline at end of file diff --git a/custos_admin_portal/custos_admin_portal/apps/workspace/urls.py b/custos_admin_portal/custos_admin_portal/apps/workspace/urls.py index f41aec0..29c5c34 100644 --- a/custos_admin_portal/custos_admin_portal/apps/workspace/urls.py +++ b/custos_admin_portal/custos_admin_portal/apps/workspace/urls.py @@ -5,6 +5,6 @@ from . import views app_name = 'custos_admin_portal_workspace' urlpatterns = [ url(r'^request-new-tenant', views.request_new_tenant, name='request_new_tenant'), - url(r'^list-requests', views.list_new_tenant_requests, name='list_request'), + url(r'^list-requests', views.list_new_tenant_requests, name='list_requests'), url(r'request/(?P<tenant_request_id>[^/]+)/$', views.view_tenant_request, name="view_tenant_request") ] diff --git a/custos_admin_portal/custos_admin_portal/apps/workspace/views.py b/custos_admin_portal/custos_admin_portal/apps/workspace/views.py index 9c4d624..8a89243 100644 --- a/custos_admin_portal/custos_admin_portal/apps/workspace/views.py +++ b/custos_admin_portal/custos_admin_portal/apps/workspace/views.py @@ -2,7 +2,7 @@ from django.shortcuts import render def request_new_tenant(request): - print("request new tenant is called") + request.active_nav_item = 'request-new-tenant' return render(request, 'workspace/request_new_tenant.html', { 'bundle_name': 'request-new-tenant', @@ -10,7 +10,7 @@ def request_new_tenant(request): def list_new_tenant_requests(request): - print("New tenant requests list is called") + request.active_nav_item = 'list-requests' return render(request, 'workspace/list_requests.html', { 'bundle_name': 'list-requests', @@ -18,7 +18,7 @@ def list_new_tenant_requests(request): def view_tenant_request(request, tenant_request_id): - print("Tenant request Id: {}".format(tenant_request_id)), + return render(request, 'workspace/view_tenant_request.html', { 'bundle_name': 'view-request', 'tenant_request_id': tenant_request_id diff --git a/custos_admin_portal/custos_admin_portal/context_processors.py b/custos_admin_portal/custos_admin_portal/context_processors.py new file mode 100644 index 0000000..824ec95 --- /dev/null +++ b/custos_admin_portal/custos_admin_portal/context_processors.py @@ -0,0 +1,87 @@ +import copy +import logging +import re + +from django.apps import apps +from django.conf import settings + +from custos_admin_portal.app_config import CustosAppConfig + +logger = logging.getLogger(__name__) + + +def airavata_app_registry(request): + """Put airavata django apps into the context.""" + print([app for app in apps.app_configs]) + print([app for app in apps.get_app_configs()]) + + airavata_apps = [app for app in apps.get_app_configs() + if isinstance(app, CustosAppConfig)] + print("Custos apps", airavata_apps) + # Sort by app_order then by verbose_name (case-insensitive) + airavata_apps.sort( + key=lambda app: "{:09}-{}".format(app.app_order, + app.verbose_name.lower())) + current_app = _get_current_app(request, airavata_apps) + + return { + 'airavata_apps': airavata_apps, + 'current_airavata_app': current_app, + 'airavata_app_nav': (_get_app_nav(request, current_app) + if current_app else None) + } + + +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)), + '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 _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'): + nav = copy.copy(current_app.nav) + # convert "/djangoapp/path/in/app" to "path/in/app" + app_path = "/".join(request.path.split("/")[2:]) + print(app_path) + 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 + + +def resolver_match(request): + """Put resolver_match (ResolverMatch instance) into the context.""" + return {'resolver_match': request.resolver_match} diff --git a/custos_admin_portal/custos_admin_portal/settings.py b/custos_admin_portal/custos_admin_portal/settings.py index 8247c61..d248ab9 100644 --- a/custos_admin_portal/custos_admin_portal/settings.py +++ b/custos_admin_portal/custos_admin_portal/settings.py @@ -38,7 +38,7 @@ INSTALLED_APPS = [ 'webpack_loader', 'custos_admin_portal.apps.auth.apps.AuthConfig', - 'custos_admin_portal.apps.workspace', + 'custos_admin_portal.apps.workspace.apps.WorkspaceConfig', 'custos_admin_portal.apps.admin.apps.AdminConfig' ] @@ -65,6 +65,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'custos_admin_portal.context_processors.airavata_app_registry', ], }, }, diff --git a/custos_admin_portal/custos_admin_portal/static/common/dist/webpack-stats.json b/custos_admin_portal/custos_admin_portal/static/common/dist/webpack-stats.json index 4e733dd..111ba4b 100644 --- a/custos_admin_portal/custos_admin_portal/static/common/dist/webpack-stats.json +++ b/custos_admin_portal/custos_admin_portal/static/common/dist/webpack-stats.json @@ -1 +1 @@ -{"status":"done","publicPath":"http://localhost:9000/static/common/dist/","chunks":{"admin-edit-request":[{"name":"admin-edit-request.js","publicPath":"http://localhost:9000/static/common/dist/admin-edit-request.js","path":"/home/shivam/Programming/custos_UI/custos_admin_portal/custos_admin_portal/static/common/dist/admin-edit-request.js"},{"name":"admin-edit-request.e3e36cbf5bfd0814904f.hot-update.js","publicPath":"http://localhost:9000/static/common/dist/admin-edit-request.e3e36cbf5bfd [...] \ No newline at end of file +{"status":"done","publicPath":"http://localhost:9000/static/common/dist/","chunks":{"admin-edit-request":[{"name":"admin-edit-request.js","publicPath":"http://localhost:9000/static/common/dist/admin-edit-request.js","path":"/home/shivam/Programming/custos_UI/custos_admin_portal/custos_admin_portal/static/common/dist/admin-edit-request.js"},{"name":"admin-edit-request.js.map","publicPath":"http://localhost:9000/static/common/dist/admin-edit-request.js.map","path":"/home/shivam/Programming [...] \ No newline at end of file diff --git a/custos_admin_portal/custos_admin_portal/static/common/package-lock.json b/custos_admin_portal/custos_admin_portal/static/common/package-lock.json index 81b4457..fc49d8d 100755 --- a/custos_admin_portal/custos_admin_portal/static/common/package-lock.json +++ b/custos_admin_portal/custos_admin_portal/static/common/package-lock.json @@ -1,5 +1,5 @@ { - "name": "django-airavata-common-ui", + "name": "custos-admin-portal-ui", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -11968,6 +11968,11 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vuelidate": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/vuelidate/-/vuelidate-0.7.5.tgz", + "integrity": "sha512-GAAG8QAFVp7BFeQlNaThpTbimq3+HypBPNwdkCkHZZeVaD5zmXXfhp357dcUJXHXTZjSln0PvP6wiwLZXkFTwg==" + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", diff --git a/custos_admin_portal/custos_admin_portal/static/common/vue.config.js b/custos_admin_portal/custos_admin_portal/static/common/vue.config.js index 6edcf61..bbe120e 100755 --- a/custos_admin_portal/custos_admin_portal/static/common/vue.config.js +++ b/custos_admin_portal/custos_admin_portal/static/common/vue.config.js @@ -39,6 +39,7 @@ module.exports = { * See also: https://bitbucket.org/calidae/dejavu/src/d63d10b0030a951c3cafa6b574dad25b3bef3fe9/%7B%7Bcookiecutter.project_slug%7D%7D/frontend/vue.config.js?at=master&fileviewer=file-view-default#vue.config.js-27 */ splitChunks: { + chunks: 'all', cacheGroups: { vendors: { name: 'chunk-vendors', diff --git a/custos_admin_portal/custos_admin_portal/templates/base.html b/custos_admin_portal/custos_admin_portal/templates/base.html index 9a4a419..07e2425 100644 --- a/custos_admin_portal/custos_admin_portal/templates/base.html +++ b/custos_admin_portal/custos_admin_portal/templates/base.html @@ -149,6 +149,69 @@ {% endblock %} <div class=c-header__title><a href="/">{% block title %}Custos Portal{% endblock %}</a> </div> + {% if not user.is_authenticated %} + <div class=c-header__controls> + <div class="btn-group"> + <div class=dropdown> + <a href=# class="dropdown-toggle text-dark" id=appDropdownMenuButton data-toggle=dropdown aria-haspopup=true + aria-expanded=false> + {% if current_airavata_app %} + <i class="fa {{ current_airavata_app.fa_icon_class }} mr-2"></i> + {{ current_airavata_app.verbose_name }} + {% elif current_custom_app %} + <i class="fa {{ current_custom_app.fa_icon_class }} mr-2"></i> + {{ current_custom_app.verbose_name }} + {% else %} + Menu + {% endif %} + </a> + <div class=dropdown-menu aria-labelledby=appDropdownMenuButton> + {% for app in airavata_apps %} + {% if app == current_airavata_app %} + <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 %} + {% 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> + <div class="btn-group ml-3"> + <div class=dropdown> + <a href=# class="dropdown-toggle text-dark" id=dropdownMenuButton data-toggle=dropdown aria-haspopup=true + aria-expanded=false> + <i class="fa fa-user mr-2"></i> + {{ request.session.USERINFO.given_name }} + {{ request.session.USERINFO.family_name }} + </a> + <div class=dropdown-menu aria-labelledby=dropdownMenuButton> + <a class=dropdown-item href=#>User settings</a> + <a class=dropdown-item href="#"> + Logout <i class="fa fa-sign-out-alt"></i> + </a> + </div> + </div> + </div> + </div> + {% endif %} </header> <div class=stage> @@ -167,11 +230,6 @@ <i class="{{ nav_item.icon }}"></i> <span class=sr-only>{{ nav_item.label }}</span> </a> {% endfor %} - {% for nav_item in custom_app_nav %} - <a href="{% url nav_item.url %}" class="c-nav__item {% if nav_item.active %}is-active{% endif %}" data-toggle=tooltip data-placement=right title="{{ nav_item.label }}"> - <i class="{{ nav_item.icon }}"></i> <span class=sr-only>{{ nav_item.label }}</span> - </a> - {% endfor %} {% endblock %} </nav> {% comment %}