1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Implement support for nested TenantGroups

This commit is contained in:
Jeremy Stretch
2020-03-11 20:20:04 -04:00
parent a4a276083a
commit 45f6ea211d
8 changed files with 163 additions and 22 deletions

View File

@ -12,11 +12,12 @@ from .nested_serializers import *
# #
class TenantGroupSerializer(ValidatedModelSerializer): class TenantGroupSerializer(ValidatedModelSerializer):
parent = NestedTenantGroupSerializer(required=False, allow_null=True)
tenant_count = serializers.IntegerField(read_only=True) tenant_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = ['id', 'name', 'slug', 'tenant_count'] fields = ['id', 'name', 'slug', 'parent', 'tenant_count']
class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer): class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):

View File

@ -2,7 +2,7 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -14,6 +14,16 @@ __all__ = (
class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=TenantGroup.objects.all(),
label='Tenant group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=TenantGroup.objects.all(),
to_field_name='slug',
label='Tenant group group (slug)',
)
class Meta: class Meta:
model = TenantGroup model = TenantGroup
@ -25,15 +35,18 @@ class TenantFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
method='search', method='search',
label='Search', label='Search',
) )
group_id = django_filters.ModelMultipleChoiceFilter( group_id = TreeNodeMultipleChoiceFilter(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
label='Group (ID)', field_name='group',
lookup_expr='in',
label='Tenant group (ID)',
) )
group = django_filters.ModelMultipleChoiceFilter( group = TreeNodeMultipleChoiceFilter(
field_name='group__slug',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
field_name='group',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Group (slug)', label='Tenant group (slug)',
) )
tag = TagFilter() tag = TagFilter()
@ -56,16 +69,17 @@ class TenancyFilterSet(django_filters.FilterSet):
""" """
An inheritable FilterSet for models which support Tenant assignment. An inheritable FilterSet for models which support Tenant assignment.
""" """
tenant_group_id = django_filters.ModelMultipleChoiceFilter( tenant_group_id = TreeNodeMultipleChoiceFilter(
field_name='tenant__group__id',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='id', field_name='tenant__group',
lookup_expr='in',
label='Tenant Group (ID)', label='Tenant Group (ID)',
) )
tenant_group = django_filters.ModelMultipleChoiceFilter( tenant_group = TreeNodeMultipleChoiceFilter(
field_name='tenant__group__slug',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
field_name='tenant__group',
to_field_name='slug', to_field_name='slug',
lookup_expr='in',
label='Tenant Group (slug)', label='Tenant Group (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
@ -73,8 +87,8 @@ class TenancyFilterSet(django_filters.FilterSet):
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = django_filters.ModelMultipleChoiceFilter( tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
field_name='tenant__slug',
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )

View File

@ -16,16 +16,32 @@ from .models import Tenant, TenantGroup
# #
class TenantGroupForm(BootstrapMixin, forms.ModelForm): class TenantGroupForm(BootstrapMixin, forms.ModelForm):
parent = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenant-groups/"
)
)
slug = SlugField() slug = SlugField()
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = [ fields = [
'name', 'slug', 'parent', 'name', 'slug',
] ]
class TenantGroupCSVForm(forms.ModelForm): class TenantGroupCSVForm(forms.ModelForm):
parent = forms.ModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent tenant group',
error_messages={
'invalid_choice': 'Tenant group not found.',
}
)
slug = SlugField() slug = SlugField()
class Meta: class Meta:

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0006_custom_tag_models'),
]
operations = [
migrations.AddField(
model_name='tenantgroup',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tenancy.TenantGroup'),
),
migrations.AddField(
model_name='tenantgroup',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='tenantgroup',
name='lft',
field=models.PositiveIntegerField(default=1, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='tenantgroup',
name='rght',
field=models.PositiveIntegerField(default=2, editable=False),
preserve_default=False,
),
# tree_id will be set to a valid value during the following migration (which needs to be a separate migration)
migrations.AddField(
model_name='tenantgroup',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
]

View File

@ -0,0 +1,21 @@
from django.db import migrations
def rebuild_mptt(apps, schema_editor):
TenantGroup = apps.get_model('tenancy', 'TenantGroup')
for i, tenantgroup in enumerate(TenantGroup.objects.all(), start=1):
TenantGroup.objects.filter(pk=tenantgroup.pk).update(tree_id=i)
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0007_nested_tenantgroups'),
]
operations = [
migrations.RunPython(
code=rebuild_mptt,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -1,10 +1,12 @@
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from extras.models import CustomFieldModel, TaggedItem from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
__all__ = ( __all__ = (
@ -13,7 +15,7 @@ __all__ = (
) )
class TenantGroup(ChangeLoggedModel): class TenantGroup(MPTTModel, ChangeLoggedModel):
""" """
An arbitrary collection of Tenants. An arbitrary collection of Tenants.
""" """
@ -24,12 +26,23 @@ class TenantGroup(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
csv_headers = ['name', 'slug'] csv_headers = ['name', 'slug', 'parent']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self): def __str__(self):
return self.name return self.name
@ -40,6 +53,16 @@ class TenantGroup(ChangeLoggedModel):
return ( return (
self.name, self.name,
self.slug, self.slug,
self.parent.name if self.parent else '',
)
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
) )

View File

@ -3,6 +3,16 @@ import django_tables2 as tables
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
MPTT_LINK = """
{% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
{% else %}
<span style="padding-left: {{ record.get_ancestors|length }}9px">
{% endif %}
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
</span>
"""
TENANTGROUP_ACTIONS = """ TENANTGROUP_ACTIONS = """
<a href="{% url 'tenancy:tenantgroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log"> <a href="{% url 'tenancy:tenantgroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
@ -27,11 +37,18 @@ COL_TENANT = """
class TenantGroupTable(BaseTable): class TenantGroupTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.TemplateColumn(
tenant_count = tables.Column(verbose_name='Tenants') template_code=MPTT_LINK,
slug = tables.Column(verbose_name='Slug') orderable=False
)
tenant_count = tables.Column(
verbose_name='Tenants'
)
slug = tables.Column()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=TENANTGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' template_code=TENANTGROUP_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -20,7 +20,13 @@ from .models import Tenant, TenantGroup
class TenantGroupListView(PermissionRequiredMixin, ObjectListView): class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'tenancy.view_tenantgroup' permission_required = 'tenancy.view_tenantgroup'
queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants')) queryset = TenantGroup.objects.add_related_count(
TenantGroup.objects.all(),
Tenant,
'group',
'tenant_count',
cumulative=True
)
table = tables.TenantGroupTable table = tables.TenantGroupTable