Adding Custom Fields to Targets

Sometimes you’d like to store data for targets but the predefined fields that the TOM Toolkit provides aren’t enough. The TOM Toolkit allows you to define extra fields for your targets so you can associate different kinds of data with them. For example, you might be studying high redshift galaxies. In this case, it would make sense to be able to store the redshift of your targets. You could then do a search for targets with a redshift less than or greater than a particular value, or use the redshift value to make decisions in your science code.

The TOM Toolkit currently supports two different methods for adding extra fields to targets: Extending Target Models and adding Extra Fields.

Extending the Target Model

Users can extend the Target model by creating a custom target model in the app where they store their custom code. This method is more flexible and allows for more intuitive relationships between the new target fields and other code the user may create. This method requires database migrations and a greater understanding of Django models to implement.

By default the TOM Toolkit will use the tom_targets.BaseTarget model as the target model, but users can create their own target model by subclassing tom_targets.BaseTarget and adding their own fields. The TOM Toolkit will then use the custom target model if it is defined in the TARGET_MODEL_CLASS setting of settings.py. To implement this a user will first have to edit a models.py file in their custom code app and define a custom target model.

Subclassing tom_targets.BaseTarget will give the user access to all the fields and methods of the BaseTarget model, but the user can also add their own fields and methods to the custom target model. Fields from the BaseTarget model will be stored in a separate table from the custom fields, and rely on separate migrations. See the Django documentation on multi-table inheritance.

Preparing your project for custom Target Models

The first thing your project will need is a custom app. If you already have a custom app (usually called custom_code) you can skip this section. You can read about custom apps in the Django tutorial here, but to quickly get started, the command to create a new app is as follows:

./manage.py startapp custom_code

Where custom_code is the name of your app. You will also need to ensure that custom_code is in your settings.py. Append it to the end of INSTALLED_APPS:

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    ...
    'tom_dataproducts',
    'custom_code',
]
...

You should now have a directory within your TOM called custom_code, which looks like this:

├── custom_code
|   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── data
├── db.sqlite3
├── manage.py
├── mytom
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── static
├── templates
└── tmp

Editing models.py

First you will need to create a custom target model in the models.py file of your custom app. The following is an example of a custom target model that adds a boolean field and a number field:

models.py
 1from django.db import models
 2
 3from tom_targets.base_models import BaseTarget
 4
 5
 6class UserDefinedTarget(BaseTarget):
 7    example_bool = models.BooleanField(default=False, verbose_name='Example Boolean')
 8    example_number = models.FloatField(null=True, blank=True, help_text='Pick a number.')
 9
10    # Set Hidden Fields
11    example_bool.hidden = True
12
13    class Meta:
14        verbose_name = "target"
15        permissions = (
16            ('view_target', 'View Target'),
17            ('add_target', 'Add Target'),
18            ('change_target', 'Change Target'),
19            ('delete_target', 'Delete Target'),
20        )

The model name, UserDefinedTarget in the example (line 6), can be replaced by whatever CamelCase name you want, but it must be a subclass of tom_targets.BaseTarget. The null=True for example_number will allow for that field to be optional in the model form. Adding help_text="" to a field will provide text to be displayed under the form field when editing a target and in a tooltip when hovering over the field name on the target detail page. The permissions in the class Meta (lines 15-20) are required for the TOM Toolkit to work properly. The hidden attribute can be set to True to hide the field from the target detail page.

Reference the Django documentation on Model Fields in order to learn more about Django models and their associated fields. The model field reference documentation will help you figure out what field options make sense for your models fields. Also, take special note of the relationship between Django models and Django model forms. This relationship is important because the TOM Toolkit uses model forms to create and edit targets.

Editing settings.py

Next you will need to tell the TOM Toolkit to use your custom target model. In the settings.py file of your project, you will need to add the following line:

TARGET_MODEL_CLASS = 'custom_code.models.UserDefinedTarget'

Changing custom_code to the name of your custom app and UserDefinedTarget to the name of your custom target model.

Creating Migrations

After you have created your custom target model, you will need to create a migration for it. To do this, run the following command:

./manage.py makemigrations custom_code

This will create a migration file in the migrations directory of your custom app. You can then apply the migration by running:

./manage.py migrate

This will build the appropriate tables in your database for your custom target model.

Convert old targets to new model

If you have existing targets in your database, you will need to convert them to the new model. This can be done by running a version of the following code. We incorporate this into a management command to make it easier to run.

Create a new file in your custom app called management/commands/convert_targets.py and add the following code:

convert_targets.py
 1from django.core.management.base import BaseCommand
 2
 3from guardian.models import GroupObjectPermission, UserObjectPermission
 4
 5from tom_targets.base_models import BaseTarget
 6from tom_targets.models import Target
 7
 8
 9class Command(BaseCommand):
10    """
11    Core code based on information found at
12    https://code.djangoproject.com/ticket/7623
13    """
14
15    help = 'A helper command to convert existing BaseTargets to UserDefinedTargets.'
16
17    def handle(self, *args, **options):
18        # Make sure Target is a subclass of BaseTarget
19        if Target != BaseTarget and issubclass(Target, BaseTarget):
20            self.stdout.write(f'{Target} is a subclass of BaseTarget, updating existing Targets.')
21            base_targets = BaseTarget.objects.all()
22            targets = Target.objects.all()
23            for base_target in base_targets:
24                # If the base_target is not already in the new target model, update it
25                # Note: subclassed models share a PK with their parent
26                if not targets.filter(pk=base_target.pk).exists():
27                    self.stdout.write(f'Updating {base_target}...')
28                    target = Target(basetarget_ptr_id=base_target.pk)  # Create a new target with the base_target PK
29                    target.__dict__.update(base_target.__dict__)  # add base_target fields to target dictionary
30                    target.save()
31                    # re-add permissions for existing users and groups
32                    group_set = set(gop.group for gop in GroupObjectPermission.objects.filter(object_pk=target.pk))
33                    user_set = set(uop.user for uop in UserObjectPermission.objects.filter(object_pk=target.pk))
34                    for group in group_set:
35                        target.give_user_access(group)
36                    for user in user_set:
37                        target.give_user_access(user)
38            self.stdout.write(f'{Target.objects.count()} Targets updated.')
39
40        return

Once this file is created, you can run the following command to convert your old targets to the new model:

./manage.py convert_targets

Once this command is run, all of your old targets will be converted to the new model, but will not have the new fields filled in. You will need to fill in these fields manually, but once you do any non-hidden fields will be displayed on the target detail page.

Any fields added in this way are fully accessible in the TOM Toolkit as Target, and can be used in the same way as the built-in fields from any custom code you write, the API, or from the admin interface.

Transferring existing Extra Field Data to your Target Fields

If you have been using Extra Fields and have now created a custom target model, you may want to transfer the data from the Extra Fields to the new fields in your custom target model. This can be done by running a management command called converttargetextras. To use this command, be sure to have already created your custom target model. You can run the command without arguments for an interactive walkthrough.

./manage.py converttargetextras

Alternatively, you can run the command with the --target_extra and/or --model_field flags to specify one or more the of the Extra Field and Target Field names respectively.

./manage.py converttargetextras --target_extra extra_bool extra_number --model_field example_bool example_number

This command will go through each target and transfer the data from the Extra Field to the Target Field. If the Target Field is already populated with a value other than the default value, the data will not be transferred unless the --force flag is set. When finished, the Extra Field data will be deleted, and you will likely want to remove the EXTRA_FIELDS setting from your settings.py file.

Adding Extra Fields

If a user does not want to create a custom target model, they can use the EXTRA_FIELDS setting to add extra fields to the Target model. This method is simpler and does not require any database migrations, but is less flexible than creating a custom target model.

Note: There is a performance hit when using extra fields. Try to use the built in fields whenever possible.

Enabling extra fields

EXTRA_FIELDS can be implemented in two different ways. If a user wants to add a unique extra field to a individual target, that can be done by either adding a “Tag” to a target through the UI, or by adding a single extra field programmatically. If the user instead wants a common extra field to be available for all targets, they can follow these instructions:

To start, find the EXTRA_FIELDS definition in your settings.py:

# Define extra target fields here. Types can be any of "number", "string", "boolean" or "datetime"
# For example:
# EXTRA_FIELDS = [
#     {'name': 'redshift', 'type': 'number'},
#     {'name': 'discoverer', 'type': 'string'}
#     {'name': 'eligible', 'type': 'boolean'},
#     {'name': 'dicovery_date', 'type': 'datetime'}
# ]
EXTRA_FIELDS = []

We can define any number of extra fields in the array. Each item in the array is a dictionary with two values: name and type. Name is simply what you would like to name your field. Type is the datatype of the field and can be one of: number, string, boolean or datetime. These types allow the TOM Toolkit to properly store, filter and display these values elsewhere.

As an example, let’s change the setting to look like this:

EXTRA_FIELDS = [
    {'name': 'redshift', 'type': 'number'},
]

This will make an extra field with the name “redshift” and a type of “number” available to add to our targets.

Using extra fields

Now if you go to the target creation page, you should see the new field available:

image0

And if we go to our list of targets, we should see redshift as a field available to filter on:

image1

Extra fields with the number type allow filtering on range of values. The same goes for fields with the datetime type. string types to a case insensitive inclusive search, and boolean fields to a simple matching comparison.

Of course, redshift does appear on our target’s display page as well:

image2

To hide extra fields from the target page, we can set the “hidden” key (this doesn’t affect filtering and searching):

EXTRA_FIELDS = [
    {'name': 'redshift', 'type': 'number', 'hidden': True},
]

And we can set a default value for an extra field by including a default key/value pair:

EXTRA_FIELDS = [
    {'name': 'redshift', 'type': 'number', 'default': 0},
]

Displaying extra fields in templates

If we want to display the redshift in other places, we can use a template filter to do that. For example, we might want to display the redshift value in the target list table.

At the top of our template make sure to load targets_extras:

{% raw %}
 {% load targets_extras %}
{% endraw %}

Now we can use the target_extra_field filter wherever a target object is available in the template context:

{% raw %}
 {{ target|target_extra_field:"redshift" }}
{% endraw %}

The result is the redshift value being printed on the template:

image3

Working with extra fields programmatically

If you’d like to update or save extra fields to your targets in code, there are a few methods you can use. The simplest is to simply pass in a dictionary of extra data to your target’s save() method using the extras keyword argument:

target = Target.objects.get(name='example')
target.save(extras={'foo': 42})

The example target above will now have an extra field “foo” with the value 42.

For more precise control, you can access TargetExtra models directly. To remove an extra, for example:

target = Target.objects.get(name='example')
target_extra = target.targetextra_set.get(key='foo')
target_extra.delete()

The above deleted the target extra on a target with the key of “foo”.