Customizing Hedwig for a New Facility

Hedwig is designed to be easily customizable for different facilities, via the use of facility-specific view classes, database methods and display templates.

Before starting this guide, you might like first to read the Software Overview, especially the sections on Facility-specific View Classes and Facility-specific Database Access if you have not already done so.

While reading this guide, you might also find it useful to take a look at existing facility-specific code, such as that for JCMT, which you can find in the lib/hedwig/facility/jcmt/ directory.

A Minimal Example View Class

The first step in customizing Hedwig for a new facility is to define a new facility view class. In this example it is assumed that you can place your new facility code within the hedwig namespace. You can create a minimal example as follows:

  1. Create a new sub-directory inside lib/hedwig/facility/ to contain code related to your facility. The name of the directory should be in lower case and there should be an empty __init__.py file inside allow Python to load modules from it (with Python 2).

    An example is included with Hedwig as lib/hedwig/facility/example/.

  2. Create a Python module view.py inside your new facility directory which defines a class. The class should have the same name as your directory but is not limited to lower case. In the case of the example, we define a class Example.

    The class inherits from Hedwig’s base Generic facility class and we override the get_code() and get_name() methods. These are class methods because we need to be able to call them before constructing an instance of the class.

    The facility code is a short, unique, normally lower case, name used to identify your facility. It is also used in file names and URLs.

    from __future__ import \
        absolute_import, division, print_function, \
        unicode_literals
    
    from ..generic.view import Generic
    from .calculator_example import ExampleCalculator
    
    
    class Example(Generic):
        @classmethod
        def get_code(cls):
            """
            Get the "code" name used to represent this facility.
            """
    
            return 'example'
    
        @classmethod
        def get_name(cls):
            """
            Get the name of the facility.
            """
    
            return 'Example Facility'
    
        def get_calculator_classes(self):
            """
            Get a tuple of calculator classes which can be used
            with this facility.
            """
    
            return (ExampleCalculator,)
    
  3. Add the name of your facility class to your etc/hedwig.ini configuration file.

    [application]
    facilities=Generic,Example
    

    (If you do not already have a configuration file, please see the Configuration section of the Installation guide.)

  4. Optional: Create a thumbnail image (picture or icon) to represent your new facility on the home page.

    The image should be placed in the data/web/static/image/ directory and be named facility_<code>.jpeg, where <code> is the short name returned by your view class’s get_code method.

  5. Launch a copy of Hedwig as described in the Running a Test Server section of the Installation guide. If you have not already done so, you will need to set up a database as described in the Database section.

    You should see your new facility on the home page. It will act like the standard Generic Facility until further customized.

Adding Custom Database Features

Each facility is likely to require some custom database tables and associated interface methods. An example is the table required to store the observing request for each proposal – this is not included in the generic code as it is expected to be quite different at different facilities.

Hedwig uses the SQLAlchemy core expression language library for database access. If you are not familiar with this library, you might like to check out the SQLAlchemy expression language tutorial before proceeding.

  1. Create a file meta.py in your facility directory. This file should declare any database tables which your facility needs.

    You should import the SQLAlchemy Metadata container metadata and table options table_opts from the hedwig.db.meta module and use them in each table defined.

    In the example below we declare one new database table, example_request. This table has:

    • A matching Python variable name and table name (first argument to Table).

    • The metadata from hedwig.db.meta (second argument to Table).

    • An integer primary key.

    • A foreign key referencing the proposal table.

    • A unique constraint to prevent the insertion of multiple rows for the same proposal and instrument.

    • Additional options provided by the table_opts value from hedwig.db.meta.

    from __future__ import \
        absolute_import, division, print_function, \
        unicode_literals
    
    from sqlalchemy.schema import Column, ForeignKey, \
        Table, UniqueConstraint
    
    from sqlalchemy.types import Float, Integer
    
    from ...db.meta import metadata, table_opts
    
    
    example_request = Table(
        'example_request',
        metadata,
        Column('id', Integer, primary_key=True),
        Column('proposal_id', None,
               ForeignKey('proposal.id',
                          onupdate='RESTRICT',
                          ondelete='RESTRICT'),
               nullable=False),
        Column('instrument', Integer, nullable=False),
        Column('time', Float, nullable=False),
        UniqueConstraint('proposal_id', 'instrument'),
        **table_opts)
    

    Please see the SQLAlchemy core metadata documentation for more information about how to describe your database tables.

  2. Hedwig generally represents data retrieved from the database via namedtuple types. These can be conveniently defined in terms of the database table’s columns.

    You can do this in a file type.py in your facility directory. This file can also contain any enum-style classes and collection classes which you wish to define. The following example contains a collection class to be used to contain the results of retieving requests from the database. This includes the CollectionByProposal mix-in since the results will include a proposal_id field.

    from __future__ import \
        absolute_import, division, print_function, \
        unicode_literals
    
    from collections import namedtuple
    
    from ...type.base import CollectionByProposal
    from ...type.collection import ResultCollection
    from .meta import example_request
    
    
    ExampleRequest = namedtuple(
        'ExampleRequest',
        [x.name for x in example_request.columns])
    
    
    class ExampleRequestCollection(
            ResultCollection, CollectionByProposal):
        """
        Class to hold a collection of requests to use
        the Example Facility.
        """
    
        pass
    
  3. Create another file control.py in your facility directory. This should declare a class named like your facility, but with the suffix Part. This class will be used as a mix-in when the system constructs a combined database access object.

    Hedwig database access methods are typically given names starting with a verb such as add, delete, get, search, set, sync or update.

    In the following example we allow queries to be performed for multiple proposals at once by using is_list_like() to check whether multiple proposal_id values have been given. If so we assign the corresponding column to iter_field and then, in the transaction with block, use _iter_stmt() to generate a series of queries, each for a block of proposals. (The size of these blocks is controlled by the database access object’s query_block_size attribute.) If iter_field is None then _iter_stmt() simply returns the statement as-is, so we don’t need a second version of the query execution step for the single-proposal case.

    from __future__ import \
        absolute_import, division, print_function, \
        unicode_literals
    
    from ...type.collection import ResultCollection
    from ...db.compat import row_as_mapping
    from ...db.meta import proposal
    from ...util import is_list_like
    from .meta import example_request
    from .type import ExampleRequest, ExampleRequestCollection
    
    
    class ExamplePart(object):
        def search_example_request(self, proposal_id):
            """
            Retrieve observing requests
            for the given proposal or proposals.
            """
    
            stmt = example_request.select()
    
            iter_field = None
            iter_list = None
    
            if proposal_id is not None:
                if is_list_like(proposal_id):
                    assert iter_field is None
                    iter_field = example_request.c.proposal_id
                    iter_list = proposal_id
                else:
                    stmt = stmt.where(
                        example_request.c.proposal_id
                        == proposal_id)
    
            ans = ExampleRequestCollection()
    
            with self._transaction() as conn:
                for iter_stmt in self._iter_stmt(
                        stmt, iter_field, iter_list):
                    for row in conn.execute(
                            iter_stmt.order_by(
                                example_request.c.id.asc())):
                        ans[row.id] = ExampleRequest(**row_as_mapping(row))
    
            return ans
    
        def sync_example_proposal_request(
                self, proposal_id, records):
            """
            Update the observing requests
            for the given proposal.
            """
    
            with self._transaction() as conn:
                return self._sync_records(
                    conn,
                    example_request,
                    example_request.c.proposal_id,
                    proposal_id,
                    records,
                    unique_columns=(
                        example_request.c.instrument,))
    
  4. Ensure that the etc/hedwig.ini configuration file includes your facility in the application.facilities entry so that the system loads your facility-specific code when setting up a database access object.

  5. Update your database to contain your new tables. If you already created a database, then you can use Alembic to automatically update the structure. Otherwise you can initialize your database and it should contain your new tables. Both of these operations are described in the Database section of the Installation guide.

Note

The database tables for all facilities will be stored in the same database namespace. The same also applies to methods which you provide for accessing these tables. Therefore, to avoid custom tables and methods from conflicting with those from other facilities, if they are ever run in the same installation of Hedwig, it is a good idea to prefix the database tables with the facility’s short code name. Database access methods should also include the facility code. For example the JCMT facility include a table jcmt_request with access methods search_jcmt_request and sync_jcmt_proposal_request.

Overriding Display Templates

Hedwig uses Jinja2 templates for HTML pages and email messages.

  • HTML pages

    These templates are rendered by the Jinja2 environment provided by Flask. In Hedwig this is done by the hedwig.web.util.templated() decorator and hedwig.web.util._make_response() function which in turn calls flask.render_template.

    A number of extra template utilities (global functions, filters and tests) are added to the Jinja2 enviroment by the hedwig.web.template_util.register_template_utils() function.

    Templates are located in the following directories:

    • data/web/template/

      This contains the base layout.html template from which most other templates inherit.

    • data/web/template/generic/

      Contains the generic version of the facility-specific templates.

    • data/web/template/<facility code>/

      Contains templates which override the generic versions for a particular facility, along with any additional templates used by the facility.

  • Email messages

    Email messages are formatted by a Jinja2 enviroment set up by the hedwig.email.format.get_environment() function and applied by hedwig.email.format.render_email_template(). This function also adjusts the line wrapping of the message in preparation for it to be sent in flowed format.

    • data/email/

      Again this contains a basic layout.txt template used by other templates.

    • data/email/generic/

    • data/email/<facility code>/

The templates make use of Jinja2’s template inheritance mechanism. For example the generic proposal_view.html template includes a placeholder block for the observing request:

{% block proposal_request %}
{% endblock %}

This can be overridden in a facility-specific template to fill in the observing request section:

{% extends 'generic/proposal_view.html' %}

{% block proposal_request %}
    <section>
        <h2 id="request">Observing Request</h2>

        <p>...</p>
    </section>
{% endblock %}

When overriding the generic templates, you may find that the section you wish to change is already marked as a block, like the proposal_request block above. It may or may not already include a generic implementation. Sometimes the part may not yet be enclosed in a block — then you should consider editing the generic template to insert a block in order to make it easier for you to create a child template.

Note that a template block can also call super to include the content of the parent block.

{% extends 'generic/some_template.html' %}

{% block content %}
<p>
    This is an extra note which this facility
    needs to show on this page.
</p>

{{ super() }}
{% endblock %}

Overriding View Methods

To customize the behavior of Hedwig for your facility, you will also need to override and add new methods to the facility view class. Some of these are used by the system to get information about your facility, while others implement handling code for the various web pages which make up the user interface.

Informational Methods

You may have noticed that the generic facility’s Generic class includes most of its functionality through mix-in classes. This is done simply to help organize the source code into manageable components. If you browse through the Generic class itself, you will find a number of methods you may wish to override, following on from the get_code() and get_name() methods discussed above. Some examples of such methods are:

  • make_proposal_code() and _parse_proposal_code()

    These methods should implement your facility’s scheme for numbering proposals. Provided you can extract the semester, queue, call type and proposal number from a formatted proposal code, you should only need to override the protected method _parse_proposal_code() and you can leave the generic public method parse_proposal_code() to look up the project in the database. If this is not possible you may have to override the parse_proposal_code() method itself.

  • make_archive_search_urls()

    If your facility has a data archive which can be searched by sky coordinates, you can override this method to provide a routine capable of producing a list of suitable URLs.

  • make_proposal_info_urls()

    This can be used to make a list of URLs related to previous proposals, for example search your archive for observations for a given project, or to see the project’s details in any online tracking system you may have.

  • calculate_overall_rating()

    During the review process, the reviewers may enter numerical ratings for each proposal. This method should implement your facility’s algorithm for combining these ratings into a single overall rating for each proposal.

    This method can typically be customized by providing an alternative get_review_rating_weight_function() method to determine the rating and weight factor for each review.

  • calculate_affiliation_assignment()

    This method should implement your rules for determining the affiliation of a proposal based on the affiliations of its members.

  • get_text_roles()

    This method should return the enum-style class defining text roles for the facility. This will usually be a subclass of BaseTextRole.

  • get_reviewer_roles()

    This method should return the enum-style class defining reviewer roles for the facility. This will usually be a subclass of BaseReviewerRole.

Web Interface Handling Methods

The majority of the methods defined in the facility view class are used to handle requests made to the web interface. These methods are attached to routes by the hedwig.web.blueprint.facility.create_facility_blueprint() function. Each view function accepts a number of arguments, such as the database access object, information from the URL path and query parameters, and information from an HTML form, if the request was a POST. If the route is associated with a Jinja2 template, then the view function should return the context dictionary to be used to render the template.

For example, the GenericProposal mix-in which forms part of the Generic view class includes a method view_proposal_view() which is used to show a complete proposal. This method creates an initial context dictionary and then calls an additional private method _view_proposal_extra() to gather more information. Here is an abbreviated version of these methods:

@with_proposal(permission=PermissionType.VIEW)
def view_proposal_view(
        self, current_user, db, proposal, can, args):
    ctx = {
        'title': proposal.title,
    }

    ctx.update(self._view_proposal_extra(
        current_user, db, proposal, auth_cache=can.cache))

    return ctx

def _view_proposal_extra(
        self, current_user, db, proposal, auth_cache=None):
    role_class = self.get_text_roles()
    proposal_text = db.search_proposal_text(proposal.id, with_text=True)

    extra = {
        'abstract': proposal_text.get_role(role_class.ABSTRACT, None),
    }

    return extra

To extend this for another facility we need only override the _view_proposal_extra() private method, for example:

def _view_proposal_extra(
        self, current_user, db, proposal, auth_cache=None):
    ctx = super(Example, self)._view_proposal_extra(
        current_user, db, proposal, auth_cache=auth_cache)

    requests = db.search_example_request(proposal.id)

    ctx.update({
        'example_requests': requests,
    })

    return ctx

This example illustrates how we can get the “extra” context from the superclass (normally the Generic facility), when it is appropriate to do so, and add additional information to it. You will probably encounter this pattern of methods in several places within Hedwig. As with overriding templates, where this mechanism is already established, you can use it to easily extend the system’s functionality. Otherwise, you should consider breaking up the appropriate method into a public (fixed) part and protected part (for overriding) so that you can add facility-specific behavior without having to re-implement the whole view function.

The above example also includes the hedwig.view.util.with_proposal() decorator. This is a convenience routine which intercepts the db and proposal_id arguments and fetches the proposal from the database. It then gets the current authorization object (”can”) for the proposal and checks that the user has the specified permission. The decorated function is then called with the db object, proposal (in place of the proposal_id), “can” authorization object (as an additional argument) and any remaining arguments.

How Hedwig Configures Facilities

This section gives description of how Hedwig loads facility-specific code and sets up the web application.

The web application is constructed by hedwig.web.app.create_web_app() which in turn uses the get_database() and get_facilities() functions from the hedwig.config module to obtain the necessary information before calling create_facility_blueprint().

  1. get_database() uses a private function _get_db_class() which reads the list of facilities from the configuration file.

    See also the Facility-specific Database Access section of the Software Overview for an introduction to this process.

    • If the facility is given with a complete package and class name then the function tries to load meta and control modules from the same package.

      For example if the facility class was mypackage.view.MyFacility, we would try to import modules mypackage.meta and mypackage.control.

    • If the facility is given as a plain class name, it is assumed to be located in a module hedwig.facility.<lower case name>.view. We try to import modules hedwig.facility.<lower case name>.meta and hedwig.facility.<lower case name>.control.

    • Importing the meta module will cause the database table information to be associated with the metadata object in hedwig.db.meta.

    • A class <facility name>Part is loaded from the control module and used as part of the dynamically-generated CombinedDatabase class.

    • Finally an instance of the CombinedDatabase class is constructed, using the SQLAlchemy engine object prepared by hedwig.db.engine.get_engine().

  2. get_facilities() first gets a list of facility classes with the assistance of the private function _import_class().

    See also the Facility-specific View Classes section of the Software Overview for an introduction to this process.

    • If a complete package and class name is provided then that class is loaded.

    • Otherwise if a plain class name is given, it is loaded from the module hedwig.facility.<lower case name>.view.

  3. It can then go ahead and construct an instance of each facility class using the private function _create_facility() as follows:

    • It obtains the facility’s code using the class method get_code().

    • It calls the database’s ensure_facility() method to get the facility identifier. This is a unique integer used within the database to refer to the facility. If the facility code was not found, this database method adds it to the database and returns the newly-assigned identifying number.

    • The facility identifier is passed to the facility class constructor which records it as the id_ attribute. Therefore within a facility method, the identifier can always be obtained from self.id_.

    • The facility’s get_calculator_classes() method is used to get a tuple of calculator classes.

      • For each calculator, get_code() is called to get the short code name. This is converted to an identifier using ensure_calculator() and then the calculator can be constructed, giving the facility view object and calculator identifier.

      • A CalculatorInfo instance including the calculator object is stored in the facility’s calculators dictionary, by identifier.

    • The facility’s get_target_tool_classes() method is used to get a tuple of target tool classes.

      • As for calculators, for each tool the code is obtained from the get_code() method. There are currently no tool identifiers since no database tables refer to target tools — a placeholder tool identifier is passed to the tool constructor along with the facility view object.

      • A TargetToolInfo instance including the target tool object is stored in the facility’s target_tools dictionary, by identifier.

  4. A FacilityInfo instance, with the constructed facility view object stored in the view attribute, is added to the facilities ordered dictionary. This dictionary is returned by get_facilities().

    The dictionary is used by some of the other blueprints — for example the “home” blueprint uses it to show each of the facilities on the home page.

  5. The create_web_app() function passes the facility view object to create_facility_blueprint() in order to create a Flask blueprint which will be registered in the application with an URL prefix containing the facility code.

    • An additional template context parameter facility_name is set up using the value returned by the get_name() method.

    • All of the fixed facility routes are registered with the blueprint, using functions which invoke the methods of the view object.

    • Each calculator is configured:

      • Calculator routes are then registered with the blueprint.

      • A default redirect route is added for the calculator’s first mode, allowing links to be created to the calculator without specifying a mode.

    • Each target tool is configured:

      • Various routes for each target tool are registered.

      • The target tool’s get_custom_routes() method is called to determine whether it has any additional routes which should be registered with the blueprint.

Adding Calculators

A “calculator” is a class which can perform any kind of calculation relevant for the preparation of proposals. These are typically integration time calculators, used to estimate the time required for an observation, but in principle other types of calculators can be added.

The calculator class is often just a type of “wrapper” which interfaces an existing Python package with the Hedwig calculation system. The existing package would already implement the required numerical routines but would not want to be contain Hedwig-specific functionality. Even if you are creating an entirely new calculator, you should consider structuring your calculator in this way because it will make it easy for you to use the separate calculation package from other code unrelated to Hedwig.

Calculators have the following properties:

  • A proposal needs to record the exact input and ouput of the calculator as it was at the time the proposal was prepared. Therefore calculations, including their results, are stored in the calculation database table.

    The input and output values of each calculation are stored as JSON-encoded dictionaries using a custom database columm class JSONEncoded.

  • A calculator can have several modes of operation. The first defined mode is considerered to be the “default” mode and will be the mode accessed by an URL which omits the mode code. The calculator should be able to convert calculations between its different modes. For example if an integration time calculator has “time” (as a function of sensitivity) and “sensitivity” (as a function of time) modes, it should be able to convert a “sensitivity” calculation to a “time” calculation by taking the output sensitivity and making it an input to the “time” calculation.

  • A calculator must have a (integer) version number which will be saved in the calculation table with any calculations attached to proposals. It must support the following version-specific operations:

    • Getting a list of input parameters for any version of the calculator. This is necessary for proposals to know how to format the inputs of calculations saved with a previous version.

    • Converting a set of input parameters from any version into a set of inputs usable with the current version. This is used whenever someone tries to re-open a saved calculation.

    • Getting a list of output parameters for any version. Again this is so that a calculation can be formatted for display on a proposal.

  • The calculator should also be able to determine the version of the underlying calculation code. When the calculations are performed by a separate Python module, this can be the version (string) of that module.

When you are ready to start implementing a new calculator class for Hedwig, you can begin as follows:

  1. Add a new module to your facility directory which implements a class derived from the hedwig.view.calculator.BaseCalculator class.

  2. Import your calculator class in your facility’s view module and add it to the tuple returned by your get_calculator_classes() method.

  3. Override the get_code() method so that it returns a short code name which will be unique in all facilities which may use the calculator.

    Also override get_name() to return the calculator’s name for display purposes and define an integer class version attribute.

  4. Define the modes implemented by your calculator. Each mode needs:

    • An integer identifier.

    • A short unique code name.

    • A display name.

    A calculator class will normally define “constant” variables for the mode identifiers. It must then define a class variable modes which is an ordered dictionary mapping mode identifiers to CalculatorMode tuples containing the code and name. For example:

    class AnotherExampleCalculator(BaseCalculator):
        TIME = 1
        SENSITIVITY = 2
    
        modes = OrderedDict((
            (TIME, CalculatorMode(
                'time',
                'Time for given sensitivity')),
            (SENSITIVITY, CalculatorMode(
                'sensitivity',
                'Sensitivity for given time')),
        ))
    
        version = 1
    
  5. Next you will need to define the inputs and outputs of your calculator. Each of these values is specified by a CalculatorValue tuple.

    The inputs and outputs are defined by implementing the methods get_inputs() and get_outputs(). Both of these take the calculator mode and version as arguments and return a list of values. In the case of the input values, this list should be an instance of SectionedList because the calculator_base.html template displays the section names as separators between parts of the HTML form. This class may also make it easier to constuct the input value list for different versions/modes of a calculator. However if you override the whole calculator_input_list template block then a SectionedList is not required and the standard list type can be used.

  6. Completing the calculator class involves overriding several more methods in order to implement your calculator’s functionality. Please take a look at the source code for the BaseCalculator and ExampleCalculator classes.

    The main view handling method is hedwig.view.calculator.BaseCalculator.view() and this is intended to be used for most calculators. It calls other calculator methods which implement the functionality which is expected to change for different calculators. It is therefore recommended that you do not override the view() method unless you are writing an unusual type of calculator for which it is not appropriate.

    Methods which you are likely to wish to customize include:

    get_calc_version()

    This returns the version (string) of the underlying calculation routine. This is just used for information in order to allow users to identify the version of a calculator which produced a given result.

    This should not be confused with the version attribute which controls the version of the interface with Hedwig — i.e. the list of input and output values for a given mode and version.

    get_default_input()

    This should return sensible default input parameters for any mode.

    convert_input_mode()

    Adjusts the input parameters when changing mode.

    convert_input_version()

    Converts the input parameters for any given version of the calculator so that they will work with the current version.

    parse_input()

    Parses values retrieved from the input form.

    This is separate from the get_form_input() method, which you may not need to override, in order to improve handing of invalid input.

    __call__()

    Performs the actual calcuation, returning a CalculatorResult tuple.

  7. Finally you will probably need to customize the HTML template used for your calculator. Your template should be named calculator_<calculator code>.html and located in your facility’s template directory. Most calculator templates will extend the generic/calculator_base.html template which contains a large number of inheritance blocks for easy customization.

    Here is the template for the example calculator which adds sections to display additional information from the extra component of the CalculatorResult.

    {% extends 'generic/calculator_base.html' %}
    
    {% block calculator_output_extra_table %}
        <tr>
            <th>Product</th>
            <td>{{ output_extra['product'] }} m&#xb2;</td>
        </tr>
    {% endblock %}
    
    {% block calculator_output_extra %}
        <p>
            Calculation performed in
            {{ output_extra['t_elapsed'] }} s.
        </p>
    {% endblock %}
    

Adding Target Tools

A “target tool” is a class which can perform some kind of analysis of one or more observing target positions. One target tool — the “Clash Tool” (implemented in the ClashTool class) is integrated with Hedwig. It queries the coverage regions stored in the moc, moc_cell and moc_fits database tables and managed through the facility administrative interface. Properties of target tools are:

  • We are generally only interested in the current analysis of the target positions from the most recent version of the tool and its associated data. Therefore tool results are currently not stored in the database. If it ever becomes necessary for a proposal author to refer to exact results from a target tool, or if a target tool becomes computationally expensive to run, then we will need to add database structures to allow target tool results to be saved.

  • Target tools can be run on a single target specified by the user, or applied to a list of targets retrieved from a proposal or uploaded by a user. These comprise the three operational modes of a target tool: “single”, “proposal” and “upload”.

  • In general target tools are simpler than calculators as they do not deal with storing results and tracking version information. However it is not anticipated that their output will fit into a common structure — each tool will likely need a completely different HTML template, at least for the output block.

If you would like to implement a new target tool for use with Hedwig, you can get started as follows:

  1. Add a new module to your facility directory which implements a class derived from the hedwig.view.tool.BaseTargetTool class. If your new target tool is intended to be more generally useful, you might wish to consider adding it to Hedwig’s generic facility instead.

  2. Import your target tool class in your facility’s view module and add it to the tuple returned by your get_target_tool_classes() method.

  3. Override the get_code() method so that it returns a short code name which will be unique in all facilities which may use the target tool.

    Also override get_name() to provide the name of the target tool and get_default_facility_code() if you intend it to be used by multiple facilities.

  4. Provide an implementation of the main target analysis method _view_any_mode(). By default this will be called with a list of target objects for all of the target tool’s modes (single target, uploaded list and proposal-based).

    If you need to implement different behavior in different modes, you can instead override the _view_single(), _view_upload() and _view_proposal() methods directly.

    Any of these view methods returns a dictionary which is used to update the template context. You can add any output from your target tool, any additional information you wish to display on the target tool’s web pages and override existing entries such as the message indicating errors with the user input and the run_button text used for the form’s submit button.

  5. Take a look at the source code of the BaseTargetTool and ClashTool (as an example) classes in case there are any other methods you would like to override.

  6. You will need to provide an HTML template to display your target tool’s output. This should be named tool_<tool code>.html and be located in the facility’s template directory. The template should almost always inherit from the generic/tool_base.html template which provides the input form for single targets and target list uploads.

    Here is a simple example target tool HTML template. Note that we check whether we have output to display — the base template calls this block in either case, because there may be fixed information we wish to show. Here we check if some output, assumed to be stored in the template context value tool_output, is None. The view function would need to ensure that it sets this value to None if called without a list of target objects to analyze.

    {% extends 'generic/tool_base.html' %}
    
    {% block tool_output %}
        {% if tool_output is not none %}
            <p>Target tool output here...</p>
        {% endif %}
    {% endblock %}
    
  7. If necessary for your target tool, you can add custom routes by overriding the get_custom_routes() method and writing the appropriate view functions and HTML templates.