Share data between entities using the API
If the selection of an entity needs to be done by the user, you can make use of the entity selection fields
The following methods were added to the API
class:
create_child_entity
(not of file-type)delete_entity
get_entities_by_type
get_entity_children
get_entity_parent
get_entity_revisions
get_entity_siblings
rename_entity
set_entity_params
The following methods were added to the Entity
class:
create_child
(not of file-type)delete
revisions
rename
set_params
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").
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
entity_name
has been added in the signature of controller methods and callback functions
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 (>= v12.1.0)
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
.
Slicing the EntityList
or EntityRevisionList
is not supported (e.g. children[0:5]
).
Bypassing user access restrictions
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:
Configure
viktor.config.toml
to make use of the privileged api. This serves as an additional layer of security/awareness.enable_privileged_api = true
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:
- By making use of the
mock_API
decorator - By isolating API actions in separate methods
Testing by using mock_API
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
representingEntity
(e.g. when callingAPI.get_entity()
)MockedEntityList
representingEntityList
(e.g. when callingAPI.get_root_entities()
)MockedEntityRevision
representingEntityRevision
(e.g. when callingAPI.get_entity_revisions()
)MockedEntityType
representingEntityType
(e.g. when callingEntity.entity_type
)MockedUser
representingUser
(e.g. when callingAPI.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)
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:
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"),
}By using the
mock_params
function, providing a JSON file consisting of the raw params along with the parametrization and (optionally) mocked resourcesfrom 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)