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

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 _ 

5 

6from netbox.models import NestedLtreeGroupModel, PrimaryModel 

7from netbox.models.features import ContactsMixin 

8 

9__all__ = ( 

10 'Tenant', 

11 'TenantGroup', 

12) 

13 

14 

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). 

31 

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') 

40 

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 ) 

56 

57 return super().delete(*args, **kwargs) 

58 

59 

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 ) 

81 

82 clone_fields = ( 

83 'group', 'description', 

84 ) 

85 

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') 

104 

105 def __str__(self): 

106 return self.name