Django Application

We’ll concentrate here on the various aspects of setting up and create the Django application.

AppConfig

The AppConfig in Oxylus is responsible of different extra things:

  • specifying application dependencies and metadata: this will later be used in order to create a user interface for installing and creating application.

  • information displayed for the user (icon, root url).

  • assets: provide Vite project built for the client application.

After the application has been created, in apps.py, some changes will be needed. Lets take at this example:

from django.utils.translation import gettext_lazy as _
from ox.core import apps


__all__ = ("AppConfig",)


class AppConfig(apps.AppConfig):
    name = "my_app"
    label = "my_app"
    verbose_name = _("My App")

    # Declare an icon used for rendering, using Material design icon
    icon = "mdi-card-account-mail"

    # Root url to use for API, templates and statics
    root_url = "my_app"

    # Name of the npm package used for frontend apps
    npm_package = "my_app"

    # Declare app dependencies (only Django applications)
    # This is used by installer to install/setup the dependencies
    # dependencies = ("django.contrib.auth", "ox.apps.auth")

Application Assets

Oxylus handles assets management, using declaration assets. It uses two classes here:

  • Asset: declaration for a npm package, with specified distributed file (js, js for development, and css);

  • Assets: this is the actual declaration for an application of its npm package, with entry points, dependencies and so on.

Those classes are used for multiple purposes:

  • Automatically include relevant javascript and stylesheets into the rendered Django template;

  • Generate importmap, which is mandatory if you don’t bundle all javascript code;

  • Finding statics in development mode, and collect them for production;

from pathlib import Path
# ...
from ox.core.assets import Asset, Assets

class AppConfig(apps.AppConfig):
    # ... other attribute

    assets = Assets(
        # Assets directory
        Path(__file__).parent / "assets",
        includes=[
            # Application entry point
            Asset("", "index.js"),
        ],
        dependencies=[
            # We require Oxylus dependencies
            apps.ox_assets,
            # We don't bundle chat.js in the compiled javascript app
            # So we provide it here
            Asset("chart", "chart.umd.min.js", dev_js="chart.umd.js"),
        ]
    )

The Assets has an extra attribute name that we don’t need to provide at this point: it will be set at the AppConfig instanciation. This allows to reuse the same assets declaration for multiple applications.

Models

from django.db import models
from django.utils.translation import gettext_lazy as _

# Use this model subclass as it already provides uuid for external
# references among other things. This uuid is NOT a primary key.
from ox.core.models import Model


__all__ = ("Book", "Author")


class Author(Model):
    first_name = models.CharField(_("First name"), max_length=64)
    last_name = models.CharField(_("Last name"), max_length=64)
    biography = models.TextField(_("Biography"), max_length=64)

    class Meta:
        verbose_name = _("Author")
        verbose_name_plural = _("Authors")


class Book(Model):
    author = models.ForeignKey(Author, models.CASCADE, verbose_name=_("Author"))
    title = models.CharField(_("Title"), max_length=64)
    summary = models.TextField(_("Summary"))
    published = models.DateField(_("Date of publication"))

    class Meta:
        verbose_name = _("Book")
        verbose_name_plural = _("Books")

It is preferred to always use translations; as we will see later, this will be gathered by the vue-i18n management command to make it available to frontend.

Permissions & security

It is a good practice to avoid exposing objects database identifiers. This avoid security pitfalls as access element exploiting predictive id.

Oxylus provide models with a uuid that can then be used from views and viewsets to access the actual resource. Most of how models are designed are not integrated into the platform as we need to see how will the future come. For the moment however there is no implemented way to access objects by uuid when they come from external applications (such as django.contrib.auth).

Regarding the permission systems being used, Oxylus uses two:

  • Django’s basic permission system: simple groups and users permissions;

  • Object permission system based on Django Caps

Serializers

Preferably use Oxylus’s ModelSerializer for serializer, and RelatedField for UUID field targeting another model instance.

from rest_framework import serializers
from ox.core.serializers import ModelSerializer, RelatedField
from . import models


__all__ = ("AuthorSerializer", "BookSerializer")

class AuthorSerializer(ModelSerializer):
    class Meta:
        model = models.Author
        fields = ["first_name", "last_name", "biography"]


class BookSerializer(ModelSerializer):
    # RelatedField is just a wrapper around DRF's SlugField
    # It uses model's UUID as reference instead of internal DB primary key
    author = RelatedField(queryset=models.Author.objects.all())

    class Meta:
        model = models.Book
        fields = ["author", "title", "summary", "published"]

Views

In regular django there is a view par use-case (eg. five views for a model’s CRUD: list, detail, create, edit, delete). In Oxylus however, things are handled differently: this is the client application which will be responsible to render the equivalent views for multiple models. Views are regrouped under a Panel component which is responsible to call API to update information from/on the server among other things.

The backend side is then used for two things: rendering the application view and providing API endpoints. It will also handle overriding templates from other Django applications if (most certainly) required.

There are two main views that can be used for server-side rendering:

  • AppView: base application view;

  • UserAppView: application view requiring user’s login;

However, in most case when declaring your app view you won’t even need to create a custom AppView class. The reason is simple: most of the logic resides in viewsets, and by providing the right attribute values, everything will be handled in a declarative manner.

However, lets take a look to what it might looks like:

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.utils.translation import gettext_lazy as _

from ox.core.views import UserAppView
from .panels import panels

class AppView(PermissionRequiredMixin, UserAppView):
    # we restrict the view to users having thoses permissions
    permission_required = ["auth.view_user", "auth.view_group"]
    # Default panel to display when none is requested by user.
    default_panel = "user-list"

    # Select which panels to render, see below
    panels = panels

    # For the sake of example: this method is used to provide initial data inside
    # the rendered page. It will be loaded when mounting Vue application.
    def get_app_data(self, **kwargs):
        kwargs.setdefault('foo', 123)
        return super().get_app_data(**kwargs)

Hint

We can directly provide some data to the client application, as this avoids extra request and we already have fetched elements from the database. This is what Oxylus does when it provides the current user information behind the scene, or the panel to be displayed. The use should be limited, taking into consideration that API may avoids deduplication of concerns and complexity.

They will be accessible from the application context’s data. [TODO/FIXME]

ViewSets

In order to handle API interaction, Oxylus uses Django Rest Framework <https://www.django-rest-framework.org/> in order to ensure this task. Please refer to its documentation.

Oxylus provides ox.core.views.api.ModelViewSet in order to manipulate objects by uuid among other things.

Lets extend views.py:

# Add to imports
from ox.core.views import ModelViewSet
from . import models, serializers

# The actual viewsets
class BookViewSet(ModelViewSet):
    queryset = models.Book.objects.all().order_by("-id")
    serializer_class = serializers.BookSerializer

    # You can override default filterset_fields
    # filterset_fields = ModelViewSet.filterset_fields
    search_fields = ["title", "summary"]
    ordering_fields = ["title", "published"]

class BookViewSet(ModelViewSet):
    queryset = models.Author.objects.all().order_by("-id")
    serializer_class = serializers.AuthorSerializer
    search_fields = ["last_name", "first_name"]
    ordering_fields = ["last_name", "first_name"]

Templates

The AppView will add an extra template name to look for by Django: [app_config.path_label]/app.html (ox.core.apps.AppConfig.path_label).

The user provided template should extend ox/core/app.html. It will handle loading assets, and rendering the default application layout in which to put the panels’ components.

However, same as for the application view, you won’t need to extend it must of the time, as configuring panels.py and urls.py is sufficient.

In Oxylus, components/ directory is used for components rendered in Django templates. Thoses files should provide only code to allow other application to extend de component, as component are basically a client side concept. We also recommend to include components from the app.html, in order to allow their usage at different places and provide clear structure.

Lets take the example of ox_contacts application: it will extend ox/core/components/model_actions.html to add a button to edit the user linked to a contact.

Panels

A client application contains one or more panels, each dedicated to a specific use case or a model. A panel may contains multiple view, eg. for CRUD there will be at least two: list.table (default list view), and detail.edit (used for: create, edit and display). The delete view is not necessary as it is considered as an action; a button appearing in list and edit view.

Panels are declared inside panels.py. This module will be loaded at initialization and is used to declare Panels and nested Panel - registered to a ox.core.panels.Registry.

This allows two things in a declarative manner:

  • Automate server-side rendering of the application view: components will be integrated based on the provided configuration;

  • Generate navigation menu;

Example of panels.py:

from django.utils.translation import gettext_lazy as _

from ox.core.panels import registry, Panel, Panels

# Declare the panels
panels = Panels(
    "my_app",    # name of the panel
    _("My App"), # label

    # Nested panels (and navigation items)
    items=[
        # Declare a single panel
        Panel(
            "authors",
            _("Authors"),
            "mdi-card-account-details",      # icon
            "myapp-author-panel",            # panel component
            url="my_app:index",              # view url
            order=0,                         # menu item order
            permission="my_app.view_author", # required permissions

            # Optional: use this template to add extra actions for a model
            actions_template="ox/contacts/components/author_actions.html",
        ),
        Panel(
            "books",
            _("Books"),
            "mdi-domain",
            "myapp-book-panel",
            url="my_app:index",
            permission="my_app.view_book",
        ),
    ]
)

# Append the Panels to the registry.
registry.append(panels)

# You also can add a panels to an existing one in the registry, lets say the
# "settings" one:
# registry["settings"].append(panels)

Urls

Each application provides its urls by the exported urls and api_urls lists. It is discovered by Oxylus in order to generate all urls (ox.urls.Router.Discover). One attribute is for views; the other for API endpoints.

The urls will be prefixed with application root_url (defaults to label), such as they are prefixed with:

  • {root_url}/: for views;

  • api/{root_url}: for api entry points. They are namespaced under {{ app.label }}-api, such as for example ox_core-api:account;

For example:

from django.urls import path
from rest_framework.routers import DefaultRouter

from ox.core.views import UserAppView
from . import panels, views

# Use DRF router to register viewsets and generate routes for us
router = DefaultRouter()
router.register(r"author", views.AuthorViewSet, basename="author")
router.register(r"book", views.BookViewSet, basename="book")

api_urls = router.urls # or: `router.urls + [ ... ]`

# Regular views url
urls = [
    # application view as application main page.
    # this is a convention to use "index" as name.
    path(
        "",
        UserAppView.as_view(
            default_panel="books",  # set a default panel
            panels=panels.panels,   # set the actual panels to render
        ),
        name="index"
    )
    # Optional: add other views which require to be on a different page
    # path("settings/", views.SettingsView.as_view(), name="settings"),
]

The generated urls will look like: /my_app/, /my_app/settings/, or /api/my_app/author/.

Important

In Oxylus the url names and paths match the lower case model name (without snake/camel case).

The api views are generated using the DRF’s router, which appends -list, -create, etc. the url name. They will be namespaced under {app.label}-api.

Example: ox_contacts-api:organisationtype-list for api/ox/contacts/organisationtype/ (because: ox_erp.contacts.apps.AppConfig.root_url == "ox/contacts")

There we are…

Lets summarize a bit. We have a Django application with:

  • correct AppConfig;

  • models, serializers and viewsets;

  • panels and urls;

Also remember:

  • internationalize everything you can (aka what is rendered to users);

  • use provided classes as they ensure better security (eg. avoid db primary keys exposure);

Install the app

Lets install the app. Grosso modo, the server is ran from the oxylus application. The packages need to be installed inside its environment. Then setup the application as regular Django app (migrations/collectstatic/etc) needs to be run.

The first step might differ depending whether you on development instance or on a production one:

  • Development: what you want is to be able to run the server and have change taken in account in the running instance. In such case, just create a symbolic link to the application module inside the instance’s environment.

  • Production: what you want is to install a package from pip inside the environment.

Once it is done, just run the following command:

# replace `module.appN` with the module path to the app
./manage.py ox install module.app1 module.app2 # ...

This commands ensure to:

  • setup settings, enabling the app in the conf/plugins.yaml file.

  • run migration, collect assets and statics, install application fixtures.

  • revert in case of error.

  • handle dependencies install declared in ox.core.apps.AppConfig.dependencies;

Well done! What’s next? Let go dive to the frontend application development.