Data Model#

../../../_images/arches-data-model-2022-04-18.png

Arches data model.#

../../../_images/full-data-model-2022-04-18.png

Full model of all apps.#

Resource Model Overview#

Resources in an Arches database are separated into distinct Resource Models designed to represent a kind of physical real-world resource, such as a historic artifact or event. In the technical sense, the term Resource Model refers collectively to the following user-facing elements in Arches:

  1. A Graph data structure representing a physical real-world resource, such as a building, a public figure, a website, an archaeological site, or a historic document.

  2. A set of Cards to collect and display data associated with instances of this Resource Model.

The relationships among these components and their dependencies are visualized below:

../../../_images/resource-model.png

The Arches logical model has been developed to support this modular construction, and the relevant models are described below as they pertain to the graph, UI components, and the resource data itself (not illustrated above).

Note

In the UI you will see a distinction between “Resource Models” and “Branches”, but underneath these are both made from instances of the Graph model. The primary difference between the two is the isresource property, which is set to True for a Resource Model.

Branches are used for records that might appear in multiple Resource Models, such as a person or place. Branches can be included as children of any Ontology-permitted Node in a Resource Model.

Controllers#

Arches platform code defines base classes for some of its core data models, and uses proxy models to implement their controllers. In smaller classes, “controller” code is included with the data model class. This documentation primarily discusses the models, but controller behavior is discussed where relevant to how the models are used, and all models are referred to by their more succinct “controller” name.

Model

Controller

ResourceInstance

Resource

CardModel

Card

TileModel

Tile

GraphModel

Graph

Note

ResourceInstance breaks the implicit naming convention above because the term “Resource Model” refers to a specific Arches construct, as explained in the Resource Model Overview above.

Graph Definition#

A Graph is a collection of NodeGroups, Nodes, and Edges which connect the Nodes.

Note

This definition does not include UI models and attributes, which are discussed below.

In the Arches data model, Nodes represent their graph data structure namesakes, sometimes called vertices. A Node does the work of defining the Graph data structure in conjunction with one or more Edges, and sometimes collecting data.

NodeGroups are an Arches feature used to represent a group of one or more Nodes that collect data. NodeGroups can be nested, creating a metadata structure which is used to display the graph in the UI and collect related information together.

A NodeGroup exists for every Node that collects data, and both contains and shares its UUID with that node (see naming conventions for references). NodeGroups with more than one member Node are used to collect composite or semantically-related information. For example, a NodeGroup for a Node named Name.E1 may contain a Name Type.E55 Node. This way, a Graph with this NodeGroup may store Names with multiple “types”, always collecting the information together.

NodeGroups are used to create Cards, and this is done based on the cardinality property. Therefore, not every NodeGroup will be used to create a Card, which allows NodeGroups to exist within other NodeGroups. The parentnodegroup property is used to record this nesting.

A user-defined Function may be registered and then associated with a Graph in order to extend the behavior of Arches. For more information, see here.

GraphModel#

class GraphModel(models.Model):
    graphid = models.UUIDField(primary_key=True, default=uuid.uuid1)
    name = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    deploymentfile = models.TextField(blank=True, null=True)
    author = models.TextField(blank=True, null=True)
    deploymentdate = models.DateTimeField(blank=True, null=True)
    version = models.TextField(blank=True, null=True)
    isresource = models.BooleanField()
    isactive = models.BooleanField()
    iconclass = models.TextField(blank=True, null=True)
    color = models.TextField(blank=True, null=True)
    subtitle = models.TextField(blank=True, null=True)
    ontology = models.ForeignKey('Ontology', db_column='ontologyid', related_name='graphs', null=True, blank=True)
    functions = models.ManyToManyField(to='Function', through='FunctionXGraph')
    jsonldcontext = models.TextField(blank=True, null=True)
    template = models.ForeignKey(
        'ReportTemplate',
        db_column='templateid',
        default='50000000-0000-0000-0000-000000000001'
    )
    config = JSONField(db_column='config', default={})

    @property
    def disable_instance_creation(self):
        if not self.isresource:
            return _('Only resource models may be edited - branches are not editable')
        if not self.isactive:
            return _('Set resource model status to Active in Graph Designer')
        return False

    def is_editable(self):
        result = True
        if self.isresource:
            resource_instances = ResourceInstance.objects.filter(graph_id=self.graphid).count()
            result = False if resource_instances > 0 else True
            if settings.OVERRIDE_RESOURCE_MODEL_LOCK == True:
                result = True
        return result

    class Meta:
        managed = True
        db_table = 'graphs'

Node#

class Node(models.Model):
    """
    Name is unique across all resources because it ties a node to values within tiles. Recommend prepending resource class to node name.

    """

    nodeid = models.UUIDField(primary_key=True, default=uuid.uuid1)
    name = models.TextField()
    description = models.TextField(blank=True, null=True)
    istopnode = models.BooleanField()
    ontologyclass = models.TextField(blank=True, null=True)
    datatype = models.TextField()
    nodegroup = models.ForeignKey(NodeGroup, db_column='nodegroupid', blank=True, null=True)
    graph = models.ForeignKey(GraphModel, db_column='graphid', blank=True, null=True)
    config = JSONField(blank=True, null=True, db_column='config')
    issearchable = models.BooleanField(default=True)
    isrequired = models.BooleanField(default=False)
    sortorder = models.IntegerField(blank=True, null=True, default=0)

    def get_child_nodes_and_edges(self):
        """
        gather up the child nodes and edges of this node

        returns a tuple of nodes and edges

        """
        nodes = []
        edges = []
        for edge in Edge.objects.filter(domainnode=self):
            nodes.append(edge.rangenode)
            edges.append(edge)

            child_nodes, child_edges = edge.rangenode.get_child_nodes_and_edges()
            nodes.extend(child_nodes)
            edges.extend(child_edges)
        return (nodes, edges)

    @property
    def is_collector(self):
        return str(self.nodeid) == str(self.nodegroup_id) and self.nodegroup is not None

    def get_relatable_resources(self):
        relatable_resource_ids = [
            r2r.resourceclassfrom for r2r in Resource2ResourceConstraint.objects.filter(resourceclassto_id=self.nodeid)]
        relatable_resource_ids = relatable_resource_ids + \
            [r2r.resourceclassto for r2r in Resource2ResourceConstraint.objects.filter(
                resourceclassfrom_id=self.nodeid)]
        return relatable_resource_ids

    def set_relatable_resources(self, new_ids):
        old_ids = [res.nodeid for res in self.get_relatable_resources()]
        for old_id in old_ids:
            if old_id not in new_ids:
                Resource2ResourceConstraint.objects.filter(Q(resourceclassto_id=self.nodeid) | Q(
                    resourceclassfrom_id=self.nodeid), Q(resourceclassto_id=old_id) | Q(resourceclassfrom_id=old_id)).delete()
        for new_id in new_ids:
            if new_id not in old_ids:
                new_r2r = Resource2ResourceConstraint.objects.create(
                    resourceclassfrom_id=self.nodeid, resourceclassto_id=new_id)
                new_r2r.save()

    class Meta:
        managed = True
        db_table = 'nodes'

NodeGroup#

class NodeGroup(models.Model):
    nodegroupid = models.UUIDField(primary_key=True, default=uuid.uuid1)
    legacygroupid = models.TextField(blank=True, null=True)
    cardinality = models.TextField(blank=True, default='1')
    parentnodegroup = models.ForeignKey('self', db_column='parentnodegroupid',
                                        blank=True, null=True)  # Allows nodegroups within nodegroups

    class Meta:
        managed = True
        db_table = 'node_groups'

        default_permissions = ()
        permissions = (
            ('read_nodegroup', 'Read'),
            ('write_nodegroup', 'Create/Update'),
            ('delete_nodegroup', 'Delete'),
            ('no_access_to_nodegroup', 'No Access'),
        )

Edge#

class Edge(models.Model):
    edgeid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    name = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    ontologyproperty = models.TextField(blank=True, null=True)
    domainnode = models.ForeignKey('Node', db_column='domainnodeid', related_name='edge_domains')
    rangenode = models.ForeignKey('Node', db_column='rangenodeid', related_name='edge_ranges')
    graph = models.ForeignKey('GraphModel', db_column='graphid', blank=True, null=True)

    class Meta:
        managed = True
        db_table = 'edges'
        unique_together = (('rangenode', 'domainnode'),)

Function#

class Function(models.Model):
    functionid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    name = models.TextField(blank=True, null=True)
    functiontype = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    defaultconfig = JSONField(blank=True, null=True)
    modulename = models.TextField(blank=True, null=True)
    classname = models.TextField(blank=True, null=True)
    component = models.TextField(blank=True, null=True, unique=True)

    class Meta:
        managed = True
        db_table = 'functions'

    @property
    def defaultconfig_json(self):
        json_string = json.dumps(self.defaultconfig)
        return json_string

    def get_class_module(self):
        mod_path = self.modulename.replace('.py', '')
        module = None
        import_success = False
        import_error = None
        for function_dir in settings.FUNCTION_LOCATIONS:
            try:
                module = importlib.import_module(function_dir + '.%s' % mod_path)
                import_success = True
            except ImportError as e:
                import_error = e
            if module != None:
                break
        if import_success == False:
            print('Failed to import ' + mod_path)
            print(import_error)

        func = getattr(module, self.classname)
        return func

Ontologies#

An ontology standardizes a set of valid CRM (Conceptual Reference Model) classes for Node instances, as well as a set of relationships that will define Edge instances. Most importantly, an ontology enforces which Edges can be used to connect which Nodes. If a pre-loaded ontology is designated for a Graph instance, every NodeGroup within that Graph must conform to that ontology. You may also create an “ontology-less” graph, which will not define specific CRM classes for the Nodes and Edges.

These rules are stored as OntologyClass instances, which are stored as JSON. These JSON objects consist of dictionaries with two properties, down and up, each of which contains another two properties ontology_property and ontology_classes (down assumes a known domain class, while up assumes a known range class).

{
  "down":[
    {
      "ontology_property":"P1_is_identified_by",
      "ontology_classes": [
        "E51_Contact_Point",
        "E75_Conceptual_Object_Appellation",
        "E42_Identifier",
        "E45_Address",
        "E41_Appellation"
      ]
    }
  ],
"up":[
  {
    "ontology_property":"P1_identifies",
    "ontology_classes":[
      "E51_Contact_Point",
      "E75_Conceptual_Object_Appellation",
      "E42_Identifier"
      ]
    }
  ]
}

Aches comes preloaded with the CIDOC CRM, an ontology created by ICOM (International Council of Museums) to model cultural heritage documentation. However, a developer may create and load an entirely new ontology.

Ontology#

class Ontology(models.Model):
    ontologyid = models.UUIDField(default=uuid.uuid1, primary_key=True)
    name = models.TextField()
    version = models.TextField()
    path = models.FileField(storage=get_ontology_storage_system())
    parentontology = models.ForeignKey('Ontology', db_column='parentontologyid',
                                       related_name='extensions', null=True, blank=True)

    class Meta:
        managed = True
        db_table = 'ontologies'

OntologyClass#

class OntologyClass(models.Model):
    """
    the target JSONField has this schema:

    values are dictionaries with 2 properties, 'down' and 'up' and within each of those another 2 properties,
    'ontology_property' and 'ontology_classes'

    "down" assumes a known domain class, while "up" assumes a known range class

    .. code-block:: python

        "down":[
            {
                "ontology_property": "P1_is_identified_by",
                "ontology_classes": [
                    "E51_Contact_Point",
                    "E75_Conceptual_Object_Appellation",
                    "E42_Identifier",
                    "E45_Address",
                    "E41_Appellation",
                    ....
                ]
            }
        ]
        "up":[
                "ontology_property": "P1i_identifies",
                "ontology_classes": [
                    "E51_Contact_Point",
                    "E75_Conceptual_Object_Appellation",
                    "E42_Identifier"
                    ....
                ]
            }
        ]

    """

    ontologyclassid = models.UUIDField(default=uuid.uuid1, primary_key=True)
    source = models.TextField()
    target = JSONField(null=True)
    ontology = models.ForeignKey('Ontology', db_column='ontologyid', related_name='ontologyclasses')

    class Meta:
        managed = True
        db_table = 'ontologyclasses'
        unique_together = (('source', 'ontology'),)

RDM Models#

The RDM (Reference Data Manager) stores all of the vocabularies used in your Arches installation. Whether they are simple wordlists or a polyhierarchical thesauri, these vocabularies are stored as “concept schemes” and can be viewed as an aggregation of one or more concepts and the semantic relationships (links) between those concepts.

In the data model, a concept scheme consists of a set of Concept instances, each paired with a Value. In our running name/name_type example, the Name Type.E55 Node would be linked to a Concept (Name Type.E55) which would have two child Concepts. Thus, where the user sees a dropdown containing “Primary” and “Alternate”, these are actually the Values of Name Type.E55’s two descendent Concepts. The parent/child relationships between Concepts are stored as Relation instances.

Concept#

class Concept(models.Model):
    conceptid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    nodetype = models.ForeignKey('DNodeType', db_column='nodetype')
    legacyoid = models.TextField(unique=True)

    class Meta:
        managed = True
        db_table = 'concepts'

Relation#

class Relation(models.Model):
    conceptfrom = models.ForeignKey(Concept, db_column='conceptidfrom', related_name='relation_concepts_from')
    conceptto = models.ForeignKey(Concept, db_column='conceptidto', related_name='relation_concepts_to')
    relationtype = models.ForeignKey(DRelationType, db_column='relationtype')
    relationid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.

    class Meta:
        managed = True
        db_table = 'relations'
        unique_together = (('conceptfrom', 'conceptto', 'relationtype'),)

Value#

class Value(models.Model):
    valueid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    concept = models.ForeignKey('Concept', db_column='conceptid')
    valuetype = models.ForeignKey(DValueType, db_column='valuetype')
    value = models.TextField()
    language = models.ForeignKey(DLanguage, db_column='languageid', blank=True, null=True)

    class Meta:
        managed = True
        db_table = 'values'

Resource Data#

Three models are used to store Arches business data:

  • ResourceInstance - one per resource in the database

  • Tile - stores all business data

  • ResourceXResource - records relationships between resource instances

Creating a new resource in the database instantiates a new ResourceInstance, which belongs to one resource model and has a unique resourceinstanceid. A resource instance may also have its own security/permissions properties in order to allow a fine-grained level of user-based permissions.

Once data have been captured, they are stored as Tiles in the database. Each Tile stores one instance of all of the attributes of a given NodeGroup for a resource instance, as referenced by the resourceinstanceid. This business data is stored as a JSON object, which is a dictionary with n number of keys/value pairs that represent a Node’s id nodeid and that Node’s value.

in theory:

{
     "nodeid": "node value",
     "nodeid": "node value"
}

in practice:

{
     "20000000-0000-0000-0000-000000000002": "John",
     "20000000-0000-0000-0000-000000000004": "Primary"
}

(In keeping with our running example, the keys in the second example would refer to an Name.E1 node and an Name Type.E55 node, respectively.)

Arches also allows for the creation of relationships between resource instances, and these are stored as instances of the ResourceXResource model. The resourceinstanceidfrom and resourceinstanceidto fields create the relationship, and relationshiptype qualifies the relationship. The latter must correspond to the appropriate top node in the RDM. This constrains the list of available types of relationships available between resource instances.

ResourceInstance#

class ResourceInstance(models.Model):
    resourceinstanceid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    graph = models.ForeignKey(GraphModel, db_column='graphid')
    legacyid = models.TextField(blank=True, unique=True, null=True)
    createdtime = models.DateTimeField(auto_now_add=True)

    class Meta:
        managed = True
        db_table = 'resource_instances'

TileModel#

class TileModel(models.Model):  # Tile
    """
    the data JSONField has this schema:

    values are dictionaries with n number of keys that represent nodeid's and values the value of that node instance

    .. code-block:: python

        {
            nodeid: node value,
            nodeid: node value,
            ...
        }

        {
            "20000000-0000-0000-0000-000000000002": "John",
            "20000000-0000-0000-0000-000000000003": "Smith",
            "20000000-0000-0000-0000-000000000004": "Primary"
        }

    the provisionaledits JSONField has this schema:

    values are dictionaries with n number of keys that represent nodeid's and values the value of that node instance

    .. code-block:: python

        {
            userid: {
                value: node value,
                status: "review", "approved", or "rejected"
                action: "create", "update", or "delete"
                reviewer: reviewer's user id,
                timestamp: time of last provisional change,
                reviewtimestamp: time of review
                }
            ...
        }

        {
            1: {
                "value": {
                        "20000000-0000-0000-0000-000000000002": "Jack",
                        "20000000-0000-0000-0000-000000000003": "Smith",
                        "20000000-0000-0000-0000-000000000004": "Primary"
                      },
               "status": "rejected",
                "action": "update",
                "reviewer": 8,
                "timestamp": "20180101T1500",
                "reviewtimestamp": "20180102T0800",
                },
            15: {
                "value": {
                        "20000000-0000-0000-0000-000000000002": "John",
                        "20000000-0000-0000-0000-000000000003": "Smith",
                        "20000000-0000-0000-0000-000000000004": "Secondary"
                      },
                "status": "review",
                "action": "update",
        }

    """

    tileid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    resourceinstance = models.ForeignKey(ResourceInstance, db_column='resourceinstanceid')
    parenttile = models.ForeignKey('self', db_column='parenttileid', blank=True, null=True)
    data = JSONField(blank=True, null=True, db_column='tiledata')  # This field type is a guess.
    nodegroup = models.ForeignKey(NodeGroup, db_column='nodegroupid')
    sortorder = models.IntegerField(blank=True, null=True, default=0)
    provisionaledits = JSONField(blank=True, null=True, db_column='provisionaledits')  # This field type is a guess.

    class Meta:
        managed = True
        db_table = 'tiles'

    def save(self, *args, **kwargs):
        if(self.sortorder is None or (self.provisionaledits is not None and self.data == {})):
            sortorder_max = TileModel.objects.filter(
                nodegroup_id=self.nodegroup_id, resourceinstance_id=self.resourceinstance_id).aggregate(Max('sortorder'))['sortorder__max']
            self.sortorder = sortorder_max + 1 if sortorder_max is not None else 0
        super(TileModel, self).save(*args, **kwargs)  # Call the "real" save() method.

ResourceXResource#

class ResourceXResource(models.Model):
    resourcexid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    resourceinstanceidfrom = models.ForeignKey(
        'ResourceInstance', db_column='resourceinstanceidfrom', blank=True, null=True, related_name='resxres_resource_instance_ids_from')
    resourceinstanceidto = models.ForeignKey(
        'ResourceInstance', db_column='resourceinstanceidto', blank=True, null=True, related_name='resxres_resource_instance_ids_to')
    notes = models.TextField(blank=True, null=True)
    relationshiptype = models.TextField(blank=True, null=True)
    datestarted = models.DateField(blank=True, null=True)
    dateended = models.DateField(blank=True, null=True)
    created = models.DateTimeField()
    modified = models.DateTimeField()

    def delete(self):
        from arches.app.search.search_engine_factory import SearchEngineFactory
        se = SearchEngineFactory().create()
        se.delete(index='resource_relations', doc_type='all', id=self.resourcexid)
        super(ResourceXResource, self).delete()

    def save(self):
        from arches.app.search.search_engine_factory import SearchEngineFactory
        se = SearchEngineFactory().create()
        if not self.created:
            self.created = datetime.datetime.now()
        self.modified = datetime.datetime.now()
        document = model_to_dict(self)
        se.index_data(index='resource_relations', doc_type='all', body=document, idfield='resourcexid')
        super(ResourceXResource, self).save()

    class Meta:
        managed = True
        db_table = 'resource_x_resource'

Edit Log#

A change in a Tile’s contents, which is the result of any resource edits, is recorded as an instance of the EditLog model.

class EditLog(models.Model):
    editlogid = models.UUIDField(primary_key=True, default=uuid.uuid1)
    resourcedisplayname = models.TextField(blank=True, null=True)
    resourceclassid = models.TextField(blank=True, null=True)
    resourceinstanceid = models.TextField(blank=True, null=True)
    nodegroupid = models.TextField(blank=True, null=True)
    tileinstanceid = models.TextField(blank=True, null=True)
    edittype = models.TextField(blank=True, null=True)
    newvalue = JSONField(blank=True, null=True, db_column='newvalue')
    oldvalue = JSONField(blank=True, null=True, db_column='oldvalue')
    newprovisionalvalue = JSONField(blank=True, null=True, db_column='newprovisionalvalue')
    oldprovisionalvalue = JSONField(blank=True, null=True, db_column='oldprovisionalvalue')
    timestamp = models.DateTimeField(blank=True, null=True)
    userid = models.TextField(blank=True, null=True)
    user_firstname = models.TextField(blank=True, null=True)
    user_lastname = models.TextField(blank=True, null=True)
    user_email = models.TextField(blank=True, null=True)
    user_username = models.TextField(blank=True, null=True)
    provisional_userid = models.TextField(blank=True, null=True)
    provisional_user_username = models.TextField(blank=True, null=True)
    provisional_edittype = models.TextField(blank=True, null=True)
    note = models.TextField(blank=True, null=True)

    class Meta:
        managed = True
        db_table = 'edit_log'

UI Component Models#

A number of models exist specifically to support the resource model UI. The purpose of this is to create direct relationships between the resource graph and the data entry cards that are used to create resource instances. Generally, the process works like this:

  1. A resource graph is an organized collection of NodeGroups which define what information will be gathered for a given resource model.

  2. A resource’s Cards and are tied to specific NodeGroups and define which input Widgets will be used to gather values for each Node in that NodeGroup. Card Components are used to render the cards in various contexts in the Arches UI.

../../../_images/graph-cards.png

Cards are UI representations of a NodeGroup, and they encapsulate the Widgets that facilitate data entry for each Node in a given NodeGroup instance.

While a Card will only handle data entry for a single NodeGroup (which may have many Nodes or NodeGroups), a single NodeGroup can be handled by more than one Card.

Throughout the Arches UI, Card Components are used to render Cards in both read-only and data entry contexts.

Note

Beginning in Arches 4.3, Card Components provide functionality formerly provided by Forms, Menus, and Reports.

CardModel#

class CardModel(models.Model):
    cardid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    name = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    instructions = models.TextField(blank=True, null=True)
    cssclass = models.TextField(blank=True, null=True)
    helpenabled = models.BooleanField(default=False)
    helptitle = models.TextField(blank=True, null=True)
    helptext = models.TextField(blank=True, null=True)
    nodegroup = models.ForeignKey('NodeGroup', db_column='nodegroupid')
    graph = models.ForeignKey('GraphModel', db_column='graphid')
    active = models.BooleanField(default=True)
    visible = models.BooleanField(default=True)
    sortorder = models.IntegerField(blank=True, null=True, default=None)
    component = models.ForeignKey('CardComponent', db_column='componentid', default=uuid.UUID(
        'f05e4d3a-53c1-11e8-b0ea-784f435179ea'), on_delete=models.SET_DEFAULT)
    config = JSONField(blank=True, null=True, db_column='config')

    def is_editable(self):
        result = True
        tiles = TileModel.objects.filter(nodegroup=self.nodegroup).count()
        result = False if tiles > 0 else True
        if settings.OVERRIDE_RESOURCE_MODEL_LOCK == True:
            result = True
        return result

    class Meta:
        managed = True
        db_table = 'cards'

Card Component#

A Card Component renders a Card.

class CardComponent(models.Model):
    componentid = models.UUIDField(primary_key=True, default=uuid.uuid1)
    name = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    component = models.TextField()
    componentname = models.TextField()
    defaultconfig = JSONField(blank=True, null=True, db_column='defaultconfig')

    @property
    def defaultconfig_json(self):
        json_string = json.dumps(self.defaultconfig)
        return json_string

    class Meta:
        managed = True
        db_table = 'card_components'

Field description:

name:

a name to be displayed in the UI for this component

description:

a description to be displayed in the UI for this component

component:

a require path for the JS module representing this component

componentname:

a Knockout.js component name used by this component (for rendering via knockout’s component binding handler)

defaultconfig:

a default JSON configuration object to be used by cards that implement this component

Widget#

class Widget(models.Model):
    widgetid = models.UUIDField(primary_key=True, default=uuid.uuid1)  # This field type is a guess.
    name = models.TextField(unique=True)
    component = models.TextField(unique=True)
    defaultconfig = JSONField(blank=True, null=True, db_column='defaultconfig')
    helptext = models.TextField(blank=True, null=True)
    datatype = models.TextField()

    @property
    def defaultconfig_json(self):
        json_string = json.dumps(self.defaultconfig)
        return json_string

    class Meta:
        managed = True
        db_table = 'widgets'

DDataType#

Used to validate data entered into widgets

class DDataType(models.Model):
    datatype = models.TextField(primary_key=True)
    iconclass = models.TextField()
    modulename = models.TextField(blank=True, null=True)
    classname = models.TextField(blank=True, null=True)
    defaultwidget = models.ForeignKey(db_column='defaultwidget', to='models.Widget', null=True)
    defaultconfig = JSONField(blank=True, null=True, db_column='defaultconfig')
    configcomponent = models.TextField(blank=True, null=True)
    configname = models.TextField(blank=True, null=True)
    issearchable = models.NullBooleanField(default=False)
    isgeometric = models.BooleanField()

    class Meta:
        managed = True
        db_table = 'd_data_types'

Naming Conventions#

id vs _id: ID as Primary Key vs Foreign Key#

Throughout the code, you will sometimes see an entity name with “id” appended and other times see the same name with “_id” appended. For example, you’ll see both nodegroupid and nodegroup_id.

What is the difference?

The first, nodegroupid, is a UUID attribute in the database and is the primary key for entities of type NodeGroup.

The second, nodegroup_id, is a foreign key attribute (thus also a UUID) that refers from somewhere else to a NodeGroup. For example, a Tile object may have an associated NodeGroup; that NodeGroup object itself would be referenced as tile.nodegroup, and the NodeGroup’s UUID – which in the context of a Tile object is a foreign key – would therefore be tile.nodegroup_id.

The reason to use tile.nodegroup_id, instead of getting the NodeGroup’s ID by going through the associated NodeGroup object with tile.nodegroup.nodegroupid, is that the latter would involve an extra database query to fetch the NodeGroup instance, which would be a waste if you don’t actually need the NodeGroup itself. When all you need is the NodeGroup’s UUID – perhaps because you’re just going to pass it along to something else that only needs the UUID – then there’s no point fetching the entire NodeGroup when you already have the Tile in hand and the Tile’s nodegroup_id field is a foreign key to the Tile’s associated NodeGroup. You might as well just get that foreign key, tile.nodegroup_id, directly.