Coverage for tenancy/models/tenants.py: 94%
34 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.contrib.postgres.indexes import GistIndex
2from django.db import models
3from django.db.models import Count, ProtectedError, Q
4from django.utils.translation import gettext_lazy as _
6from netbox.models import NestedLtreeGroupModel, PrimaryModel
7from netbox.models.features import ContactsMixin
9__all__ = (
10 'Tenant',
11 'TenantGroup',
12)
15class TenantGroup(NestedLtreeGroupModel):
16 """
17 An arbitrary collection of Tenants.
18 """
19 name = models.CharField(
20 verbose_name=_('name'),
21 max_length=100,
22 unique=True,
23 db_collation="natural_sort"
24 )
25 slug = models.SlugField(
26 verbose_name=_('slug'),
27 max_length=100,
28 unique=True
29 )
30 # sort_path inherits natural_sort collation from `name` automatically (LtreeModelBase).
32 class Meta:
33 ordering = ('sort_path',)
34 indexes = (
35 GistIndex(fields=['path'], name='tenancy_tenantgroup_path_gist'),
36 models.Index(fields=['sort_path'], name='tenancy_tg_sort_path_idx'),
37 )
38 verbose_name = _('tenant group')
39 verbose_name_plural = _('tenant groups')
41 def delete(self, *args, **kwargs):
42 # Ungrouping the tenants of this group and its descendants can violate tenant name and slug uniqueness.
43 ungrouped = Tenant.objects.filter(
44 Q(group__isnull=True) | Q(group__in=self.get_descendants(include_self=True))
45 )
46 duplicate_names = ungrouped.values('name').annotate(count=Count('pk')).filter(count__gt=1).values('name')
47 duplicate_slugs = ungrouped.values('slug').annotate(count=Count('pk')).filter(count__gt=1).values('slug')
48 if conflicts := set(ungrouped.filter(Q(name__in=duplicate_names) | Q(slug__in=duplicate_slugs))): 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 raise ProtectedError(
50 _(
51 "Unable to delete tenant group {tenant_group}. Ungrouping its tenants, including those of any "
52 "nested groups, would create duplicate tenant names or slugs."
53 ).format(tenant_group=self),
54 conflicts,
55 )
57 return super().delete(*args, **kwargs)
60class Tenant(ContactsMixin, PrimaryModel):
61 """
62 A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
63 department.
64 """
65 name = models.CharField(
66 verbose_name=_('name'),
67 max_length=100,
68 db_collation="natural_sort"
69 )
70 slug = models.SlugField(
71 verbose_name=_('slug'),
72 max_length=100
73 )
74 group = models.ForeignKey(
75 to='tenancy.TenantGroup',
76 on_delete=models.SET_NULL,
77 related_name='tenants',
78 blank=True,
79 null=True
80 )
82 clone_fields = (
83 'group', 'description',
84 )
86 class Meta:
87 ordering = ['name']
88 constraints = (
89 models.UniqueConstraint(
90 fields=('group', 'name'),
91 name='%(app_label)s_%(class)s_unique_group_name',
92 nulls_distinct=False,
93 violation_error_message=_("Tenant name must be unique per group.")
94 ),
95 models.UniqueConstraint(
96 fields=('group', 'slug'),
97 name='%(app_label)s_%(class)s_unique_group_slug',
98 nulls_distinct=False,
99 violation_error_message=_("Tenant slug must be unique per group.")
100 ),
101 )
102 verbose_name = _('tenant')
103 verbose_name_plural = _('tenants')
105 def __str__(self):
106 return self.name