class SerializerModel(models.Model):
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=200)
    number = models.IntegerField(null=True)
    attributes = JSONAttributeField(null=True, blank=True)

    class Meta:
        app_label = 'core'
Ejemplo n.º 2
0
class Organization(models.Model):
    name = models.CharField(max_length=100)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('name', )

    def __str__(self):
        return self.name
Ejemplo n.º 3
0
class Project(models.Model):
    name = models.CharField(max_length=100)
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('organization', 'name')

    def __str__(self):
        return self.name
Ejemplo n.º 4
0
class Department(models.Model):
    name = models.CharField(max_length=100)
    division = models.ForeignKey(Division, related_name='departments')
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('division', 'name')

    def __str__(self):
        return self.name
Ejemplo n.º 5
0
class SpatialResource(RandomIDModel):

    class Meta:
        ordering = ('name',)

    class TutelaryMeta:
        perm_type = 'resource'
        path_fields = ('resource', 'pk')
        actions = (
            ('resource.list',
             {'description': _("List resources"),
              'permissions_object': 'resource',
              'error_message': messages.RESOURCE_LIST}),
            ('resource.view',
             {'description': _("View resource"),
              'error_message': messages.RESOURCE_VIEW}),
            ('resource.archive',
             {'description': _("Archive resource"),
              'error_message': messages.RESOURCE_ARCHIVE}),
            ('resource.unarchive',
             {'description': _("Unarchive resource"),
              'error_message': messages.RESOURCE_UNARCHIVE}),
        )

    name = models.CharField(max_length=256, null=True, blank=True)

    time = models.DateTimeField(auto_now_add=True, null=True, blank=True)

    resource = models.ForeignKey(
        Resource,
        on_delete=models.CASCADE, related_name='spatial_resources'
    )
    geom = GeometryCollectionField(
        srid=4326, blank=True, null=True
    )
    attributes = JSONAttributeField(default={})

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    def __repr__(self):
        repr_string = ('<SpatialResource id={obj.id}'
                       ' resource={obj.resource.id}>')
        return repr_string.format(obj=self)

    @property
    def archived(self):
        return self.resource.archived

    @property
    def project(self):
        return self.resource.project
Ejemplo n.º 6
0
class Party(models.Model):
    department = models.ForeignKey(Department, related_name='parties')
    name = models.CharField(max_length=100)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('department', 'name')

    def get_absolute_url(self):
        return reverse('party-detail', kwargs={'pk': self.pk})

    def __str__(self):
        return self.name
Ejemplo n.º 7
0
class Party(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('project', 'name')

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('party-detail', kwargs={'pk': self.pk})
Ejemplo n.º 8
0
class Contract(models.Model):
    department = models.ForeignKey(Department, related_name='contracts')
    responsible = models.ForeignKey(Party)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('department', 'pk')

    def get_absolute_url(self):
        return reverse('parcel-detail', kwargs={'pk': self.pk})

    def __str__(self):
        return str(self.pk)
Ejemplo n.º 9
0
class Parcel(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    type = models.CharField(max_length=20)
    address = models.CharField(max_length=200)
    attrs = JSONAttributeField()

    class Meta:
        ordering = ('project', 'address')

    def __str__(self):
        return self.address

    def get_absolute_url(self):
        return reverse('parcel-detail', kwargs={'pk': self.pk})
Ejemplo n.º 10
0
class Party(ResourceModelMixin, RandomIDModel):
    """
    Party model.

    A single party: has a name, a type, a type-dependent set of
    attributes and relationships with other parties and spatial units
    (i.e. tenure relationships).

    """

    # Possible party types: TYPE_CHOICES is the well-known name used
    # by the JSONAttributesField field type to manage the range of
    # allowed attribute fields.
    INDIVIDUAL = 'IN'
    CORPORATION = 'CO'
    GROUP = 'GR'
    TYPE_CHOICES = ((INDIVIDUAL, __('Individual')),
                    (CORPORATION, __('Corporation')), (GROUP, __('Group')))

    # All parties are associated with a single project.
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='parties')

    # All parties have a name: for individuals, this is the full name,
    # while for groups and corporate entities, it's whatever name is
    # conventionally used to identify the organisation.
    name = models.CharField(max_length=200)

    # Party type: used to manage range of allowed attributes.
    type = models.CharField(max_length=2,
                            choices=TYPE_CHOICES,
                            default=INDIVIDUAL)

    contacts = JSONField(validators=[validate_contact], default={})

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})

    # Party-party relationships: includes things like family
    # relationships and group memberships.
    relationships = models.ManyToManyField('self',
                                           through='PartyRelationship',
                                           through_fields=('party1', 'party2'),
                                           symmetrical=False,
                                           related_name='relationships_set')

    # Tenure relationships.
    tenure_relationships = models.ManyToManyField(
        SpatialUnit,
        through='TenureRelationship',
        related_name='tenure_relationships')

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class Meta:
        ordering = ('name', )

    class TutelaryMeta:
        perm_type = 'party'
        path_fields = ('project', 'id')
        actions = (('party.list', {
            'description': _("List existing parties of a project"),
            'error_message': messages.PARTY_LIST,
            'permissions_object': 'project'
        }), ('party.create', {
            'description': _("Add a party to a project"),
            'error_message': messages.PARTY_CREATE,
            'permissions_object': 'project'
        }), ('party.view', {
            'description': _("View an existing party"),
            'error_message': messages.PARTY_VIEW
        }), ('party.update', {
            'description': _("Update an existing party"),
            'error_message': messages.PARTY_UPDATE
        }), ('party.delete', {
            'description': _("Delete an existing party"),
            'error_message': messages.PARTY_DELETE
        }), ('party.resources.add', {
            'description': _("Add resources to the party"),
            'error_message': messages.PARTY_RESOURCES_ADD
        }))

    def __str__(self):
        return "<Party: {}>".format(self.name)

    def __repr__(self):
        repr_string = ('<Party id={obj.id} name={obj.name}'
                       ' project={obj.project.slug}'
                       ' type={obj.type}>')
        return repr_string.format(obj=self)

    @property
    def ui_class_name(self):
        return _("Party")

    def get_absolute_url(self):
        return iri_to_uri(
            reverse(
                'parties:detail',
                kwargs={
                    'organization': self.project.organization.slug,
                    'project': self.project.slug,
                    'party': self.id,
                },
            ))
Ejemplo n.º 11
0
class TenureRelationship(ResourceModelMixin, RandomIDModel):
    """TenureRelationship model.

    Governs relationships between Party and SpatialUnit.
    """

    # All tenure relationships are associated with a single project
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='tenure_relationships')

    # Party to the relationship
    party = models.ForeignKey(Party, on_delete=models.CASCADE)

    # Spatial unit in the relationship
    spatial_unit = models.ForeignKey(SpatialUnit, on_delete=models.CASCADE)

    # Tenure relationships type: used to manage range of allowed attributes
    tenure_type = models.CharField(max_length=10)

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})
    objects = managers.TenureRelationshipManager()

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class TutelaryMeta:
        perm_type = 'tenure_rel'
        path_fields = ('project', 'id')
        actions = (
            ('tenure_rel.list', {
                'description':
                _("List existing tenure relationships"
                  " of a project"),
                'error_message':
                messages.TENURE_REL_LIST,
                'permissions_object':
                'project'
            }),
            ('tenure_rel.create', {
                'description': _("Add a tenure relationship to a project"),
                'error_message': messages.TENURE_REL_CREATE,
                'permissions_object': 'project'
            }),
            ('tenure_rel.view', {
                'description': _("View an existing tenure relationship"),
                'error_message': messages.TENURE_REL_VIEW
            }),
            ('tenure_rel.update', {
                'description': _("Update an existing tenure relationship"),
                'error_message': messages.TENURE_REL_UPDATE
            }),
            ('tenure_rel.delete', {
                'description': _("Delete an existing tenure relationship"),
                'error_message': messages.TENURE_REL_DELETE
            }),
            ('tenure_rel.resources.add', {
                'description': _("Add a resource to a tenure relationship"),
                'error_message': messages.TENURE_REL_RESOURCES_ADD
            }),
        )

    def __str__(self):
        return "<TenureRelationship: {}>".format(self.name)

    def __repr__(self):
        repr_string = ('<TenureRelationship id={obj.id}'
                       ' party={obj.party.id}'
                       ' spatial_unit={obj.spatial_unit.id}'
                       ' project={obj.project.slug}'
                       ' tenure_type={obj.tenure_type}>')
        return repr_string.format(obj=self)

    @property
    def name(self):
        return "<{party}> {type} <{su}>".format(
            party=self.party.name,
            su=self.spatial_unit.name,
            type=self.tenure_type,
        )

    @property
    def ui_class_name(self):
        return _("Relationship")

    def get_absolute_url(self):
        return iri_to_uri(
            reverse(
                'parties:relationship_detail',
                kwargs={
                    'organization': self.project.organization.slug,
                    'project': self.project.slug,
                    'relationship': self.id,
                },
            ))

    @cached_property
    def tenure_type_label(self):
        if not self.project.current_questionnaire:
            return dict(TENURE_RELATIONSHIP_TYPES)[self.tenure_type]

        question = Question.objects.get(
            questionnaire_id=self.project.current_questionnaire,
            name='tenure_type')
        label = question.options.get(name=self.tenure_type).label_xlat
        if label is None or isinstance(label, str):
            return label
        else:
            return label.get(get_language(),
                             label[question.questionnaire.default_language])
Ejemplo n.º 12
0
class PartyRelationship(RandomIDModel):
    """
    PartyRelationship model.

    A relationship between parties: encodes simple logical terms like
    ``party1 is-spouse-of party2`` or ``party1 is-member-of party2``.
    May have additional type-dependent attributes.

    """

    # Possible party relationship types: TYPE_CHOICES is the
    # well-known name used by the JSONAttributesField field type to
    # manage the range of allowed attribute fields.
    TYPE_CHOICES = (('S', 'is-spouse-of'), ('C', 'is-child-of'),
                    ('M', 'is-member-of'))

    # All party relationships are associated with a single project
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='party_relationships')

    # Parties to the relationship.
    party1 = models.ForeignKey(Party,
                               on_delete=models.CASCADE,
                               related_name='party1')
    party2 = models.ForeignKey(Party,
                               on_delete=models.CASCADE,
                               related_name='party2')

    # Party relationship type: used to manage range of allowed attributes.
    type = models.CharField(max_length=1, choices=TYPE_CHOICES)

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})

    objects = managers.PartyRelationshipManager()

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class TutelaryMeta:
        perm_type = 'party_rel'
        path_fields = ('project', 'id')
        actions = (
            ('party_rel.list', {
                'description':
                _("List existing party relationships"
                  " of a project"),
                'error_message':
                messages.PARTY_REL_LIST,
                'permissions_object':
                'project'
            }),
            ('party_rel.create', {
                'description': _("Add a party relationship to a project"),
                'error_message': messages.PARTY_REL_CREATE,
                'permissions_object': 'project'
            }),
            ('party_rel.view', {
                'description': _("View an existing party relationship"),
                'error_message': messages.PARTY_REL_VIEW
            }),
            ('party_rel.update', {
                'description': _("Update an existing party relationship"),
                'error_message': messages.PARTY_REL_UPDATE
            }),
            ('party_rel.delete', {
                'description': _("Delete an existing party relationship"),
                'error_message': messages.PARTY_REL_DELETE
            }),
        )

    def __str__(self):
        return "<PartyRelationship: <{party1}> {type} <{party2}>>".format(
            party1=self.party1.name,
            party2=self.party2.name,
            type=dict(self.TYPE_CHOICES).get(self.type))

    def __repr__(self):
        repr_string = ('<PartyRelationship id={obj.id}'
                       ' party1={obj.party1.id}'
                       ' party2={obj.party2.id}'
                       ' project={obj.project.slug}'
                       ' type={obj.type}>')
        return repr_string.format(obj=self)
Ejemplo n.º 13
0
class TenureRelationship(ResourceModelMixin, RandomIDModel):
    """TenureRelationship model.

    Governs relationships between Party and SpatialUnit.
    """

    CONTRACTUAL_SHARE_CROP = 'CS'
    CUSTOMARY_ARRANGEMENT = 'CA'
    GIFT = 'GF'
    HOMESTEAD = 'HS'
    INFORMAL_OCCUPANT = 'IO'
    INHERITANCE = 'IN'
    LEASEHOLD = 'LH'
    PURCHASED_FREEHOLD = 'PF'
    RENTAL = 'RN'
    OTHER = 'OT'

    ACQUIRED_CHOICES = ((CONTRACTUAL_SHARE_CROP, _('Contractual/Share Crop')),
                        (CUSTOMARY_ARRANGEMENT, _('Customary Arrangement')),
                        (GIFT, _('Gift')),
                        (HOMESTEAD, _('Homestead')),
                        (INFORMAL_OCCUPANT, _('Informal Occupant')),
                        (INHERITANCE, _('Inheritance')),
                        (LEASEHOLD, _('Leasehold')),
                        (PURCHASED_FREEHOLD, _('Purchased Freehold')),
                        (RENTAL, _('Rental')),
                        (OTHER, _('Other')))

    # All tenure relationships are associated with a single project
    project = models.ForeignKey(
        Project, on_delete=models.CASCADE, related_name='tenure_relationships')

    # Party to the relationship
    party = models.ForeignKey(Party, on_delete=models.CASCADE)

    # Spatial unit in the relationship
    spatial_unit = models.ForeignKey(SpatialUnit, on_delete=models.CASCADE)

    # Tenure relationships type: used to manage range of allowed attributes
    tenure_type = models.ForeignKey(
        'TenureRelationshipType',
        related_name='tenure_type', null=False, blank=False
    )

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})
    objects = managers.TenureRelationshipManager()

    history = HistoricalRecords()

    class TutelaryMeta:
        perm_type = 'tenure_rel'
        path_fields = ('project', 'id')
        actions = (
            ('tenure_rel.list',
             {'description': _("List existing tenure relationships"
                               " of a project"),
              'error_message': messages.TENURE_REL_LIST,
              'permissions_object': 'project'}),
            ('tenure_rel.create',
             {'description': _("Add a tenure relationship to a project"),
              'error_message': messages.TENURE_REL_CREATE,
              'permissions_object': 'project'}),
            ('tenure_rel.view',
             {'description': _("View an existing tenure relationship"),
              'error_message': messages.TENURE_REL_VIEW}),
            ('tenure_rel.update',
             {'description': _("Update an existing tenure relationship"),
              'error_message': messages.TENURE_REL_UPDATE}),
            ('tenure_rel.delete',
             {'description': _("Delete an existing tenure relationship"),
              'error_message': messages.TENURE_REL_DELETE}),
            ('tenure_rel.resources.add',
             {'description': _("Add a resource to a tenure relationship"),
              'error_message': messages.TENURE_REL_RESOURCES_ADD}),
        )

    def __str__(self):
        return "<TenureRelationship: {}>".format(self.name)

    def __repr__(self):
        repr_string = ('<TenureRelationship id={obj.id}'
                       ' party={obj.party.id}'
                       ' spatial_unit={obj.spatial_unit.id}'
                       ' project={obj.project.slug}'
                       ' tenure_type={obj.tenure_type.id}>')
        return repr_string.format(obj=self)

    @property
    def name(self):
        return "<{party}> {type} <{su}>".format(
            party=self.party.name,
            su=self.spatial_unit.name,
            type=self.tenure_type_label,
        )

    @property
    def ui_class_name(self):
        return _("Relationship")

    def get_absolute_url(self):
        return iri_to_uri(reverse(
            'parties:relationship_detail',
            kwargs={
                'organization': self.project.organization.slug,
                'project': self.project.slug,
                'relationship': self.id,
            },
        ))

    @property
    def tenure_type_label(self):
        return _(self.tenure_type.label)
Ejemplo n.º 14
0
class Labelled(models.Model):
    label = models.CharField(max_length=64)
    name = models.CharField(max_length=64)
    attrs = JSONAttributeField()
Ejemplo n.º 15
0
class SpatialRelationship(RandomIDModel):
    """A relationship between spatial units: encodes simple logical terms
    like ``su1 is-contained-in su2`` or ``su1 is-split-of su2``.  May
    have additional requirements.

    """

    # Possible spatial unit relationships types: TYPE_CHOICES is the
    # well-known name used by the JSONAttributesField field type to
    # manage the range of allowed attribute fields.
    TYPE_CHOICES = (('C', 'is-contained-in'), ('S', 'is-split-of'),
                    ('M', 'is-merge-of'))

    # All spatial unit relationships are associated with a single project.
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='spatial_relationships')

    # Spatial units are in the relationships.
    su1 = models.ForeignKey(SpatialUnit,
                            on_delete=models.CASCADE,
                            related_name='spatial_unit_one')
    su2 = models.ForeignKey(SpatialUnit,
                            on_delete=models.CASCADE,
                            related_name='spatial_unit_two')

    # Spatial unit relationship type: used to manage range of allowed
    # attributes
    type = models.CharField(max_length=1, choices=TYPE_CHOICES)

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})
    objects = managers.SpatialRelationshipManager()

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class TutelaryMeta:
        perm_type = 'spatial_rel'
        path_fields = ('project', 'id')
        actions = (
            ('spatial_rel.list', {
                'description':
                _("List existing spatial relationships"
                  " of a project"),
                'error_message':
                messages.SPATIAL_REL_LIST,
                'permissions_object':
                'project'
            }),
            ('spatial_rel.create', {
                'description': _("Add a spatial relationship to a project"),
                'error_message': messages.SPATIAL_REL_CREATE,
                'permissions_object': 'project'
            }),
            ('spatial_rel.view', {
                'description': _("View an existing spatial relationship"),
                'error_message': messages.SPATIAL_REL_VIEW
            }),
            ('spatial_rel.update', {
                'description': _("Update an existing spatial relationship"),
                'error_message': messages.SPATIAL_REL_UPDATE
            }),
            ('spatial_rel.delete', {
                'description': _("Delete an existing spatial relationship"),
                'error_message': messages.SPATIAL_REL_DELETE
            }),
        )

    def __str__(self):
        return "<SpatialRelationship: <{su1}> {type} <{su2}>>".format(
            su1=self.su1.name,
            su2=self.su2.name,
            type=dict(self.TYPE_CHOICES).get(self.type))

    def __repr__(self):
        repr_string = ('<SpatialRelationship id={obj.id}'
                       ' project={obj.project.slug}'
                       ' su1={obj.su1_id}'
                       ' su2={obj.su2_id}'
                       ' type={obj.type}>')
        return repr_string.format(obj=self)
Ejemplo n.º 16
0
class SpatialUnit(ResourceModelMixin, RandomIDModel):
    """A single spatial unit: has a type, an optional geometry, a
    type-dependent set of attributes, and a set of relationships to
    other spatial units.

    """

    # All spatial units are associated with a single project.
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='spatial_units')

    # Spatial unit type: used to manage range of allowed attributes.
    type = models.CharField(max_length=10)

    # Spatial unit geometry is optional: some spatial units may only
    # have a textual description of their location.
    geometry = GeometryField(null=True, geography=True)

    # Area, auto-calculated via trigger (see spatial/migrations/#0005)
    area = models.FloatField(default=0)

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})

    # Spatial unit-spatial unit relationships: includes spatial
    # containment and split/merge relationships.
    relationships = models.ManyToManyField(
        'self',
        through='SpatialRelationship',
        through_fields=('su1', 'su2'),
        symmetrical=False,
        related_name='relationships_set',
    )

    # Audit history
    created_date = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

    history = HistoricalRecords()

    class Meta:
        ordering = ('type', )

    class TutelaryMeta:
        perm_type = 'spatial'
        path_fields = ('project', 'id')
        actions = (('spatial.list', {
            'description':
            _("List existing spatial units of a project"),
            'error_message':
            messages.SPATIAL_LIST,
            'permissions_object':
            'project'
        }), ('spatial.create', {
            'description': _("Add a spatial unit to a project"),
            'error_message': messages.SPATIAL_CREATE,
            'permissions_object': 'project'
        }), ('spatial.view', {
            'description': _("View an existing spatial unit"),
            'error_message': messages.SPATIAL_VIEW
        }), ('spatial.update', {
            'description': _("Update an existing spatial unit"),
            'error_message': messages.SPATIAL_UPDATE
        }), ('spatial.delete', {
            'description': _("Delete an existing spatial unit"),
            'error_message': messages.SPATIAL_DELETE
        }), ('spatial.resources.add', {
            'description': _("Add resources to this spatial unit"),
            'error_message': messages.SPATIAL_ADD_RESOURCE
        }))

    def __str__(self):
        return "<SpatialUnit: {}>".format(self.name)

    def __repr__(self):
        repr_string = ('<SpatialUnit id={obj.id}'
                       ' project={obj.project.slug}'
                       ' type={obj.type}>')
        return repr_string.format(obj=self)

    @property
    def name(self):
        return self.location_type_label

    @property
    def ui_class_name(self):
        return _("Location")

    def get_absolute_url(self):
        return iri_to_uri(
            reverse(
                'locations:detail',
                kwargs={
                    'organization': self.project.organization.slug,
                    'project': self.project.slug,
                    'location': self.id,
                },
            ))

    @cached_property
    def location_type_label(self):
        if not self.project.current_questionnaire:
            return dict(TYPE_CHOICES)[self.type]

        question = Question.objects.get(
            questionnaire_id=self.project.current_questionnaire,
            name='location_type')
        label = question.options.get(name=self.type).label_xlat
        if label is None or isinstance(label, str):
            return label
        else:
            return label.get(get_language(),
                             label[question.questionnaire.default_language])
Ejemplo n.º 17
0
class SpatialUnit(ResourceModelMixin, RandomIDModel):
    """A single spatial unit: has a type, an optional geometry, a
    type-dependent set of attributes, and a set of relationships to
    other spatial units.

    """

    # All spatial units are associated with a single project.
    project = models.ForeignKey(Project,
                                on_delete=models.CASCADE,
                                related_name='spatial_units')

    # Spatial unit type: used to manage range of allowed attributes.
    type = models.CharField(max_length=2, choices=TYPE_CHOICES, default='PA')

    # Spatial unit geometry is optional: some spatial units may only
    # have a textual description of their location.
    geometry = GeometryField(null=True)

    # JSON attributes field with management of allowed members.
    attributes = JSONAttributeField(default={})

    # Spatial unit-spatial unit relationships: includes spatial
    # containment and split/merge relationships.
    relationships = models.ManyToManyField(
        'self',
        through='SpatialRelationship',
        through_fields=('su1', 'su2'),
        symmetrical=False,
        related_name='relationships_set',
    )

    history = HistoricalRecords()

    class Meta:
        ordering = ('type', )

    class TutelaryMeta:
        perm_type = 'spatial'
        path_fields = ('project', 'id')
        actions = (('spatial.list', {
            'description':
            _("List existing spatial units of a project"),
            'error_message':
            messages.SPATIAL_LIST,
            'permissions_object':
            'project'
        }), ('spatial.create', {
            'description': _("Add a spatial unit to a project"),
            'error_message': messages.SPATIAL_CREATE,
            'permissions_object': 'project'
        }), ('spatial.view', {
            'description': _("View an existing spatial unit"),
            'error_message': messages.SPATIAL_VIEW
        }), ('spatial.update', {
            'description': _("Update an existing spatial unit"),
            'error_message': messages.SPATIAL_UPDATE
        }), ('spatial.delete', {
            'description': _("Delete an existing spatial unit"),
            'error_message': messages.SPATIAL_DELETE
        }), ('spatial.resources.add', {
            'description': _("Add resources to this spatial unit"),
            'error_message': messages.SPATIAL_ADD_RESOURCE
        }))

    def __str__(self):
        return "<SpatialUnit: {}>".format(self.name)

    def __repr__(self):
        repr_string = ('<SpatialUnit id={obj.id}'
                       ' project={obj.project.slug}'
                       ' type={obj.type}>')
        return repr_string.format(obj=self)

    @property
    def name(self):
        return self.get_type_display()

    @property
    def ui_class_name(self):
        return _("Location")

    def get_absolute_url(self):
        return iri_to_uri(
            reverse(
                'locations:detail',
                kwargs={
                    'organization': self.project.organization.slug,
                    'project': self.project.slug,
                    'location': self.id,
                },
            ))