Models

Related Issues

Document Status

#17

In Progress

#21

In Progress

A prototypical implementation of the django based model system is available on https://gitlab.hzdr.de/model-data-explorer/django-psql-dag-test.

As you see from the Graph, the exact implementation is quite complicated due to the many relations between the different objects. But we’ll break it down in the following paragraphs.

DataGroup Relations

To efficiently mimic the relations between multiple DataGroups, we make use of so-called directed acyclic graphs implemented by the django-postgresql-dag library.

There are four main classes that we need to represant the relations between data groups:

DataGroup

The data group as a central object.

DataGroupNode

Each DataGroup holds a reference to multiple nodes, and each node represents one specific graph type that represents the permission system. There is a parent graph, a member graph and a list graph. See DataGroup Permission Graphs for more explanation.

DataGroupUserGroup

Each DataGroup holds reference to multiple user groups, one group per role that a user can have in terms of a data group (see Users and data groups). This is mainly relevant for the parent and members graph, see below.

DataGroupRelation

A relation between two data groups. Each DataGroupRelation is for one specific graph system (parent, member or list, see below). If an owner of the parent or child group requests a relation, such a DataGroupRelation object is created. The owners of the other groups are then asked for approval. As soon as they approve, the connections between the two DataGroupNodes in the corresponding graph are made.

digraph model_graph {
  // Dotfile by Django-Extensions graph_models
  // Created: 2023-02-14 14:40
  // Cli Options: --output /home/docs/checkouts/readthedocs.org/user_builds/mde-prototype/checkouts/develop/source/tmp_graph.dot templateapp --exclude-models Group,Node,Edge,DataGroupEdge,User

  fontname = "Roboto"
  fontsize = 8
  splines  = true
  rankdir = "TB"

  node [
    fontname = "Roboto"
    fontsize = 8
    shape = "plaintext"
  ]

  edge [
    fontname = "Roboto"
    fontsize = 8
  ]

  // Labels


  templateapp_models_DataGroupNode [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DataGroupNode<BR/>&lt;<FONT FACE="Roboto"><I>Node</I></FONT>&gt;
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>datagroup</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">graph_type</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_DataGroupUserGroup [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DataGroupUserGroup
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>group_ptr</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>OneToOneField (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>datagroup</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">role</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_DataGroupRelation [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DataGroupRelation
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><B>child_approved_by</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>child_group</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><B>parent_approved_by</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>parent_group</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">child_approved</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">BooleanField</FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">graph_type</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">parent_approved</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">BooleanField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_DataGroup [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DataGroup
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">name</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]




  // Relations

  templateapp_models_DataGroupNode -> templateapp_models_DataGroup
  [label=" datagroup (datagroupnode)"] [arrowhead=none, arrowtail=dot, dir=both];

  templateapp_models_DataGroupUserGroup -> templateapp_models_DataGroup
  [label=" datagroup (datagroupusergroup)"] [arrowhead=none, arrowtail=dot, dir=both];

  templateapp_models_DataGroupRelation -> templateapp_models_DataGroup
  [label=" child_group (datagrouprelation_child)"] [arrowhead=none, arrowtail=dot, dir=both];

  templateapp_models_DataGroupRelation -> templateapp_models_DataGroup
  [label=" parent_group (datagrouprelation_parent)"] [arrowhead=none, arrowtail=dot, dir=both];


}
from django.db import models
from django_postgresql_dag.models import node_factory, edge_factory
from django.contrib.auth.models import User, Group


class DataGroupEdge(edge_factory("templateapp.DataGroupNode", concrete=False)):

    def __str__(self):
        return (
            f"{self.parent.graph_type}: {self.parent.datagroup} "
            "→ {self.child.datagroup}"
        )


class DataGroupNode(node_factory(DataGroupEdge)):
    """A node to display relations between data groups."""

    class Meta:

        unique_together = ("graph_type", "datagroup")

    class GraphType(models.TextChoices):
        """Type of the graph for a datagroup."""

        parent_graph = "PARENT", "Parent-Child graph"
        member_graph = "MEMBER", "Member inheritance graph"
        list_graph = "LIST", "Dataset listing graph"

    graph_type = models.CharField(max_length=10, choices=GraphType.choices)
    datagroup = models.ForeignKey("DataGroup", on_delete=models.CASCADE)


class DataGroupUserGroup(Group):
    """A group with a certain role in a :class:`DataGroup`"""

    class Roles(models.TextChoices):
        owner = "OWNER", "owner priviliges"
        user_manager = "USERMANAGER", "user manager privileges"
        data_manager = "DATAMANAGER", "data manager privileges"
        data_editor = "DATAEDITOR", "data editor privileges"
        editor = "EDITOR", "editor privileges"
        member = "MEMBER", "view permissions"

    datagroup = models.ForeignKey("DataGroup", on_delete=models.CASCADE)
    role = models.CharField(max_length=20, choices=Roles.choices)


class DataGroupRelation(models.Model):
    """An relation between two datagroups that awaits approval."""

    class Meta:

        unique_together = ("child_group", "parent_group", "graph_type")

    child_group = models.ForeignKey(
        "DataGroup", on_delete=models.CASCADE, related_name="%(class)s_child"
    )
    parent_group = models.ForeignKey(
        "DataGroup", on_delete=models.CASCADE, related_name="%(class)s_parent"
    )

    child_approved = models.BooleanField(default=False)
    parent_approved = models.BooleanField(default=False)

    child_approved_by = models.ForeignKey(
        User,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_child_approval",
    )
    parent_approved_by = models.ForeignKey(
        User,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_parent_approval",
    )

    graph_type = models.CharField(
        max_length=10, choices=DataGroupNode.GraphType.choices
    )


class DataGroup(models.Model):
    """A DataGroup in the Model Data Explorer"""

    name = models.CharField(max_length=100)

DataGroup Permission Graphs

The parent graph

Nodes in the parent graph inherit the owners, data managers, etc. from the parents. As an example, consider Hereon and the Institute for Coastal Systems.

Hereon has a node in the parent graph, and the institute does. To represent the relation between the two, we set the DataGroupNode in the parent graph of Hereon as a parent for institutes DataGroupNode in the parent graph. As a consequence, each owner, data manager, etc. of Hereon will become an owner, data manager, etc. of the institute.

The members graph

Nodes in the parent graph inherit members. But in contrast to the parent graph above, members of the child node are added as members of the parent node. As an example: If there is a relation in the members graph like HereonInstitute for Coastal Systems, then each member of the Institute for Coastal Systems will be automatically added as a member of Hereon.

The list graph

The list graph implements the permission to list the contents of a data group as the result of another data group. If there is a relation HereonInstitute for Coastal Systems between the two DataGroupNodes in the list graph, this means that

  1. the institute is listed as a child on the Hereon page

  2. each Dataset that grants list permissions to the institute is also listed on the Hereon page

Dataset relations

Relations between Datasets and DataGroups and between Datasets and Users are implemented more or less the same, as they both share the same roles (see Users and datasets and Datasets and data groups). Each relation needs to be approved by the dataset owner and the related party (user or data group). Once the relation is approved, the owner or corresponding DataGroupUserGroups get the relevant permissions.

Four models are important here:

Dataset

The dataset that is related to the user or data group

DatasetUserRelation

A relation between a dataset and a user

DatasetDataGroupRelation

A relation between a dataset and a data group

digraph model_graph {
  // Dotfile by Django-Extensions graph_models
  // Created: 2023-02-14 14:40
  // Cli Options: --output /home/docs/checkouts/readthedocs.org/user_builds/mde-prototype/checkouts/develop/source/tmp_graph.dot templateapp --exclude-models DatasetRelationBase,User

  fontname = "Roboto"
  fontsize = 8
  splines  = true
  rankdir = "TB"

  node [
    fontname = "Roboto"
    fontsize = 8
    shape = "plaintext"
  ]

  edge [
    fontname = "Roboto"
    fontsize = 8
  ]

  // Labels


  templateapp_models_DataGroup [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DataGroup
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">name</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_Dataset [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    Dataset
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto">name</FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto">CharField</FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_DatasetDataGroupRelation [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DatasetDataGroupRelation<BR/>&lt;<FONT FACE="Roboto"><I>DatasetRelationBase</I></FONT>&gt;
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I><B>dataset</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>dataset_approved_by</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>related_party</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>related_party_approved_by</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>dataset_approved</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>BooleanField</I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>related_party_approved</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>BooleanField</I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>role</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>CharField</I></FONT>
    </TD></TR>
  
  
    </TABLE>
    >]

  templateapp_models_DatasetUserRelation [label=<
    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#1b563f">
    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
    DatasetUserRelation<BR/>&lt;<FONT FACE="Roboto"><I>DatasetRelationBase</I></FONT>&gt;
    </B></FONT></TD></TR>
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>id</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>BigAutoField</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I><B>dataset</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>dataset_approved_by</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><B>related_party</B></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><B>ForeignKey (id)</B></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>related_party_approved_by</B></I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT COLOR="#7B7B7B" FACE="Roboto"><I><B>ForeignKey (id)</B></I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>dataset_approved</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>BooleanField</I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>related_party_approved</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>BooleanField</I></FONT>
    </TD></TR>
  
  
  
    <TR><TD ALIGN="LEFT" BORDER="0">
    <FONT FACE="Roboto"><I>role</I></FONT>
    </TD><TD ALIGN="LEFT">
    <FONT FACE="Roboto"><I>CharField</I></FONT>
    </TD></TR>
  
  
    </TABLE>
    >]




  // Relations

  templateapp_models_DatasetDataGroupRelation -> templateapp_models_Dataset
  [label=" dataset (datasetdatagrouprelation)"] [arrowhead=none, arrowtail=dot, dir=both];

  templateapp_models_DatasetDataGroupRelation -> templateapp_models_DataGroup
  [label=" related_party (datasetdatagrouprelation)"] [arrowhead=none, arrowtail=dot, dir=both];

  templateapp_models_DatasetUserRelation -> templateapp_models_Dataset
  [label=" dataset (datasetuserrelation)"] [arrowhead=none, arrowtail=dot, dir=both];


}
from django.contrib.auth.models import User, Group

class DataGroup(models.Model):
    """A DataGroup in the Model Data Explorer"""

    name = models.CharField(max_length=100)


class Dataset(models.Model):
    """A dataset in the model data explorer."""

    name = models.CharField(max_length=100)


class DatasetRelationBase(models.Model):
    """Abstract base for a relation between dataset and user or group."""

    class Meta:

        unique_together = ("dataset", "related_party", "role")
        abstract = True

    class Roles(models.TextChoices):
        owner = "OWNER", "owner priviliges"
        data_manager = "DATAMANAGER", "data manager privileges"
        data_editor = "DATAEDITOR", "data editor privileges"
        editor = "EDITOR", "editor privileges"
        member = "MEMBER", "view permissions"


    dataset = models.ForeignKey(
        "Dataset", on_delete=models.CASCADE, related_name="%(class)s"
    )
    role = models.CharField(
        max_length=20, choices=Roles.choices
    )

    dataset_approved = models.BooleanField(default=False)
    related_party_approved = models.BooleanField(default=False)

    dataset_approved_by = models.ForeignKey(
        User,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_dataset_approval",
    )
    related_party_approved_by = models.ForeignKey(
        User,
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_related_party_approval",
    )


class DatasetDataGroupRelation(DatasetRelationBase):
    """A permission for a relation."""

    related_party = models.ForeignKey(DataGroup, on_delete=models.CASCADE)


class DatasetUserRelation(DatasetRelationBase):
    """A permission for a relation."""

    related_party = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="%(class)s_related_party"
    )