Adding a Django App

The functionality of the Airavata Django Portal is broken up into separate Django apps. The apps live in the django_airavata/apps directory. When adding new functionality to the Django portal it may make sense to add it as a new separate Django app instead of adding it to an existing app. The following steps document how to do this.

Create the new Django App

For this example, assume the name of the app is myapp. The following also assumes you have sourced your virtual environment.

cd airavata-django-portal
mkdir django_airavata/apps/myapp
python manage.py startapp myapp django_airavata/apps/myapp

Integrating with the Django Portal

AppConfig settings

Edit the AppConfig so that it extends the AiravataAppConfig and fill in the required details:

from django_airavata.app_config import AiravataAppConfig


class MyAppConfig(AiravataAppConfig):
    name = 'django_airavata.apps.myapp'
    label = 'django_airavata_myapp'
    verbose_name = 'My App'
    app_order = 10
    url_home = 'django_airavata_myapp:home'
    fa_icon_class = 'fa-bolt'
    app_description = """
        My app for doing stuff in the Airavata Django Portal.
    """

Some of these are self explanatory, but here are some details on each of these properties:

  • name - this is the python package of the app
  • 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
  • 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.
  • url_home - namespaced url of the "home" page of this app. This will be the url used when a user selects this app in a navigational menu.
  • fa_icon_class - a FontAwesome icon class. See the list of available icons for v. 4.7.
  • app_description - description of this app

Add AppConfig to INSTALLED_APPS

Edit INSTALLED_APPS in settings.py:

INSTALLED_APPS = [
  # ...
  'django_airavata.apps.myapp.MyAppConfig'
]

Add Webpack bundle loader config to settings.py

If the new app has Webpack built frontend, then add the following configuration to webpack_loader_util.py:

...
'MYAPP': {
  'BUNDLE_DIR_NAME': 'django_airavata_myapp/dist/',
  'STATS_FILE': os.path.join(
      static_root if static_root else
      os.path.join(
          BASE_DIR,
          'django_airavata',
          'apps',
          'myapp',
          'static',
      ),
      'django_airavata_myapp',
      'dist',
      'webpack-stats.json'),
},
...

Add the apps urls to the site's urls.py

Edit django_airavata/urls.py and add the app's urls config:

urlpatterns = [
    url(r'^djadmin/', admin.site.urls),
    url(r'^admin/', include('django_airavata.apps.admin.urls')),
    url(r'^auth/', include('django_airavata.apps.auth.urls')),
    url(r'^workspace/', include('django_airavata.apps.workspace.urls')),
    url(r'^api/', include('django_airavata.apps.api.urls')),
    url(r'^groups/', include('django_airavata.apps.groups.urls')),
    # ... Add the app urls here
    url(r'^myapp/', include('django_airavata.apps.myapp.urls')),
    # ...
    url(r'^home$', views.home, name='home'),
    url(r'^cms/', include(wagtailadmin_urls)),
    url(r'^documents/', include(wagtaildocs_urls)),
    url(r'', include(wagtail_urls)),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

It's important that the app urls are added before the wagtail_urls since wagtail controls those urls.

App urls.py and base template

Let's add a starter home page and urls.py config for this app. Create a urls.py file in myapp/:

from django.conf.urls import url

from . import views

app_name = 'django_airavata_myapp'
urlpatterns = [
    url(r'^home$', views.home, name='home'),
]

Add a view function called home in views.py:

from django.shortcuts import render


def home(request):
    return render(request, 'django_airavata_myapp/home.html')

Create a templates directory called in myapp called templates/django_airavata_myapp/.

Then create a base template in that directory called base.html. We'll create this file assuming that it will load webpack bundles generated by vue-cli:


{% extends 'base.html' %}

{% load static %}
{% load render_bundle from webpack_loader %}

{% block css %}
{% render_bundle 'chunk-vendors' 'css' 'MYAPP' %}
{% comment %}BUT NOTE: if you only have one entry point you won't have a 'chunk-common' bundle so you may need to comment out the next line until you have more than one entry point.{% endcomment %}
{% render_bundle 'chunk-common' 'css' 'MYAPP' %}
{% render_bundle bundle_name 'css' 'MYAPP' %}
{% endblock %}

{% block nav-items %}

        {% comment %}Define the left side navigation for drilling into sub pages of your app{% endcomment %}
        <a href="{% url 'django_airavata_myapp:function1' %}"
            class="c-nav__item {% if request.active_nav_item == 'function1' %}is-active{% endif %}"
            data-toggle=tooltip data-placement=right title="Function #1">
            <i class="fa fa-bolt"></i> <span class=sr-only>Function #1</span>
        </a>
        <a href="{% url 'django_airavata_myapp:function2' %}"
            class="c-nav__item {% if request.active_nav_item == 'function2' %}is-active{% endif %}"
            data-toggle=tooltip data-placement=right title="Function #2">
            <i class="fa fa-bolt"></i> <span class=sr-only>Function #2</span>
        </a>
        <a href="{% url 'django_airavata_myapp:function3' %}"
            class="c-nav__item {% if request.active_nav_item == 'function3' %}is-active{% endif %}"
            data-toggle=tooltip data-placement=right title="Function #3">
            <i class="fa fa-bolt"></i> <span class=sr-only>Function #3</span>
        </a>

{% endblock %}

{% block content %}
<div id="{{ bundle_name }}"/>
{% endblock %}


{% block scripts %}
{% render_bundle 'chunk-vendors' 'js' 'MYAPP' %}
{% comment %}BUT NOTE: if you only have one entry point you won't have a 'chunk-common' bundle so you may need to comment out the next line until you have more than one entry point.{% endcomment %}
{% render_bundle 'chunk-common' 'js' 'MYAPP' %}
{% render_bundle bundle_name 'js' 'MYAPP' %}
{% endblock %}

Now, create a home.html template:

{% extends './base.html' %} {% load static %} {% block css %} {% comment %}This
isn't a Vue.js app, so just turn off loading CSS.{% endcomment %} {% endblock %}
{% block content %}

<h1>Hello World!</h1>

{% endblock content %} {% block scripts %} {% comment %}This isn't a Vue.js app,
so just turn off loading JavaScript.{% endcomment %} {% endblock %}

Now if you log into the Django portal you should see "My App" in the menu at the top and clicking on it should display the home page of this app.

JS build config - Vue.js

Now we'll add JavaScript build config to the app using Vue.js, npm and webpack.

Add a package.json file to the app's directory (i.e., django_airavata/apps/myapp):

{
  "name": "django-airavata-myapp-views",
  "description": "A Vue.js project",
  "version": "1.0.0",
  "author": "Marcus Christie <machristie@apache.org>",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "bootstrap": "^4.0.0-beta.2",
    "bootstrap-vue": "^1.4.1",
    "django-airavata-api": "file:../api",
    "django-airavata-common-ui": "file:../../static/common",
    "vue": "^2.5.21"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.1.1",
    "@vue/cli-plugin-eslint": "^3.1.1",
    "@vue/cli-service": "^3.1.1",
    "babel-eslint": "^10.0.1",
    "eslint": "^5.8.0",
    "eslint-plugin-vue": "^5.0.0-0",
    "vue-template-compiler": "^2.5.21",
    "webpack-bundle-tracker": "^0.4.2-beta"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": ["plugin:vue/essential", "eslint:recommended"],
    "rules": {},
    "parserOptions": {
      "parser": "babel-eslint"
    }
  },
  "postcss": {
    "plugins": {
      "autoprefixer": {}
    }
  },
  "browserslist": ["> 1%", "last 2 versions", "not ie <= 8"]
}

Add a babel.config.js to this directory too:

module.exports = {
  presets: ["@vue/app"]
};

Now add a vue.config.js file too:

const BundleTracker = require("webpack-bundle-tracker");
const path = require("path");
const staticDir = process.env.STATIC_ROOT ? process.env.STATIC_ROOT : "./static/";

module.exports = {
  publicPath:
    process.env.NODE_ENV === "development"
      ? "http://localhost:9000/static/django_airavata_myapp/dist/"
      : "/static/django_airavata_myapp/dist/",
  outputDir: staticDir + "django_airavata_myapp/dist",
  pages: {
    home: "./static/django_airavata_myapp/js/entry-home"
    // additional entry points go here ...
  },
  css: {
    loaderOptions: {
      postcss: {
        config: {
          path: __dirname
        }
      }
    }
  },
  configureWebpack: {
    plugins: [
      new BundleTracker({
        filename: "webpack-stats.json",
        path: staticDir + "django_airavata_myapp/dist/"
      })
    ],
    optimization: {
      /*
       * Force creating a vendor bundle so we can load the 'app' and 'vendor'
       * bundles on development as well as production using django-webpack-loader.
       * Otherwise there is no vendor bundle on development and we would need
       * some template logic to skip trying to load it.
       * 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: {
        cacheGroups: {
          vendors: {
            name: "chunk-vendors",
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: "initial"
          },
          common: {
            name: "chunk-common",
            minChunks: 2,
            priority: -20,
            chunks: "initial",
            reuseExistingChunk: true
          }
        }
      }
    }
  },
  chainWebpack: config => {
    /*
     * Specify the eslint config file otherwise it complains of a missing
     * config file for the ../api and ../../static/common packages
     *
     * See: https://github.com/vuejs/vue-cli/issues/2539#issuecomment-422295246
     */
    config.module
      .rule("eslint")
      .use("eslint-loader")
      .tap(options => {
        options.configFile = path.resolve(__dirname, "package.json");
        return options;
      });
  },
  devServer: {
    port: 9000,
    headers: {
      "Access-Control-Allow-Origin": "*"
    },
    hot: true,
    hotOnly: true
  }
};

You'll customize pages by modifying and/or adding additional entry points and you'll need to modify publicPath and outputDir and the BundleTracker config to correspond to your folder structure.

Now create a static folder for holding javascript code. For this example we would create static/django_airavata_myapp/js. In this folder you can put the entry points, for example entry-home.js.

For each entry point you'll create a template, extending your app's base.html and including that entry points generated css and js file. See Adding an entry point for further instructions.

For a complete example, see the workspace app.

build_js.sh build script

In the root of the project is a master build script, build_js.sh, that generates a production build of all of the JS frontend code in the project. Add a line in there for your Django app, like so:

...
(cd $SCRIPT_DIR/django_airavata/apps/myapp && npm install && npm run build) || exit 1

You can test it by running ./build_js.sh in the root folder.