Skip to content

API reference: Entity#

Entity and related models

The entity concept might feel a bit abstract, so it might be useful to reason about them using a concrete example (beneficiaries):

  • Entities are used to track beneficiaries (=people who will benefit from the help an organization provides). Those beneficiaries can be of different types (E.g.: Children under 5, Pregnant or lactating women, etc.).
  • Those beneficiaries are visited multiple times, so multiple submissions/instances (that we call "records") are attached to them via the entity_id foreign key of Instance.
  • In addition to those records, we also want to track some core metadata about the beneficiary, such as their name, age,... Because entities can be of very different natures, we avoid hardcoding those fields in the Entity model, and also reuse the form mechanism: each EntityType has a foreign key to a reference form, and each entity has a foreign key (attributes) to an instance/submission of that form.

Entity #

Bases: SoftDeletableModel

An entity represents a physical object or person with a known Entity Type

Contrary to forms, they are not linked to a specific OrgUnit. The core attributes that define this entity are not stored as fields in the Entity model, but in an Instance / submission

Source code in iaso/models/entity.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
class Entity(SoftDeletableModel):
    """An entity represents a physical object or person with a known Entity Type

    Contrary to forms, they are not linked to a specific OrgUnit.
    The core attributes that define this entity are not stored as fields in the Entity model, but in an Instance /
    submission
    """

    name = models.CharField(max_length=255, blank=True)  # this field is not used, name value is taken from attributes
    uuid = models.UUIDField(default=uuid.uuid4, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    entity_type = models.ForeignKey(EntityType, blank=True, on_delete=models.PROTECT)
    attributes = models.OneToOneField(
        Instance, on_delete=models.PROTECT, help_text="instance", related_name="attributes", blank=True, null=True
    )
    account = models.ForeignKey(Account, on_delete=models.PROTECT)
    merged_to = models.ForeignKey("self", null=True, blank=True, on_delete=models.PROTECT)

    objects = DefaultSoftDeletableManager.from_queryset(EntityQuerySet)()

    objects_only_deleted = OnlyDeletedSoftDeletableManager.from_queryset(EntityQuerySet)()

    objects_include_deleted = IncludeDeletedSoftDeletableManager.from_queryset(EntityQuerySet)()

    class Meta:
        verbose_name_plural = "Entities"

    def __str__(self):
        return "%s %s %s %d" % (self.entity_type.name, self.uuid, self.name, self.id)

    def get_nfc_cards(self):
        from iaso.models.storage import StorageDevice

        nfc_count = StorageDevice.objects.filter(entity=self, type=StorageDevice.NFC).count()
        return nfc_count

    def as_small_dict(self):
        return {
            "id": self.pk,
            "uuid": self.uuid,
            "name": self.name,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "entity_type": self.entity_type_id,
            "entity_type_name": self.entity_type and self.entity_type.name,
            "attributes": self.attributes and self.attributes.as_dict(),
        }

    def as_small_dict_with_nfc_cards(self, instance):
        entity_dict = self.as_small_dict()
        entity_dict["nfc_cards"] = self.get_nfc_cards()
        return entity_dict

    def as_dict(self):
        instances = dict()

        for i in self.instances.all():
            instances["uuid"] = i.uuid
            instances["file_name"]: i.file_name
            instances[str(i.name)] = i.name

        return {
            "id": self.pk,
            "uuid": self.uuid,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "entity_type": self.entity_type.as_dict(),
            "attributes": self.attributes.as_dict(),
            "instances": instances,
            "account": self.account.as_dict(),
        }

    def soft_delete_with_instances_and_pending_duplicates(self, audit_source, user):
        """
        This method does a proper soft-deletion of the entity:
        - soft delete the entity
        - soft delete its attached form instances
        - delete relevant pending EntityDuplicate pairs
        """
        from hat.audit.models import log_modification
        from iaso.models.deduplication import ValidationStatus

        original = copy(self)
        self.delete()  # soft delete
        log_modification(original, self, audit_source, user=user)

        for instance in set(filter(None, [self.attributes] + list(self.instances.all()))):
            original = copy(instance)
            instance.soft_delete()
            log_modification(original, instance, audit_source, user=user)

        self.duplicates1.filter(validation_status=ValidationStatus.PENDING).delete()
        self.duplicates2.filter(validation_status=ValidationStatus.PENDING).delete()

        return self

soft_delete_with_instances_and_pending_duplicates(audit_source, user) #

This method does a proper soft-deletion of the entity: - soft delete the entity - soft delete its attached form instances - delete relevant pending EntityDuplicate pairs

Source code in iaso/models/entity.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def soft_delete_with_instances_and_pending_duplicates(self, audit_source, user):
    """
    This method does a proper soft-deletion of the entity:
    - soft delete the entity
    - soft delete its attached form instances
    - delete relevant pending EntityDuplicate pairs
    """
    from hat.audit.models import log_modification
    from iaso.models.deduplication import ValidationStatus

    original = copy(self)
    self.delete()  # soft delete
    log_modification(original, self, audit_source, user=user)

    for instance in set(filter(None, [self.attributes] + list(self.instances.all()))):
        original = copy(instance)
        instance.soft_delete()
        log_modification(original, instance, audit_source, user=user)

    self.duplicates1.filter(validation_status=ValidationStatus.PENDING).delete()
    self.duplicates2.filter(validation_status=ValidationStatus.PENDING).delete()

    return self

EntityType #

Bases: models.Model

Its reference_form describes the core attributes/metadata about the entity type (in case it refers to a person: name, age, ...)

Source code in iaso/models/entity.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class EntityType(models.Model):
    """Its `reference_form` describes the core attributes/metadata about the entity type (in case it refers to a person: name, age, ...)"""

    name = models.CharField(max_length=255)  # Example: "Child under 5"
    code = models.CharField(
        max_length=255, null=True, blank=True
    )  # As the name could change over the time, this field will never change once the entity type created and ETL script will rely on that
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    # Link to the reference form that contains the core attribute/metadata specific to this entity type
    reference_form = models.ForeignKey(Form, blank=True, null=True, on_delete=models.PROTECT)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, blank=True, null=True)
    is_active = models.BooleanField(default=False)
    # Fields (subset of the fields from the reference form) that will be shown in the UI - entity list view
    fields_list_view = ArrayField(
        models.CharField(max_length=255, blank=True, db_collation="case_insensitive"), size=100, null=True, blank=True
    )
    # Fields (subset of the fields from the reference form) that will be shown in the UI - entity detail view
    fields_detail_info_view = ArrayField(
        models.CharField(max_length=255, blank=True, db_collation="case_insensitive"), size=100, null=True, blank=True
    )
    # Fields (subset of the fields from the reference form) that will be used to search for duplicate entities
    fields_duplicate_search = ArrayField(
        models.CharField(max_length=255, blank=True, db_collation="case_insensitive"), size=100, null=True, blank=True
    )
    prevent_add_if_duplicate_found = models.BooleanField(
        default=False,
    )

    class Meta:
        unique_together = ["name", "account"]

    def __str__(self):
        return f"{self.name}"

    def as_dict(self):
        return {
            "name": self.name,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "reference_form": self.reference_form.as_dict(show_version=False) if self.reference_form else None,
            "account": self.account.as_dict(),
        }

    def get_list_view_fields(self) -> list:
        """
        Fetch the fields listed in `fields_list_view` from the reference form.

        Return an array of field descriptions (see `Form.possile_fields`):
        ```
        [
            {
                "name": "last_name",
                "type": "text",
                "label": "Nom de famille"
            },
            ...
        ]
        ```
        """

        if not self.reference_form or not self.reference_form.possible_fields:
            return []

        selected_fields = set(self.fields_list_view or [])
        if not selected_fields:
            return []

        fields = {}  # Used for deduplication by field name

        for field_data in self.reference_form.possible_fields:
            name = field_data.get("name")
            if name in selected_fields:
                fields[name] = field_data

        return list(fields.values())

get_list_view_fields() #

Fetch the fields listed in fields_list_view from the reference form.

Return an array of field descriptions (see Form.possile_fields):

[
    {
        "name": "last_name",
        "type": "text",
        "label": "Nom de famille"
    },
    ...
]
Source code in iaso/models/entity.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def get_list_view_fields(self) -> list:
    """
    Fetch the fields listed in `fields_list_view` from the reference form.

    Return an array of field descriptions (see `Form.possile_fields`):
    ```
    [
        {
            "name": "last_name",
            "type": "text",
            "label": "Nom de famille"
        },
        ...
    ]
    ```
    """

    if not self.reference_form or not self.reference_form.possible_fields:
        return []

    selected_fields = set(self.fields_list_view or [])
    if not selected_fields:
        return []

    fields = {}  # Used for deduplication by field name

    for field_data in self.reference_form.possible_fields:
        name = field_data.get("name")
        if name in selected_fields:
            fields[name] = field_data

    return list(fields.values())