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):
parent = NestedTenantGroupSerializer(required=False, allow_null=True)
tenant_count = serializers.IntegerField(read_only=True)
class Meta:
model = TenantGroup
fields = ['id', 'name', 'slug', 'tenant_count']
fields = ['id', 'name', 'slug', 'parent', 'tenant_count']
class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):

View File

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

View File

@ -16,16 +16,32 @@ from .models import Tenant, TenantGroup
#
class TenantGroupForm(BootstrapMixin, forms.ModelForm):
parent = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenant-groups/"
)
)
slug = SlugField()
class Meta:
model = TenantGroup
fields = [
'name', 'slug',
'parent', 'name', 'slug',
]
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()
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.db import models
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
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.utils import serialize_object
__all__ = (
@ -13,7 +15,7 @@ __all__ = (
)
class TenantGroup(ChangeLoggedModel):
class TenantGroup(MPTTModel, ChangeLoggedModel):
"""
An arbitrary collection of Tenants.
"""
@ -24,12 +26,23 @@ class TenantGroup(ChangeLoggedModel):
slug = models.SlugField(
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:
ordering = ['name']
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
@ -40,6 +53,16 @@ class TenantGroup(ChangeLoggedModel):
return (
self.name,
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 .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 = """
<a href="{% url 'tenancy:tenantgroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
<i class="fa fa-history"></i>
@ -27,11 +37,18 @@ COL_TENANT = """
class TenantGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
tenant_count = tables.Column(verbose_name='Tenants')
slug = tables.Column(verbose_name='Slug')
name = tables.TemplateColumn(
template_code=MPTT_LINK,
orderable=False
)
tenant_count = tables.Column(
verbose_name='Tenants'
)
slug = tables.Column()
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):

View File

@ -20,7 +20,13 @@ from .models import Tenant, TenantGroup
class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
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