Skip to content

Commit

Permalink
Merge pull request #4351 from netbox-community/1754-nested-rackgroups
Browse files Browse the repository at this point in the history
Closes #1754: Nested rack groups
  • Loading branch information
jeremystretch authored Mar 12, 2020
2 parents 2b33e91 + c42613c commit a4a2760
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 51 deletions.
3 changes: 2 additions & 1 deletion netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@ class Meta:

class RackGroupSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer()
parent = NestedRackGroupSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True)

class Meta:
model = RackGroup
fields = ['id', 'name', 'slug', 'site', 'rack_count']
fields = ['id', 'name', 'slug', 'site', 'parent', 'rack_count']


class RackRoleSerializer(ValidatedModelSerializer):
Expand Down
47 changes: 32 additions & 15 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug',
label='Site (slug)',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=RackGroup.objects.all(),
to_field_name='slug',
label='Rack group (slug)',
)

class Meta:
model = RackGroup
Expand Down Expand Up @@ -194,15 +204,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
label='Group (ID)',
field_name='group',
lookup_expr='in',
label='Rack group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
field_name='group__slug',
group = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
field_name='group',
lookup_expr='in',
to_field_name='slug',
label='Group',
label='Rack group (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices,
Expand Down Expand Up @@ -262,16 +275,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack__group',
group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
label='Group (ID)',
field_name='rack__group',
lookup_expr='in',
label='Rack group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
field_name='rack__group__slug',
group = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
field_name='rack__group',
lookup_expr='in',
to_field_name='slug',
label='Group',
label='Rack group (slug)',
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
Expand Down Expand Up @@ -551,9 +566,10 @@ class DeviceFilterSet(
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack__group',
rack_group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
field_name='rack__group',
lookup_expr='in',
label='Rack group (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
Expand Down Expand Up @@ -1243,9 +1259,10 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack_group',
rack_group_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(),
field_name='rack_group',
lookup_expr='in',
label='Rack group (ID)',
)

Expand Down
38 changes: 35 additions & 3 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,25 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
widget=APISelect(
api_url="/api/dcim/sites/"
api_url="/api/dcim/sites/",
filter_for={
'parent': 'site_id',
}
)
)
parent = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/rack-groups/"
)
)
slug = SlugField()

class Meta:
model = RackGroup
fields = (
'site', 'name', 'slug',
'site', 'parent', 'name', 'slug',
)


Expand All @@ -407,6 +417,15 @@ class RackGroupCSVForm(forms.ModelForm):
'invalid_choice': 'Site not found.',
}
)
parent = forms.ModelChoiceField(
queryset=RackGroup.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent rack group',
error_messages={
'invalid_choice': 'Rack group not found.',
}
)

class Meta:
model = RackGroup
Expand All @@ -426,7 +445,8 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form):
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
'site': 'region',
'parent': 'region',
}
)
)
Expand All @@ -437,6 +457,18 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form):
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
filter_for={
'parent': 'site',
}
)
)
parent = DynamicModelMultipleChoiceField(
queryset=RackGroup.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
value_field="slug",
)
)

Expand Down
43 changes: 43 additions & 0 deletions netbox/dcim/migrations/0101_nested_rackgroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields


class Migration(migrations.Migration):

dependencies = [
('dcim', '0100_mptt_remove_indexes'),
]

operations = [
migrations.AddField(
model_name='rackgroup',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.RackGroup'),
),
migrations.AddField(
model_name='rackgroup',
name='level',
field=models.PositiveIntegerField(default=0, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='rackgroup',
name='lft',
field=models.PositiveIntegerField(default=1, editable=False),
preserve_default=False,
),
migrations.AddField(
model_name='rackgroup',
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='rackgroup',
name='tree_id',
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
preserve_default=False,
),
]
21 changes: 21 additions & 0 deletions netbox/dcim/migrations/0102_nested_rackgroups_rebuild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import migrations


def rebuild_mptt(apps, schema_editor):
RackGroup = apps.get_model('dcim', 'RackGroup')
for i, rackgroup in enumerate(RackGroup.objects.all(), start=1):
RackGroup.objects.filter(pk=rackgroup.pk).update(tree_id=i)


class Migration(migrations.Migration):

dependencies = [
('dcim', '0101_nested_rackgroups'),
]

operations = [
migrations.RunPython(
code=rebuild_mptt,
reverse_code=migrations.RunPython.noop
),
]
31 changes: 29 additions & 2 deletions netbox/dcim/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def get_status_class(self):
# Racks
#

class RackGroup(ChangeLoggedModel):
class RackGroup(MPTTModel, ChangeLoggedModel):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
Expand All @@ -298,8 +298,16 @@ class RackGroup(ChangeLoggedModel):
on_delete=models.CASCADE,
related_name='rack_groups'
)
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)

csv_headers = ['site', 'name', 'slug']
csv_headers = ['site', 'parent', 'name', 'slug']

class Meta:
ordering = ['site', 'name']
Expand All @@ -308,6 +316,9 @@ class Meta:
['site', 'slug'],
]

class MPTTMeta:
order_insertion_by = ['name']

def __str__(self):
return self.name

Expand All @@ -317,10 +328,26 @@ def get_absolute_url(self):
def to_csv(self):
return (
self.site,
self.parent.name if self.parent else '',
self.name,
self.slug,
)

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

def clean(self):

# Parent RackGroup (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")


class RackRole(ChangeLoggedModel):
"""
Expand Down
11 changes: 7 additions & 4 deletions netbox/dcim/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
VirtualChassis,
)

REGION_LINK = """
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="{% url 'dcim:site_list' %}?region={{ record.slug }}">{{ record.name }}</a>
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
</span>
"""

Expand Down Expand Up @@ -214,7 +214,7 @@ def get_component_template_actions(model_name):

class RegionTable(BaseTable):
pk = ToggleColumn()
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
name = tables.TemplateColumn(template_code=MPTT_LINK, orderable=False)
site_count = tables.Column(verbose_name='Sites')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
Expand Down Expand Up @@ -250,7 +250,10 @@ class Meta(BaseTable.Meta):

class RackGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
name = tables.TemplateColumn(
template_code=MPTT_LINK,
orderable=False
)
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')],
Expand Down
Loading

0 comments on commit a4a2760

Please sign in to comment.