# How to Integrate a New Module This guide helps in extending HELIPORT. You will find that Extensions in HELIPORT are just Django apps. But they may use the available [functionality from heliport](#use-heliport-functionality) and [hook](#heliport-hooks) into the HELIPORT core to integrate seamlessly. ## Quick Start 1. **Start your Django app** by running the following command inside the top-level `heliport` module (i.e. `$REPO_ROOT/heliport`): ```bash heliport-cli startapp your_app_name ``` This will create a new app directory on the same level as the existing ones. 2. **Implement your Django app.** A simple test view is sufficient for now. For a more serious implementation you may find it useful to [use HELIPORT functionality](#use-heliport-functionality). A relatively quick path could be to to create a [DigitalObject subclass](#writing-models) and a [DigitalObjectListView](#writing-views) for it. 3. **Add your URLs** by adding `path("", include("heliport.your_app_name.urls"))`, to the URL patterns in `heliport_config/urls.py`. If your app exposes a Django Rest Framework API, also import the router in this file and register it in the `api_routers` list. 4. **Register your app** in `heliport_config/settings.py` by adding `your_app_name` to the `HELIPORT_SYSTEM_APPS` list, which just adds the app to `INSTALLED_APPS` but keeps HELIPORT apps separate. The most basic way to make you app do something in HELIPORT is by providing a [HELIPORT Module](#modules) which creates an entry to your app from the project graph. 5. **Update the database** after you changed the database schema: :::{note} In some environments `your_app_name` might not be required as by default migrations for all apps are made. ::: ```bash heliport-cli makemigrations your_app_name heliport-cli migrate ``` ## Use HELIPORT functionality ### Writing Templates Use the heliport `base.html` in your templates: - Begin your template with `{% extends "core/base/base.html" %}` - Implement ```html {% block content %} {% endblock %} ``` - If you have scripts use ```html {% block script %} {% endblock %} ``` ### Writing Models To create Django models that make use of HELIPORTS functionality, for example that can be added to a HELIPORT project, inherit from `DigitalObject` (which inherits from Django's `Model` class). ```python # my_app_name/models.py from heliport.core.models import DigitalObject from django.db import models class MyModel(DigitalObject): my_model_id = models.AutoField(primary_key=True) ... ``` Consider reading the HELIPORT documentation on {class}`heliport.core.models.DigitalObject`. Here is an overview: - Digital Objects help with access control. See {meth}`heliport.core.models.DigitalObject.access` - To save a Digital Object including project and namespace, you can use {meth}`heliport.core.models.DigitalObject.save_with_namespace` - Allow user to invoke actions defined in other apps by calling {meth}`heliport.core.digital_object_interface.GeneralDigitalObject.actions`. (See also how to [register actions for other apps](#define-actions)) - Define how the metadata is exported by registering a [serializer](#metadata) - Manage dynamic attributes of your objects by using tags ({meth}`heliport.core.models.DigitalObjects.add_tag`) or setting relations ({func}`heliport.core.models.assert_relation`) ### Writing Views - To use heliport authentication, inherit from one of the following mixins: - {class}`heliport.core.mixins.HeliportLoginRequiredMixin` is the most basic and just asserts that a user to be logged in - {class}`heliport.core.mixins.HeliportProjectMixin` asserts that the user has access to the current project - {class}`heliport.core.mixins.HeliportObjectMixin` asserts that the user has access to the current digital object - To let HELIPORT "generate" a view and template for you (and of course also handle authentication), create a View inheriting from {class}`heliport.core.views.generic.HeliportObjectListView`. - To be able to retrieve rendered HTML partials from the API, {class}`heliport.core.renderers.HeliportPartialRenderer` can be used. Check {class}`heliport.core.views.api.MaintenanceMessageView` for an example. - {class}`heliport.core.admin.DigitalObjectAdmin` can be used as the default admin view for all digital object classes: `admin.site.register(MyModel, DigitalObjectAdmin)` ## HELIPORT Hooks Apps can register entrypoints to their functionality using hooks to allow interaction between the HELIPORT core and the app as well as interaction between apps. A list of extension points can be found below. An extension point is typically a {class}`heliport.core.utils.collections.DecoratorIndex` (or similar) instance, where a class of appropriate type can be registered. For larger apps we recommend to not register hooks via decorators but instead do it explicitly in `apps.py`, like Django signal connecting. This makes it explicit what extensions an app registers and avoids problems where an extension is no longer registered because the file that contains it is no longer imported anywhere. An example: ```python # my_app/apps.py from django.apps import AppConfig class MyAppConfig(AppConfig): def ready(self): from .models import MyModule from heliport.core.app_interaction import heliport_modules heliport_modules.register(MyModule) ``` Some hooks can not be registered in this way and instead need to exist with correct name in a file called `interface.py` in your app. This file needs to be imported in `__init__.py` of your app like `from . import interface` to be recognized. You need to be careful with cyclical imports. ### Modules Modules represent a bundle of functionality. For example, there may be a module for a specific kind of research object. The entries in the HELIPORT project graph are modules. To register a module implement a subclass of {class}`heliport.core.app_interaction.Module`. You can think of a module like giving a name to a URL that is entrypoint for some bundle of functionality. Read the documentation of that class for more detail. For modules that represent a subclass of {class}`heliport.core.models.DigitalObject` you may use {class}`heliport.core.app_interaction.DigitalObjectModule` which is simpler to implement and usable in more contexts in HELIPORT because of the additional information contained in the {attr}`heliport.core.app_interaction.DigitalObjectModule.object_class`. An example: ```python from django.urls import reverse from heliport.core.app_interaction import DigitalObjectModule class MyModule(DigitalObjectModule): name = "My Module" module_id = "my_module" object_class = MyDigitalObject def get_url(self, project): return reverse("my_app:my_digital_object_list", kwargs={"project": project.pk}) ``` Register your module at {attr}`heliport.core.app_interaction.heliport_modules`. Alternatively a module is automatically recognized by HELIPORT if placed in the `interface.py` file in your app and that file is imported in your `__init__.py`. To show a module in HELIPORT configure it in `settings.py`. Refer to the module by its `module_id`. - Add it to a phase in `HELIPORT_GRAPH`, you can also create an arbitrary new phase. - Add it to the navbar in the top right via `HELIPORT_PROJECT_NAVBAR_ITEMS`. - Add it to the drop-down menu in the navbar via `HELIPORT_PROJECT_DROPDOWN_ITEMS`. - Add dependencies of a module to `HELIPORT_DEPENDENCIES` so that the module only becomes available to a project if the dependencies are already in the project. ### Text Search To make objects in your app searchable with the global text search add a function called `get_search_url()` with no arguments to `interface.py` returning the url to a view that renders a html div containing search results given a query parameter `q=multi word search string` Make sure `from . import interface` is in `__init__.py` of your app for HELIPORT to find this function. ### Project Creation To register a view for creating a HELIPORT Project in a custom way add a function `get_project_create_urls()` with no arguments, returning a `dict` mapping the name of your Project creation method (Arbitrary String) to a URL in your app where the user can create a new HELIPORT project. This is then added to the create options in the HELIPORT project dashboard. Make sure `from . import interface` is in `__init__.py` of your app for HELIPORT to find this function. ### Define Actions To support an action that may be performed on a digital object of any app, subclass {class}`heliport.core.digital_object_actions.Action` and register it to {attr}`heliport.core.digital_object_actions.actions` in `apps.py` of your app. You decide in your implementation for which objects the action is applicable. See also the docs of {mod}`heliport.core.digital_object_actions` for more information. ### External Objects Most of what HELIPORT shows are instances of {class}`heliport.core.models.DigitalObject`. But HELIPORT can also show and work with objects that are not stored in heliport and are dynamically accessed on demand. To register such a type of object you can implement a subclass of {class}`heliport.core.digital_object_interface.GeneralDigitalObject` and register it at {attr}`heliport.core.digital_object_resolution.object_types`. See also the docu on {mod}`heliport.core.digital_object_resolution` for more information. Most importantly the {class}`heliport.core.views.digital_objects.AsDigitalObjectView`, for importing such external object, supports this resolution mechanism. Other examples include views for files and directories. ### Metadata There are different hooks for registering functionality that enhances available metadata in HELIPORT. This mostly is about the metadata export but can also affect some other functionality that can work with general metadata. Registries of these hooks are in {mod}`heliport.core.serializers`, which is also a good place for further information. They are instances of {class}`heliport.core.utils.collections.DecoratorDict` which requires an additional parameter - the key in the dict - when registering something: `static_attributes.register(MyClass)(func_that_returns_list_of_attributes)`. `serializers.py` of your app gets imported by Django Rest Framework and is therefore slightly better for registering the hook as decorator like this: ```python @static_attributes.register(MyClass) def func_that_returns_list_of_attributes(): return [] ``` :::{note} You may find that the `static_attributes` hook, where you need to understand the {mod}`heliport.core.attribute_description` module, and the `rdf_serializers` hook, where you need to understand the {mod}`heliport.core.vocabulary_descriptor` module, are complex. An alternative approach already exist, and gets included in the serialization in the future, where you basically just need to know the rdflib library and the process of running the serialization is also much simpler. Have a look at {class}`heliport.core.utils.collections.RootedRDFGraph` which you can produce just by implementing {meth}`heliport.core.utils.serialization.GeneralValue.as_rdf`. Note that {class}`heliport.core.utils.serialization.GeneralValue` is a base class of all kinds of digital objects in HELIPORT. ::: These extension points are available: #### {attr}`heliport.core.serializers.static_attributes` / {func}`heliport.core.serializers.register_digital_object_attributes`: Register a function that returns a list of {class}`heliport.core.attribute_description.BaseAttribute` subclass instances. This is used to describe the static Django database model for example to be included in the metadata export. Static attributes can also include constant attributes like a type that is the same for all instances. See {class}`heliport.core.attribute_description.TypeAttribute`. Normally you would register attributes for a subclass of {class}`heliport.core.models.DigitalObject`. For this the decorator {func}`heliport.core.serializers.register_digital_object_attributes` exists, which includes some general attributes automatically. It is also possible to register attributes for arbitrary classes by using {attr}`heliport.core.serializers.static_attributes`, with the `static_attributes.register(cls)` decorator. Also have a look at `rdf_serializers` below in that case. #### {attr}`heliport.core.serializers.datacite_serializers`: Register a subclass of {class}`heliport.core.serializers.DATACITESerializer` to allow serializing a class to DataCite metadata. If you want to register a serializer for a class that is a subclass of {class}`heliport.core.models.DigitalObject`, you can register a subclass of {class}`heliport.core.serializers.DigitalObjectDATACITESerializer` where you often can simply set just the `resource_type` and in some cases `related_identifiers`. #### {attr}`heliport.core.serializers.rdf_serializers`: A more flexible alternative to {attr}`heliport.core.serializers.static_attributes` where you can just generate triples as you like. Register a subclass of {class}`heliport.core.serializers.RDFSerializer` for your class. {class}`heliport.core.serializers.DigitalObjectRDFSerializer` is the serializer that implements the RDF serialization using {attr}`heliport.core.serializers.static_attributes` and also includes the dynamic attributes. If you want to serialize things that are not digital objects you need to implement your own {class}`heliport.core.serializers.RDFSerializer` optionally also making use of {attr}`heliport.core.serializers.static_attributes`. #### {attr}`heliport.core.serializers.renderers`: For renderers that are not RDF or DataCite or to support additional serialization formats. ### Special Digital Objects You can implement additional interfaces for your Django models that inherit from {class}`heliport.core.models.DigitalObject`. To do this inherit additionally from one or more of the classes in {mod}`heliport.core.digital_object_aspects` and implement the respective interface. Digital object classes implementing these interfaces are automatically found and recognized by HELIPORT in some places. There is a dedicated guide for this at [Special Digital Objects](special-digital-objects.md). ### Customize Digital Object Subclass Some behaviour can be tweaked by implementing / overwriting certain methods in your {class}`heliport.core.models.DigitalObject` subclass. - `get_custom_view_url(user) -> str` If this function does not return `None`, but a URL, the user can be redirected to a special page representing the digital object instead of the landing page. The user parameter can be used to avoid redirecting to a page where the user has no permission. The user may be `None` if there is no logged-in {class}`heliport.core.models.HeliportUser`. This gets evaluated in the {meth}`heliport.core.views.landing_page.LandingPageView.get_custom_url`. - As digital objects inherit from {class}`heliport.core.utils.serialization.GeneralValue` you can overwrite the conversion functions defined there as appropriate.