Skip to main content

Share data between entities using the API

tip

If the selection of an entity needs to be done by the user, you can make use of the entity selection fields

Sometimes information is needed from another entity. For example, when you want to show all the child locations on a MapView. Or when you require general settings, which are specified on the parent entity. This guide explains how to achieve this using the API (stands for "application programming interface").

caution

Please keep in mind that API requests are relatively slow, so make sure you only execute them when necessary.

The API class can be instantiated using VIKTOR's api_v1 module:

from viktor.api_v1 import API

api = API()

There are three starting points to obtain an entity/entities:

  • Start from the root entities: api.get_root_entities()
  • Get all entities of a given type: api.get_entities_by_type('MyType')
  • Start with a given entity id: api.get_entity(entity_id)

Next to 'getting' the entity by its id, it is also possible to navigate to relatives (get_entity_children, get_entity_siblings, get_entity_parent), modify the corresponding entity (rename_entity, delete_entity, set_entity_params), get its history (get_entity_revisions), get the corresponding file (get_entity_file), or create a child entity (create_child_entity), directly by using the API methods.

For convenience, the Entity object itself also provides (most of) above methods. So it is also possible to navigate by:

entity.parent()
entity.children()
entity.siblings()

It's possible to chain these methods, e.g.:

grand_parent = entity.parent().parent()

And filter on entity type:

gef_children = entity.children(entity_type_names=['GEF'])

In case only the id, name or type of an Entity is required, the performance of the API call(s) can be enhanced by setting the include_params flag to False:

gef_children = entity.children(entity_type_names=['GEF'], include_params=False)

The params of an Entity can simply be obtained by calling last_saved_params. In a similar way, the summary can be obtained by calling last_saved_summary:

params = entity.last_saved_params
summary = entity.last_saved_summary

Current entity and relative entities

The entity_id and entity_name of the current entity are sent along within all view-methods and callback-functions. The id can be used to construct a current_entity object, from which you can navigate to relatives.

For example within a view-method:

from viktor.api_v1 import API

@DataView("Data", duration_guess=1)
def calculate_view(self, params, entity_id, entity_name, **kwargs):
current_entity = API().get_entity(entity_id)
parent = current_entity.parent()
parent_params = parent.last_saved_params
# do something with parent_params
return DataResult(...)

Or a callback-function in the parametrization:

from viktor.api_v1 import API
from viktor.parametrization import ViktorParametrization, OptionField


def get_options(params, entity_id, entity_name, **kwargs):
current_entity = API().get_entity(entity_id)
parent = current_entity.parent()
parent_params = parent.last_saved_params
# do something with parent_params
return [...]


class Parametrization(ViktorParametrization):
field = OptionField(..., options=get_options)

Within a single job (see VIKTOR's call flow for the possible triggers of a job) the results of API calls are temporarily stored (memoized). This means that multiple calls to the same entity only require a single request in the background. This is particularly handy for option functions:

from viktor.api_v1 import API

def get_options1_from_parent(params, entity_id, **kwargs):
parent = API().get_entity(entity_id).parent()
parent_params = parent.last_saved_params
# get options1 from parent_params
...

def get_options2_from_parent(params, entity_id, **kwargs):
parent = API().get_entity(entity_id).parent()
parent_params = parent.last_saved_params
# get options2 from parent_params
...

class Parametrization(ViktorParametrization):
option1 = OptionField('Option 1', options=get_options1_from_parent)
option2 = OptionField('Option 2', options=get_options2_from_parent)

EntityList / EntityRevisionList

The methods that return a series of entities do so by returning an EntityList, instead of a list of Entity instances, for performance reasons. This has little effect on handling, as most normal list operations are still allowed:

children = entity.children()  # children is of type `EntityList`

len(children) # ok
children[0] # positive indexing: ok
children[-1] # negative indexing: ok
for child in children: # iterating: ok
# do something with child

Similarly, methods that return a series of entity-revisions do so by returning an EntityRevisionList.

note

Slicing the EntityList or EntityRevisionList is not supported (e.g. children[0:5]).

Bypassing user access restrictions

caution

If not used properly, (confidential) information which SHOULD NOT be accessible to a user may leak (e.g. by including confidential data in a view, the data is visible to users with access to that view). Please consider the following when using this flag:

  • Make sure the app's admin is aware that the code circumvents certain permissions at specific places.
  • Make sure to test the implementation thoroughly, to ensure no confidential data is leaked.

Some applications have user restrictions that limit access to certain entities or entity types. The code of an application inherits the permissions of the user currently running it. This means that, for example, if a user does not have permission to read some entity data, the code will also NOT be able to reach this data through an API call when run by the same user.

In some cases it is desirable to restrict user access to a certain entity but have access within the code nonetheless. An example is the case of a global settings entity that consists of confidential pricing data, which should be inaccessible for users but accessible for the code to calculate costs. For these cases a privileged flag can be used:

  1. Configure viktor.config.toml to make use of the privileged api. This serves as an additional layer of security/awareness.

    enable_privileged_api = true
  2. Use the privileged flag on the relevant API methods.

    from viktor.api_v1 import API
    ...
    settings_entity = API().get_root_entities(entity_type_names=['Settings'], privileged=True)[0]
    settings = settings_entity.last_saved_params

Testing

API calls need to be mocked within the context of (automated) testing. There are two approaches you could take to achieve this:

  1. By making use of the mock_API decorator
  2. By isolating API actions in separate methods

Testing by using mock_API

New in v13.3.0
import unittest

from viktor.testing import mock_API

from app.my_entity_type.controller import MyEntityTypeController

class TestMyEntityTypeController(unittest.TestCase):
@mock_API()
def test_function(self):
MyEntityTypeController().function(...)

For each method that is called on the API class, the decorator will return mock objects:

  • MockedEntity representing Entity (e.g. when calling API.get_entity())
  • MockedEntityList representing EntityList (e.g. when calling API.get_root_entities())
  • MockedEntityRevision representing EntityRevision (e.g. when calling API.get_entity_revisions())
  • MockedEntityType representing EntityType (e.g. when calling Entity.entity_type)
  • MockedUser representing User (e.g. when calling API.get_current_user())

Each of these mock objects supports the properties and methods of their represented objects, for example, MockedEntity.last_saved_params mimics Entity.last_saved_params.

Assume the function() of the example above performs an API call to retrieve the child entities to count the number of red, green, and blue entities:

...

class MyEntityTypeController(ViktorController):
...

def function(self, entity_id):
red, green, blue = 0, 0, 0
children = API().get_entity_children(entity_id)
for child in children:
if child.last_saved_params.color == 'red':
red += 1
if child.last_saved_params.color == 'green':
green += 1
if child.last_saved_params.color == 'blue':
blue += 1
return red, green, blue

By default, the mock_API decorator returns a zero-length MockedEntityList when get_entity_children() is called. This means that, in the example above, function will return (0, 0, 0).

However, the decorator allows you to specify the outcome of each individual method such that we can actually test what would happen if a VIKTOR workspace consists of specific entities. For example, let's pass the entities that get_entity_children() in the example above should return, by providing a sequence of MockedEntity objects in the decorator:

import unittest

from viktor.testing import mock_API, MockedEntity

from app.my_entity_type.controller import MyEntityTypeController

CHILD_ENTITIES = [
MockedEntity(params={'color': 'red'}),
MockedEntity(params={'color': 'green'}),
MockedEntity(params={'color': 'green'}),
MockedEntity(params={'color': 'blue'}),
]

class TestMyEntityTypeController(unittest.TestCase):

@mock_API(get_entity_children=CHILD_ENTITIES)
def test_function(self):
red, green, blue = MyEntityTypeController().function(...)
self.assertEqual(red, 1)
self.assertEqual(green, 2)
self.assertEqual(blue, 1)

Instead of the default zero-length MockedEntityList, the decorator is now instructed to return the provided entities. Note that in this example the params are passed to MockedEntity, but you can define much more (e.g. parent / children / siblings etc.).

Testing by isolating API actions

Structuring the controller in such a way that each API action is isolated in a separate method makes it easier to maintain, debug, and reuse your code. Assume a controller that looks like this:

...

class MyEntityTypeController(ViktorController):
...

def get_child_params(self, entity_id):
# perform API actions
return child_params

def get_parent_params(self, entity_id):
# perform API actions
return parent_params

@GeometryView("3D model", duration_guess=3, x_axis_to_right=True)
def visualize(self, params, entity_id, **kwargs):
parent_params = self.get_parent_params(entity_id)
child_params = self.get_child_params(entity_id)
visualization = self.create_visualisation(parent_params, child_params)
return GeometryResult(visualization)

def download_file(self, params, entity_id, **kwargs):
parent_params = self.get_parent_params(entity_id)
download_content = self.generate_download_result(parent_params)
return DownloadResult(download_content)

If the visualize and download_file methods are now to be tested, we can make use of unittests mock.patch.object decorator in combination with mock.MagicMock to control the return value:

import unittest

from unittest import mock

from app.my_entity_type.controller import MyEntityTypeController

# params dictionaries
ENTITY_PARAMS = ...
PARENT_PARAMS = ...
CHILD_PARAMS = ...

class TestMyEntityTypeController(unittest.TestCase):

@mock.patch.object(MyEntityTypeController, 'get_child_params', mock.MagicMock(return_value=CHILD_PARAMS))
@mock.patch.object(MyEntityTypeController, 'get_parent_params', mock.MagicMock(return_value=PARENT_PARAMS))
def test_visualise(self):
MyEntityTypeController().visualize(params=ENTITY_PARAMS)

@mock.patch.object(MyEntityTypeController, 'get_child_params', mock.MagicMock(return_value=CHILD_PARAMS))
def test_generate_download_content(self):
MyEntityTypeController().generate_download_content(params=ENTITY_PARAMS)

Mocking the deserialized params

The VIKTOR platform deserializes the raw params, such that they will enter the app code in their intuitive format. For example, an EntityOptionField will return an Entity object in the params instead of an entity_id. However, when you are using raw params in your tests (e.g. a JSON file), you will need to deserialize the params yourself. This is necessary when any of the following fields is used in the parametrization of the corresponding entity type:

  • DateField
  • EntityOptionField
  • ChildEntityOptionField
  • SiblingEntityOptionField
  • EntityMultiSelectField
  • ChildEntityMultiSelectField
  • SiblingEntityMultiSelectField
  • GeoPointField
  • GeoPolylineField
  • GeoPolygonField
  • FileField
  • MultiFileField

For example, when the parametrization consists of a NumberField, ChildEntityOptionField, and a FileField, the raw params could look like:

params = {
'number': 1,
'entity': 2, # corresponds to entity id
'file': 3, # corresponds to file resource id
}

Let's assume function() to perform an API call to

  • retrieve the last_saved_params of the selected child entity
  • retrieve the content of the selected file
...

class MyEntityTypeController(ViktorController):
...

def function(self, params):
child_params = params.entity.last_saved_params
file = params.file.file
...

The raw integers of params_dict should be converted to their corresponding mock objects (i.e. deserialized), in order for function() to succeed. There are two ways to achieve this:

  1. By defining a dictionary consisting of the mock objects manually

    from viktor import File
    from viktor.testing import MockedEntity, MockedFileResource

    params = {
    'number': 1,
    'entity': MockedEntity(name="My Entity"),
    'file': MockedFileResource(file=File.from_data("content"), filename="file.txt"),
    }
  2. By using the mock_params function, providing a JSON file consisting of the raw params along with the parametrization and (optionally) mocked resources

    from viktor import File
    from viktor.testing import MockedEntity, MockedFileResource, mock_params

    params = mock_params(
    params=File.from_path("path to JSON file"),
    parametrization=MyEntityTypeController.parametrization,
    entities={2: MockedEntity(name="My Entity")},
    file_resources={3: MockedFileResource(file=File.from_data("content"), filename="file.txt")},
    )

The function() can then be tested by providing the deserialized params:

...

class TestMyEntityTypeController(unittest.TestCase):

def test_function(self):
MyEntityTypeController().function(params=params)