Merge branch 'main' into feature

This commit is contained in:
Jeremy Stretch
2025-05-01 09:45:38 -04:00
191 changed files with 10297 additions and 9102 deletions

View File

@@ -12,7 +12,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
@@ -28,6 +28,7 @@ from netbox.config import get_config
from netbox.views import generic
from users import forms, tables
from users.models import UserConfig
from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
@@ -148,7 +149,7 @@ class LoginView(View):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
if redirect_url and safe_for_redirect(redirect_url):
logger.debug(f"Redirecting user to {remove_linebreaks(redirect_url)}")
else:
if redirect_url:

View File

@@ -61,9 +61,8 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
type = columns.ColoredLabelColumn(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_a = columns.TemplateColumn(

View File

@@ -35,7 +35,19 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
'related_models': self.get_related_models(
request,
instance,
omit=(),
extra=(
(
VirtualCircuit.objects.restrict(request.user, 'view').filter(
provider_network__provider=instance
),
'provider_id',
),
),
),
}
@@ -51,7 +63,7 @@ class ProviderDeleteView(generic.ObjectDeleteView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_import', detail=False)
@register_model_view(Provider, 'bulk_import', path='import', detail=False)
class ProviderBulkImportView(generic.BulkImportView):
queryset = Provider.objects.all()
model_form = forms.ProviderImportForm
@@ -112,7 +124,7 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
@register_model_view(ProviderAccount, 'bulk_import', path='import', detail=False)
class ProviderAccountBulkImportView(generic.BulkImportView):
queryset = ProviderAccount.objects.all()
model_form = forms.ProviderAccountImportForm
@@ -186,7 +198,7 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
@register_model_view(ProviderNetwork, 'bulk_import', path='import', detail=False)
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkImportForm
@@ -243,7 +255,7 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_import', detail=False)
@register_model_view(CircuitType, 'bulk_import', path='import', detail=False)
class CircuitTypeBulkImportView(generic.BulkImportView):
queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeImportForm
@@ -299,7 +311,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'bulk_import', detail=False)
@register_model_view(Circuit, 'bulk_import', path='import', detail=False)
class CircuitBulkImportView(generic.BulkImportView):
queryset = Circuit.objects.all()
model_form = forms.CircuitImportForm
@@ -439,7 +451,7 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
@register_model_view(CircuitTermination, 'bulk_import', path='import', detail=False)
class CircuitTerminationBulkImportView(generic.BulkImportView):
queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationImportForm
@@ -500,7 +512,7 @@ class CircuitGroupDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
@register_model_view(CircuitGroup, 'bulk_import', path='import', detail=False)
class CircuitGroupBulkImportView(generic.BulkImportView):
queryset = CircuitGroup.objects.all()
model_form = forms.CircuitGroupImportForm
@@ -550,7 +562,7 @@ class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
@register_model_view(CircuitGroupAssignment, 'bulk_import', path='import', detail=False)
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
queryset = CircuitGroupAssignment.objects.all()
model_form = forms.CircuitGroupAssignmentImportForm
@@ -607,7 +619,7 @@ class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
@register_model_view(VirtualCircuitType, 'bulk_import', path='import', detail=False)
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitType.objects.all()
model_form = forms.VirtualCircuitTypeImportForm

View File

@@ -0,0 +1,17 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_job_object_type_optional'),
]
operations = [
migrations.AlterField(
model_name='job',
name='data',
field=models.JSONField(blank=True, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
]

View File

@@ -1,12 +1,10 @@
# Generated by Django 5.1.6 on 2025-02-26 19:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_job_object_type_optional'),
('core', '0013_job_data_encoder'),
]
operations = [

View File

@@ -1,12 +1,10 @@
# Generated by Django 5.2b1 on 2025-04-03 18:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0013_datasource_sync_interval'),
('core', '0014_datasource_sync_interval'),
]
operations = [

View File

@@ -5,6 +5,7 @@ import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.urls import reverse
@@ -90,8 +91,9 @@ class Job(models.Model):
)
data = models.JSONField(
verbose_name=_('data'),
encoder=DjangoJSONEncoder,
null=True,
blank=True
blank=True,
)
error = models.TextField(
verbose_name=_('error'),

View File

@@ -49,6 +49,7 @@ class Plugin:
The representation of a NetBox plugin in the catalog API.
"""
id: str = ''
icon_url: str = ''
status: str = ''
title_short: str = ''
title_long: str = ''
@@ -210,6 +211,7 @@ def get_catalog_plugins():
# Populate plugin data
plugins[data['config_name']] = Plugin(
id=data['id'],
icon_url=data['icon'],
status=data['status'],
title_short=data['title_short'],
title_long=data['title_long'],

View File

@@ -2,7 +2,7 @@ import logging
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models.fields.reverse_related import ManyToManyRel
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal
from django.utils.translation import gettext_lazy as _
@@ -145,8 +145,10 @@ def handle_deleted_object(sender, instance, **kwargs):
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
# the association. This triggers an m2m_changed signal with the `post_remove` action type
# for the forward direction of the relationship, ensuring that the change is recorded.
# Similarly, for many-to-one relationships, we set the value on the related object to None
# and save it to trigger a change record on that object.
for relation in instance._meta.related_objects:
if type(relation) is not ManyToManyRel:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
@@ -156,7 +158,11 @@ def handle_deleted_object(sender, instance, **kwargs):
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
getattr(obj, related_field_name).remove(instance)
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.field.null is True:
setattr(obj, related_field_name, None)
obj.save()
# Enqueue the object for event processing
queue = events_queue.get()

View File

@@ -12,6 +12,12 @@ __all__ = (
)
PLUGIN_NAME_TEMPLATE = """
<img class="plugin-icon" src="{{ record.icon_url }}">
<a href="{% url 'core:plugin' record.config_name %}">{{ record.title_long }}</a>
"""
class PluginVersionTable(BaseTable):
version = tables.Column(
verbose_name=_('Version')
@@ -42,8 +48,9 @@ class PluginVersionTable(BaseTable):
class CatalogPluginTable(BaseTable):
title_long = tables.Column(
verbose_name=_('Name'),
title_long = columns.TemplateColumn(
template_code=PLUGIN_NAME_TEMPLATE,
verbose_name=_('Name')
)
author = tables.Column(
accessor=tables.A('author__name'),

View File

@@ -103,7 +103,7 @@ class DataSourceDeleteView(generic.ObjectDeleteView):
queryset = DataSource.objects.all()
@register_model_view(DataSource, 'bulk_import', detail=False)
@register_model_view(DataSource, 'bulk_import', path='import', detail=False)
class DataSourceBulkImportView(generic.BulkImportView):
queryset = DataSource.objects.all()
model_form = forms.DataSourceImportForm

View File

@@ -1390,10 +1390,75 @@ class ModuleFilterSet(NetBoxModelFilterSet):
lookup_expr='in',
label=_('Module bay (ID)'),
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site name (slug)'),
)
location_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__location',
queryset=Location.objects.all(),
label=_('Location (ID)'),
)
location = django_filters.ModelMultipleChoiceFilter(
field_name='device__location__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label=_('Location (slug)'),
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack',
queryset=Rack.objects.all(),
label=_('Rack (ID)'),
)
rack = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack__name',
queryset=Rack.objects.all(),
to_field_name='name',
label=_('Rack (name)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label=_('Device (ID)'),
)
device = django_filters.ModelMultipleChoiceFilter(
field_name='device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Device (name)'),
)
status = django_filters.MultipleChoiceFilter(
choices=ModuleStatusChoices,
null_value=None

View File

@@ -121,11 +121,11 @@ class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
class InventoryItemBulkCreateForm(
form_from_model(InventoryItem, ['role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
form_from_model(InventoryItem, ['status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered']),
DeviceBulkAddComponentForm
):
model = InventoryItem
field_order = (
'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
)

View File

@@ -41,7 +41,6 @@ class InterfaceCommonForm(forms.Form):
def clean(self):
super().clean()
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
if 'tagged_vlans' in self.fields.keys():
tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
@@ -61,6 +60,12 @@ class InterfaceCommonForm(forms.Form):
"or they must be global"
).format(vlans=', '.join(invalid_vlans))
})
# Validate mode change
if self.instance.pk and (self.instance.mode != self.cleaned_data['mode']):
if 'untagged_vlan' not in self.cleaned_data and self.instance.untagged_vlan is not None:
self.instance.untagged_vlan = None
if 'tagged_vlans' not in self.cleaned_data and self.instance.tagged_vlans is not None:
self.instance.tagged_vlans.clear()
class ModuleCommonForm(forms.Form):

View File

@@ -959,8 +959,56 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
model = Module
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
'rack_id': '$rack_id',
},
label=_('Device')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
},
label=_('Location')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
label=_('Rack'),
null_option='None',
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
}
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,

View File

@@ -993,7 +993,7 @@ class ComponentTemplateForm(forms.ModelForm):
class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(),
queryset=DeviceType.objects.all(),
required=False,
context={
'parent': 'manufacturer',
@@ -1008,6 +1008,16 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
}
)
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'description'
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1024,10 +1034,6 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsolePortTemplate
fields = [
@@ -1036,10 +1042,6 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
@@ -1050,7 +1052,11 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
class PowerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
),
)
@@ -1072,7 +1078,13 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
)
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
),
)
class Meta:
@@ -1095,7 +1107,11 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('rf_role', name=_('Wireless')),
@@ -1122,8 +1138,11 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
),
)
@@ -1137,7 +1156,13 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'positions', 'description',
),
)
class Meta:
@@ -1149,7 +1174,13 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
class ModuleBayTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'position', 'description',
),
)
class Meta:

View File

@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import *
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from utilities.forms.rendering import FieldSet
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import APISelect
from . import model_forms
@@ -118,7 +118,13 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
fieldsets = (
FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'description',
),
)
class Meta(model_forms.FrontPortTemplateForm.Meta):

View File

@@ -159,7 +159,7 @@ class ModuleBayTemplateImportForm(forms.ModelForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
'device_type', 'module_type', 'name', 'label', 'position', 'description',
]

View File

@@ -3,7 +3,6 @@ import itertools
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.dispatch import Signal
from django.utils.translation import gettext_lazy as _
@@ -774,9 +773,28 @@ class CablePath(models.Model):
Return a tuple containing the sum of the length of each cable in the path
and a flag indicating whether the length is definitive.
"""
cable_ct = ObjectType.objects.get_for_model(Cable).pk
# Pre-cache cable lengths by ID
cable_ids = self.get_cable_ids()
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
cables = {
cable['pk']: cable['_abs_length']
for cable in Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False).values('pk', '_abs_length')
}
# Iterate through each set of nodes in the path. For cables, add the length of the longest cable to the total
# length of the path.
total_length = 0
for node_set in self.path:
hop_length = 0
for node in node_set:
ct, pk = decompile_path_node(node)
if ct != cable_ct:
break # Not a cable
if pk in cables and cables[pk] > hop_length:
hop_length = cables[pk]
total_length += hop_length
is_definitive = len(cables) == len(cable_ids)
return total_length, is_definitive

View File

@@ -732,3 +732,8 @@ class RackReservation(PrimaryModel):
@property
def unit_list(self):
return array_to_string(self.units)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.rack
return objectchange

View File

@@ -2859,15 +2859,23 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
addresses = IPAddress.objects.filter(address__family=4)
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip4': [str(addresses[0].address), str(addresses[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip4_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
params = {'primary_ip4': [str(addresses[2].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
def test_primary_ip6(self):
addresses = IPAddress.objects.filter(address__family=6)
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip6': [str(addresses[0].address), str(addresses[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip6_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
params = {'primary_ip6': [str(addresses[2].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
def test_virtual_chassis_id(self):
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
@@ -2961,6 +2969,29 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for group in groups:
group.save()
sites = Site.objects.bulk_create((
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
Site(name='Site X', slug='site-x'),
))
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
@@ -2968,11 +2999,66 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Manufacturer.objects.bulk_create(manufacturers)
devices = (
create_test_device('Test Device 1'),
create_test_device('Test Device 2'),
create_test_device('Test Device 3'),
device_types = (
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturers[1], model='Device Type 2', slug='device-type-2'),
DeviceType(manufacturer=manufacturers[2], model='Device Type 3', slug='device-type-3'),
)
DeviceType.objects.bulk_create(device_types)
roles = (
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
)
for role in roles:
role.save()
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(
name='Test Device 1',
device_type=device_types[0],
role=roles[0],
site=sites[0],
location=locations[0],
rack=racks[0],
status='active',
),
Device(
name='Test Device 2',
device_type=device_types[1],
role=roles[1],
site=sites[1],
location=locations[1],
rack=racks[1],
status='planned',
),
Device(
name='Test Device 3',
device_type=device_types[2],
role=roles[2],
site=sites[2],
location=locations[2],
rack=racks[2],
status='offline',
),
)
Device.objects.bulk_create(devices)
module_types = (
ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
@@ -3120,6 +3206,41 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'asset_tag': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'rack': [racks[0].name, racks[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePort.objects.all()
@@ -6722,15 +6843,23 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
addresses = IPAddress.objects.filter(address__family=4)
params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip4': [str(addresses[0].address), str(addresses[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip4_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
params = {'primary_ip4': [str(addresses[2].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
def test_primary_ip6(self):
addresses = IPAddress.objects.filter(address__family=6)
params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip6': [str(addresses[0].address), str(addresses[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'primary_ip6_id': [addresses[2].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
params = {'primary_ip6': [str(addresses[2].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):

View File

@@ -1217,6 +1217,13 @@ front-ports:
- name: Front Port 3
type: 8p8c
rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
position: 1
- name: Module Bay 2
position: 2
- name: Module Bay 3
position: 3
"""
# Create the manufacturer
@@ -1234,6 +1241,7 @@ front-ports:
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
)
form_data = {
@@ -1288,6 +1296,11 @@ front-ports:
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(module_type.modulebaytemplates.count(), 3)
mb1 = ModuleBayTemplate.objects.first()
self.assertEqual(mb1.name, 'Module Bay 1')
self.assertEqual(mb1.position, '1')
def test_export_objects(self):
url = reverse('dcim:moduletype_list')
self.add_permissions('dcim.view_moduletype')

View File

@@ -22,6 +22,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
from utilities.query import count_related
from utilities.query_functions import CollateAsChar
from utilities.request import safe_for_redirect
from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
@@ -279,7 +280,7 @@ class RegionDeleteView(generic.ObjectDeleteView):
queryset = Region.objects.all()
@register_model_view(Region, 'bulk_import', detail=False)
@register_model_view(Region, 'bulk_import', path='import', detail=False)
class RegionBulkImportView(generic.BulkImportView):
queryset = Region.objects.all()
model_form = forms.RegionImportForm
@@ -405,7 +406,7 @@ class SiteGroupDeleteView(generic.ObjectDeleteView):
queryset = SiteGroup.objects.all()
@register_model_view(SiteGroup, 'bulk_import', detail=False)
@register_model_view(SiteGroup, 'bulk_import', path='import', detail=False)
class SiteGroupBulkImportView(generic.BulkImportView):
queryset = SiteGroup.objects.all()
model_form = forms.SiteGroupImportForm
@@ -496,7 +497,7 @@ class SiteDeleteView(generic.ObjectDeleteView):
queryset = Site.objects.all()
@register_model_view(Site, 'bulk_import', detail=False)
@register_model_view(Site, 'bulk_import', path='import', detail=False)
class SiteBulkImportView(generic.BulkImportView):
queryset = Site.objects.all()
model_form = forms.SiteImportForm
@@ -594,7 +595,7 @@ class LocationDeleteView(generic.ObjectDeleteView):
queryset = Location.objects.all()
@register_model_view(Location, 'bulk_import', detail=False)
@register_model_view(Location, 'bulk_import', path='import', detail=False)
class LocationBulkImportView(generic.BulkImportView):
queryset = Location.objects.all()
model_form = forms.LocationImportForm
@@ -663,7 +664,7 @@ class RackRoleDeleteView(generic.ObjectDeleteView):
queryset = RackRole.objects.all()
@register_model_view(RackRole, 'bulk_import', detail=False)
@register_model_view(RackRole, 'bulk_import', path='import', detail=False)
class RackRoleBulkImportView(generic.BulkImportView):
queryset = RackRole.objects.all()
model_form = forms.RackRoleImportForm
@@ -724,7 +725,7 @@ class RackTypeDeleteView(generic.ObjectDeleteView):
queryset = RackType.objects.all()
@register_model_view(RackType, 'bulk_import', detail=False)
@register_model_view(RackType, 'bulk_import', path='import', detail=False)
class RackTypeBulkImportView(generic.BulkImportView):
queryset = RackType.objects.all()
model_form = forms.RackTypeImportForm
@@ -903,7 +904,7 @@ class RackDeleteView(generic.ObjectDeleteView):
queryset = Rack.objects.all()
@register_model_view(Rack, 'bulk_import', detail=False)
@register_model_view(Rack, 'bulk_import', path='import', detail=False)
class RackBulkImportView(generic.BulkImportView):
queryset = Rack.objects.all()
model_form = forms.RackImportForm
@@ -960,7 +961,7 @@ class RackReservationDeleteView(generic.ObjectDeleteView):
queryset = RackReservation.objects.all()
@register_model_view(RackReservation, 'bulk_import', detail=False)
@register_model_view(RackReservation, 'bulk_import', path='import', detail=False)
class RackReservationImportView(generic.BulkImportView):
queryset = RackReservation.objects.all()
model_form = forms.RackReservationImportForm
@@ -1031,7 +1032,7 @@ class ManufacturerDeleteView(generic.ObjectDeleteView):
queryset = Manufacturer.objects.all()
@register_model_view(Manufacturer, 'bulk_import', detail=False)
@register_model_view(Manufacturer, 'bulk_import', path='import', detail=False)
class ManufacturerBulkImportView(generic.BulkImportView):
queryset = Manufacturer.objects.all()
model_form = forms.ManufacturerImportForm
@@ -1252,7 +1253,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
)
@register_model_view(DeviceType, 'bulk_import', detail=False)
@register_model_view(DeviceType, 'bulk_import', path='import', detail=False)
class DeviceTypeImportView(generic.BulkImportView):
additional_permissions = [
'dcim.add_devicetype',
@@ -1522,7 +1523,7 @@ class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
)
@register_model_view(ModuleType, 'bulk_import', detail=False)
@register_model_view(ModuleType, 'bulk_import', path='import', detail=False)
class ModuleTypeImportView(generic.BulkImportView):
additional_permissions = [
'dcim.add_moduletype',
@@ -1533,6 +1534,7 @@ class ModuleTypeImportView(generic.BulkImportView):
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
]
queryset = ModuleType.objects.all()
model_form = forms.ModuleTypeImportForm
@@ -1544,6 +1546,7 @@ class ModuleTypeImportView(generic.BulkImportView):
'interfaces': forms.InterfaceTemplateImportForm,
'rear-ports': forms.RearPortTemplateImportForm,
'front-ports': forms.FrontPortTemplateImportForm,
'module-bays': forms.ModuleBayTemplateImportForm,
}
def prep_related_object_data(self, parent, data):
@@ -2018,7 +2021,7 @@ class DeviceRoleDeleteView(generic.ObjectDeleteView):
queryset = DeviceRole.objects.all()
@register_model_view(DeviceRole, 'bulk_import', detail=False)
@register_model_view(DeviceRole, 'bulk_import', path='import', detail=False)
class DeviceRoleBulkImportView(generic.BulkImportView):
queryset = DeviceRole.objects.all()
model_form = forms.DeviceRoleImportForm
@@ -2082,7 +2085,7 @@ class PlatformDeleteView(generic.ObjectDeleteView):
queryset = Platform.objects.all()
@register_model_view(Platform, 'bulk_import', detail=False)
@register_model_view(Platform, 'bulk_import', path='import', detail=False)
class PlatformBulkImportView(generic.BulkImportView):
queryset = Platform.objects.all()
model_form = forms.PlatformImportForm
@@ -2365,7 +2368,7 @@ class DeviceVirtualMachinesView(generic.ObjectChildrenView):
return self.child_model.objects.restrict(request.user, 'view').filter(cluster=parent.cluster, device=parent)
@register_model_view(Device, 'bulk_import', detail=False)
@register_model_view(Device, 'bulk_import', path='import', detail=False)
class DeviceBulkImportView(generic.BulkImportView):
queryset = Device.objects.all()
model_form = forms.DeviceImportForm
@@ -2438,7 +2441,7 @@ class ModuleDeleteView(generic.ObjectDeleteView):
queryset = Module.objects.all()
@register_model_view(Module, 'bulk_import', detail=False)
@register_model_view(Module, 'bulk_import', path='import', detail=False)
class ModuleBulkImportView(generic.BulkImportView):
queryset = Module.objects.all()
model_form = forms.ModuleImportForm
@@ -2499,7 +2502,7 @@ class ConsolePortDeleteView(generic.ObjectDeleteView):
queryset = ConsolePort.objects.all()
@register_model_view(ConsolePort, 'bulk_import', detail=False)
@register_model_view(ConsolePort, 'bulk_import', path='import', detail=False)
class ConsolePortBulkImportView(generic.BulkImportView):
queryset = ConsolePort.objects.all()
model_form = forms.ConsolePortImportForm
@@ -2574,7 +2577,7 @@ class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
queryset = ConsoleServerPort.objects.all()
@register_model_view(ConsoleServerPort, 'bulk_import', detail=False)
@register_model_view(ConsoleServerPort, 'bulk_import', path='import', detail=False)
class ConsoleServerPortBulkImportView(generic.BulkImportView):
queryset = ConsoleServerPort.objects.all()
model_form = forms.ConsoleServerPortImportForm
@@ -2649,7 +2652,7 @@ class PowerPortDeleteView(generic.ObjectDeleteView):
queryset = PowerPort.objects.all()
@register_model_view(PowerPort, 'bulk_import', detail=False)
@register_model_view(PowerPort, 'bulk_import', path='import', detail=False)
class PowerPortBulkImportView(generic.BulkImportView):
queryset = PowerPort.objects.all()
model_form = forms.PowerPortImportForm
@@ -2724,7 +2727,7 @@ class PowerOutletDeleteView(generic.ObjectDeleteView):
queryset = PowerOutlet.objects.all()
@register_model_view(PowerOutlet, 'bulk_import', detail=False)
@register_model_view(PowerOutlet, 'bulk_import', path='import', detail=False)
class PowerOutletBulkImportView(generic.BulkImportView):
queryset = PowerOutlet.objects.all()
model_form = forms.PowerOutletImportForm
@@ -2856,7 +2859,7 @@ class InterfaceDeleteView(generic.ObjectDeleteView):
queryset = Interface.objects.all()
@register_model_view(Interface, 'bulk_import', detail=False)
@register_model_view(Interface, 'bulk_import', path='import', detail=False)
class InterfaceBulkImportView(generic.BulkImportView):
queryset = Interface.objects.all()
model_form = forms.InterfaceImportForm
@@ -2942,7 +2945,7 @@ class FrontPortDeleteView(generic.ObjectDeleteView):
queryset = FrontPort.objects.all()
@register_model_view(FrontPort, 'bulk_import', detail=False)
@register_model_view(FrontPort, 'bulk_import', path='import', detail=False)
class FrontPortBulkImportView(generic.BulkImportView):
queryset = FrontPort.objects.all()
model_form = forms.FrontPortImportForm
@@ -3017,7 +3020,7 @@ class RearPortDeleteView(generic.ObjectDeleteView):
queryset = RearPort.objects.all()
@register_model_view(RearPort, 'bulk_import', detail=False)
@register_model_view(RearPort, 'bulk_import', path='import', detail=False)
class RearPortBulkImportView(generic.BulkImportView):
queryset = RearPort.objects.all()
model_form = forms.RearPortImportForm
@@ -3092,7 +3095,7 @@ class ModuleBayDeleteView(generic.ObjectDeleteView):
queryset = ModuleBay.objects.all()
@register_model_view(ModuleBay, 'bulk_import', detail=False)
@register_model_view(ModuleBay, 'bulk_import', path='import', detail=False)
class ModuleBayBulkImportView(generic.BulkImportView):
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayImportForm
@@ -3239,7 +3242,7 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
})
@register_model_view(DeviceBay, 'bulk_import', detail=False)
@register_model_view(DeviceBay, 'bulk_import', path='import', detail=False)
class DeviceBayBulkImportView(generic.BulkImportView):
queryset = DeviceBay.objects.all()
model_form = forms.DeviceBayImportForm
@@ -3305,7 +3308,7 @@ class InventoryItemDeleteView(generic.ObjectDeleteView):
queryset = InventoryItem.objects.all()
@register_model_view(InventoryItem, 'bulk_import', detail=False)
@register_model_view(InventoryItem, 'bulk_import', path='import', detail=False)
class InventoryItemBulkImportView(generic.BulkImportView):
queryset = InventoryItem.objects.all()
model_form = forms.InventoryItemImportForm
@@ -3386,7 +3389,7 @@ class InventoryItemRoleDeleteView(generic.ObjectDeleteView):
queryset = InventoryItemRole.objects.all()
@register_model_view(InventoryItemRole, 'bulk_import', detail=False)
@register_model_view(InventoryItemRole, 'bulk_import', path='import', detail=False)
class InventoryItemRoleBulkImportView(generic.BulkImportView):
queryset = InventoryItemRole.objects.all()
model_form = forms.InventoryItemRoleImportForm
@@ -3582,7 +3585,7 @@ class CableDeleteView(generic.ObjectDeleteView):
queryset = Cable.objects.all()
@register_model_view(Cable, 'bulk_import', detail=False)
@register_model_view(Cable, 'bulk_import', path='import', detail=False)
class CableBulkImportView(generic.BulkImportView):
queryset = Cable.objects.all()
model_form = forms.CableImportForm
@@ -3811,7 +3814,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
)
))
if '_addanother' in request.POST:
if '_addanother' in request.POST and safe_for_redirect(request.get_full_path()):
return redirect(request.get_full_path())
return redirect(self.get_return_url(request, device))
@@ -3883,7 +3886,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
})
@register_model_view(VirtualChassis, 'bulk_import', detail=False)
@register_model_view(VirtualChassis, 'bulk_import', path='import', detail=False)
class VirtualChassisBulkImportView(generic.BulkImportView):
queryset = VirtualChassis.objects.all()
model_form = forms.VirtualChassisImportForm
@@ -3940,7 +3943,7 @@ class PowerPanelDeleteView(generic.ObjectDeleteView):
queryset = PowerPanel.objects.all()
@register_model_view(PowerPanel, 'bulk_import', detail=False)
@register_model_view(PowerPanel, 'bulk_import', path='import', detail=False)
class PowerPanelBulkImportView(generic.BulkImportView):
queryset = PowerPanel.objects.all()
model_form = forms.PowerPanelImportForm
@@ -3992,7 +3995,7 @@ class PowerFeedDeleteView(generic.ObjectDeleteView):
queryset = PowerFeed.objects.all()
@register_model_view(PowerFeed, 'bulk_import', detail=False)
@register_model_view(PowerFeed, 'bulk_import', path='import', detail=False)
class PowerFeedBulkImportView(generic.BulkImportView):
queryset = PowerFeed.objects.all()
model_form = forms.PowerFeedImportForm
@@ -4064,7 +4067,7 @@ class VirtualDeviceContextDeleteView(generic.ObjectDeleteView):
queryset = VirtualDeviceContext.objects.all()
@register_model_view(VirtualDeviceContext, 'bulk_import', detail=False)
@register_model_view(VirtualDeviceContext, 'bulk_import', path='import', detail=False)
class VirtualDeviceContextBulkImportView(generic.BulkImportView):
queryset = VirtualDeviceContext.objects.all()
model_form = forms.VirtualDeviceContextImportForm
@@ -4114,7 +4117,7 @@ class MACAddressDeleteView(generic.ObjectDeleteView):
queryset = MACAddress.objects.all()
@register_model_view(MACAddress, 'bulk_import', detail=False)
@register_model_view(MACAddress, 'bulk_import', path='import', detail=False)
class MACAddressBulkImportView(generic.BulkImportView):
queryset = MACAddress.objects.all()
model_form = forms.MACAddressImportForm

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_remove_redundant_indexes'),
('core', '0015_remove_redundant_indexes'),
('extras', '0127_configtemplate_as_attachment_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

View File

@@ -123,7 +123,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
ordered = [
script_objects.pop(sc) for sc in self.module_scripts.keys() if sc in script_objects
]
ordered.extend(script_objects.items())
ordered.extend(script_objects.values())
return ordered
@property

View File

@@ -5,6 +5,8 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from extras.models import *
from core.tables import JobTable
from core.models import Job
from netbox.constants import EMPTY_TABLE_TEXT
from netbox.events import get_event_text
from netbox.tables import BaseTable, NetBoxTable, columns
@@ -26,6 +28,7 @@ __all__ = (
'SavedFilterTable',
'ReportResultsTable',
'ScriptResultsTable',
'ScriptJobTable',
'SubscriptionTable',
'TableConfigTable',
'TaggedItemTable',
@@ -693,6 +696,23 @@ class ScriptResultsTable(BaseTable):
return format_html("<a href='{}'>{}</a>", value, value)
class ScriptJobTable(JobTable):
id = tables.TemplateColumn(
template_code="""<a href="{% url 'extras:script_result' job_pk=record.pk %}">{{ record.id }}</a>""",
verbose_name=_('ID'),
)
class Meta(NetBoxTable.Meta):
model = Job
fields = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
'completed', 'user', 'error', 'job_id',
)
default_columns = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
)
class ReportResultsTable(BaseTable):
index = tables.Column(
verbose_name=_('Line')

View File

@@ -14,7 +14,6 @@ from jinja2.exceptions import TemplateError
from core.choices import ManagedFileRootPathChoices
from core.models import Job
from core.tables import JobTable
from dcim.models import Device, DeviceRole, Platform
from extras.choices import LogLevelChoices
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
@@ -36,7 +35,7 @@ from virtualization.models import VirtualMachine
from . import filtersets, forms, tables
from .constants import LOG_LEVEL_RANK
from .models import *
from .tables import ReportResultsTable, ScriptResultsTable
from .tables import ReportResultsTable, ScriptResultsTable, ScriptJobTable
#
@@ -83,7 +82,7 @@ class CustomFieldDeleteView(generic.ObjectDeleteView):
queryset = CustomField.objects.select_related('choice_set')
@register_model_view(CustomField, 'bulk_import', detail=False)
@register_model_view(CustomField, 'bulk_import', path='import', detail=False)
class CustomFieldBulkImportView(generic.BulkImportView):
queryset = CustomField.objects.select_related('choice_set')
model_form = forms.CustomFieldImportForm
@@ -152,7 +151,7 @@ class CustomFieldChoiceSetDeleteView(generic.ObjectDeleteView):
queryset = CustomFieldChoiceSet.objects.all()
@register_model_view(CustomFieldChoiceSet, 'bulk_import', detail=False)
@register_model_view(CustomFieldChoiceSet, 'bulk_import', path='import', detail=False)
class CustomFieldChoiceSetBulkImportView(generic.BulkImportView):
queryset = CustomFieldChoiceSet.objects.all()
model_form = forms.CustomFieldChoiceSetImportForm
@@ -202,7 +201,7 @@ class CustomLinkDeleteView(generic.ObjectDeleteView):
queryset = CustomLink.objects.all()
@register_model_view(CustomLink, 'bulk_import', detail=False)
@register_model_view(CustomLink, 'bulk_import', path='import', detail=False)
class CustomLinkBulkImportView(generic.BulkImportView):
queryset = CustomLink.objects.all()
model_form = forms.CustomLinkImportForm
@@ -257,7 +256,7 @@ class ExportTemplateDeleteView(generic.ObjectDeleteView):
queryset = ExportTemplate.objects.all()
@register_model_view(ExportTemplate, 'bulk_import', detail=False)
@register_model_view(ExportTemplate, 'bulk_import', path='import', detail=False)
class ExportTemplateBulkImportView(generic.BulkImportView):
queryset = ExportTemplate.objects.all()
model_form = forms.ExportTemplateImportForm
@@ -317,7 +316,7 @@ class SavedFilterDeleteView(SharedObjectViewMixin, generic.ObjectDeleteView):
queryset = SavedFilter.objects.all()
@register_model_view(SavedFilter, 'bulk_import', detail=False)
@register_model_view(SavedFilter, 'bulk_import', path='import', detail=False)
class SavedFilterBulkImportView(SharedObjectViewMixin, generic.BulkImportView):
queryset = SavedFilter.objects.all()
model_form = forms.SavedFilterImportForm
@@ -457,7 +456,7 @@ class NotificationGroupDeleteView(generic.ObjectDeleteView):
queryset = NotificationGroup.objects.all()
@register_model_view(NotificationGroup, 'bulk_import', detail=False)
@register_model_view(NotificationGroup, 'bulk_import', path='import', detail=False)
class NotificationGroupBulkImportView(generic.BulkImportView):
queryset = NotificationGroup.objects.all()
model_form = forms.NotificationGroupImportForm
@@ -603,7 +602,7 @@ class WebhookDeleteView(generic.ObjectDeleteView):
queryset = Webhook.objects.all()
@register_model_view(Webhook, 'bulk_import', detail=False)
@register_model_view(Webhook, 'bulk_import', path='import', detail=False)
class WebhookBulkImportView(generic.BulkImportView):
queryset = Webhook.objects.all()
model_form = forms.WebhookImportForm
@@ -653,7 +652,7 @@ class EventRuleDeleteView(generic.ObjectDeleteView):
queryset = EventRule.objects.all()
@register_model_view(EventRule, 'bulk_import', detail=False)
@register_model_view(EventRule, 'bulk_import', path='import', detail=False)
class EventRuleBulkImportView(generic.BulkImportView):
queryset = EventRule.objects.all()
model_form = forms.EventRuleImportForm
@@ -726,7 +725,7 @@ class TagDeleteView(generic.ObjectDeleteView):
queryset = Tag.objects.all()
@register_model_view(Tag, 'bulk_import', detail=False)
@register_model_view(Tag, 'bulk_import', path='import', detail=False)
class TagBulkImportView(generic.BulkImportView):
queryset = Tag.objects.all()
model_form = forms.TagImportForm
@@ -902,7 +901,7 @@ class ConfigTemplateDeleteView(generic.ObjectDeleteView):
queryset = ConfigTemplate.objects.all()
@register_model_view(ConfigTemplate, 'bulk_import', detail=False)
@register_model_view(ConfigTemplate, 'bulk_import', path='import', detail=False)
class ConfigTemplateBulkImportView(generic.BulkImportView):
queryset = ConfigTemplate.objects.all()
model_form = forms.ConfigTemplateImportForm
@@ -1081,7 +1080,7 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
return reverse(viewname, kwargs={'pk': obj.pk})
@register_model_view(JournalEntry, 'bulk_import', detail=False)
@register_model_view(JournalEntry, 'bulk_import', path='import', detail=False)
class JournalEntryBulkImportView(generic.BulkImportView):
queryset = JournalEntry.objects.all()
model_form = forms.JournalEntryImportForm
@@ -1393,7 +1392,7 @@ class ScriptJobsView(BaseScriptView):
def get(self, request, **kwargs):
script = self.get_object(**kwargs)
jobs_table = JobTable(
jobs_table = ScriptJobTable(
data=script.jobs.all(),
orderable=False,
user=request.user

View File

@@ -1256,8 +1256,20 @@ class PrimaryIPFilterSet(django_filters.FilterSet):
queryset=IPAddress.objects.all(),
label=_('Primary IPv4 (ID)'),
)
primary_ip4 = django_filters.ModelMultipleChoiceFilter(
field_name='primary_ip4__address',
queryset=IPAddress.objects.all(),
to_field_name='address',
label=_('Primary IPv4 (address)'),
)
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
field_name='primary_ip6',
queryset=IPAddress.objects.all(),
label=_('Primary IPv6 (ID)'),
)
primary_ip6 = django_filters.ModelMultipleChoiceFilter(
field_name='primary_ip6__address',
queryset=IPAddress.objects.all(),
to_field_name='address',
label=_('Primary IPv6 (address)'),
)

View File

@@ -864,6 +864,7 @@ class ServiceCreateForm(ServiceForm):
# Fields which may be populated from a ServiceTemplate are not required
for field in ('name', 'protocol', 'ports'):
self.fields[field].required = False
self.fields[field].widget.is_required = False
def clean(self):
super().clean()

View File

@@ -1,4 +1,5 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField, IntegerRangeField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
@@ -6,7 +7,7 @@ from django.db import models
from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext_lazy as _
from dcim.models import Interface
from dcim.models import Interface, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
@@ -285,12 +286,20 @@ class VLAN(PrimaryModel):
super().clean()
# Validate VLAN group (if assigned)
if self.group and self.site and self.group.scope != self.site:
raise ValidationError(
_(
"VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
).format(group=self.group, scope=self.group.scope, site=self.site)
)
if self.group and self.site and self.group.scope_type == ContentType.objects.get_for_model(Site):
if self.site != self.group.scope:
raise ValidationError(
_(
"VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
).format(group=self.group, scope=self.group.scope, site=self.site)
)
if self.group and self.site and self.group.scope_type == ContentType.objects.get_for_model(SiteGroup):
if self.site not in self.group.scope.sites.all():
raise ValidationError(
_(
"The assigned site {site} is not a member of the assigned group {group} (scope: {scope})."
).format(group=self.group, scope=self.group.scope, site=self.site)
)
# Check that the VLAN ID is permitted in the assigned group (if any)
if self.group:

View File

@@ -1,8 +1,10 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from netaddr import IPNetwork, IPSet
from utilities.data import string_to_ranges
from dcim.models import Site, SiteGroup
from ipam.choices import *
from ipam.models import *
@@ -679,3 +681,54 @@ class TestVLAN(TestCase):
)
with self.assertRaises(ValidationError):
vlan.full_clean()
def test_vlan_group_site_validation(self):
sitegroup = SiteGroup.objects.create(
name='Site Group 1',
slug='site-group-1',
)
sites = Site.objects.bulk_create((
Site(
name='Site 1',
slug='site-1',
),
Site(
name='Site 2',
slug='site-2',
),
))
sitegroup.sites.add(sites[0])
vlangroups = VLANGroup.objects.bulk_create((
VLANGroup(
name='VLAN Group 1',
slug='vlan-group-1',
scope=sitegroup,
scope_type=ContentType.objects.get_for_model(SiteGroup),
),
VLANGroup(
name='VLAN Group 2',
slug='vlan-group-2',
scope=sites[0],
scope_type=ContentType.objects.get_for_model(Site),
),
VLANGroup(
name='VLAN Group 2',
slug='vlan-group-2',
scope=sites[1],
scope_type=ContentType.objects.get_for_model(Site),
),
))
vlan = VLAN(
name='VLAN 1',
vid=1,
group=vlangroups[0],
site=sites[0],
)
# VLAN Group 1 and 2 should be valid
vlan.full_clean()
vlan.group = vlangroups[1]
vlan.full_clean()
vlan.group = vlangroups[2]
with self.assertRaises(ValidationError):
vlan.full_clean()

View File

@@ -69,7 +69,7 @@ class VRFDeleteView(generic.ObjectDeleteView):
queryset = VRF.objects.all()
@register_model_view(VRF, 'bulk_import', detail=False)
@register_model_view(VRF, 'bulk_import', path='import', detail=False)
class VRFBulkImportView(generic.BulkImportView):
queryset = VRF.objects.all()
model_form = forms.VRFImportForm
@@ -119,7 +119,7 @@ class RouteTargetDeleteView(generic.ObjectDeleteView):
queryset = RouteTarget.objects.all()
@register_model_view(RouteTarget, 'bulk_import', detail=False)
@register_model_view(RouteTarget, 'bulk_import', path='import', detail=False)
class RouteTargetBulkImportView(generic.BulkImportView):
queryset = RouteTarget.objects.all()
model_form = forms.RouteTargetImportForm
@@ -176,7 +176,7 @@ class RIRDeleteView(generic.ObjectDeleteView):
queryset = RIR.objects.all()
@register_model_view(RIR, 'bulk_import', detail=False)
@register_model_view(RIR, 'bulk_import', path='import', detail=False)
class RIRBulkImportView(generic.BulkImportView):
queryset = RIR.objects.all()
model_form = forms.RIRImportForm
@@ -251,7 +251,7 @@ class ASNRangeDeleteView(generic.ObjectDeleteView):
queryset = ASNRange.objects.all()
@register_model_view(ASNRange, 'bulk_import', detail=False)
@register_model_view(ASNRange, 'bulk_import', path='import', detail=False)
class ASNRangeBulkImportView(generic.BulkImportView):
queryset = ASNRange.objects.all()
model_form = forms.ASNRangeImportForm
@@ -316,7 +316,7 @@ class ASNDeleteView(generic.ObjectDeleteView):
queryset = ASN.objects.all()
@register_model_view(ASN, 'bulk_import', detail=False)
@register_model_view(ASN, 'bulk_import', path='import', detail=False)
class ASNBulkImportView(generic.BulkImportView):
queryset = ASN.objects.all()
model_form = forms.ASNImportForm
@@ -408,7 +408,7 @@ class AggregateDeleteView(generic.ObjectDeleteView):
queryset = Aggregate.objects.all()
@register_model_view(Aggregate, 'bulk_import', detail=False)
@register_model_view(Aggregate, 'bulk_import', path='import', detail=False)
class AggregateBulkImportView(generic.BulkImportView):
queryset = Aggregate.objects.all()
model_form = forms.AggregateImportForm
@@ -471,7 +471,7 @@ class RoleDeleteView(generic.ObjectDeleteView):
queryset = Role.objects.all()
@register_model_view(Role, 'bulk_import', detail=False)
@register_model_view(Role, 'bulk_import', path='import', detail=False)
class RoleBulkImportView(generic.BulkImportView):
queryset = Role.objects.all()
model_form = forms.RoleImportForm
@@ -657,7 +657,7 @@ class PrefixDeleteView(generic.ObjectDeleteView):
queryset = Prefix.objects.all()
@register_model_view(Prefix, 'bulk_import', detail=False)
@register_model_view(Prefix, 'bulk_import', path='import', detail=False)
class PrefixBulkImportView(generic.BulkImportView):
queryset = Prefix.objects.all()
model_form = forms.PrefixImportForm
@@ -746,7 +746,7 @@ class IPRangeDeleteView(generic.ObjectDeleteView):
queryset = IPRange.objects.all()
@register_model_view(IPRange, 'bulk_import', detail=False)
@register_model_view(IPRange, 'bulk_import', path='import', detail=False)
class IPRangeBulkImportView(generic.BulkImportView):
queryset = IPRange.objects.all()
model_form = forms.IPRangeImportForm
@@ -910,7 +910,7 @@ class IPAddressBulkCreateView(generic.BulkCreateView):
template_name = 'ipam/ipaddress_bulk_add.html'
@register_model_view(IPAddress, 'bulk_import', detail=False)
@register_model_view(IPAddress, 'bulk_import', path='import', detail=False)
class IPAddressBulkImportView(generic.BulkImportView):
queryset = IPAddress.objects.all()
model_form = forms.IPAddressImportForm
@@ -983,7 +983,7 @@ class VLANGroupDeleteView(generic.ObjectDeleteView):
queryset = VLANGroup.objects.all()
@register_model_view(VLANGroup, 'bulk_import', detail=False)
@register_model_view(VLANGroup, 'bulk_import', path='import', detail=False)
class VLANGroupBulkImportView(generic.BulkImportView):
queryset = VLANGroup.objects.all()
model_form = forms.VLANGroupImportForm
@@ -1070,7 +1070,7 @@ class VLANTranslationPolicyDeleteView(generic.ObjectDeleteView):
queryset = VLANTranslationPolicy.objects.all()
@register_model_view(VLANTranslationPolicy, 'bulk_import', detail=False)
@register_model_view(VLANTranslationPolicy, 'bulk_import', path='import', detail=False)
class VLANTranslationPolicyBulkImportView(generic.BulkImportView):
queryset = VLANTranslationPolicy.objects.all()
model_form = forms.VLANTranslationPolicyImportForm
@@ -1125,7 +1125,7 @@ class VLANTranslationRuleDeleteView(generic.ObjectDeleteView):
queryset = VLANTranslationRule.objects.all()
@register_model_view(VLANTranslationRule, 'bulk_import', detail=False)
@register_model_view(VLANTranslationRule, 'bulk_import', path='import', detail=False)
class VLANTranslationRuleBulkImportView(generic.BulkImportView):
queryset = VLANTranslationRule.objects.all()
model_form = forms.VLANTranslationRuleImportForm
@@ -1218,7 +1218,7 @@ class FHRPGroupDeleteView(generic.ObjectDeleteView):
queryset = FHRPGroup.objects.all()
@register_model_view(FHRPGroup, 'bulk_import', detail=False)
@register_model_view(FHRPGroup, 'bulk_import', path='import', detail=False)
class FHRPGroupBulkImportView(generic.BulkImportView):
queryset = FHRPGroup.objects.all()
model_form = forms.FHRPGroupImportForm
@@ -1344,7 +1344,7 @@ class VLANDeleteView(generic.ObjectDeleteView):
queryset = VLAN.objects.all()
@register_model_view(VLAN, 'bulk_import', detail=False)
@register_model_view(VLAN, 'bulk_import', path='import', detail=False)
class VLANBulkImportView(generic.BulkImportView):
queryset = VLAN.objects.all()
model_form = forms.VLANImportForm
@@ -1394,7 +1394,7 @@ class ServiceTemplateDeleteView(generic.ObjectDeleteView):
queryset = ServiceTemplate.objects.all()
@register_model_view(ServiceTemplate, 'bulk_import', detail=False)
@register_model_view(ServiceTemplate, 'bulk_import', path='import', detail=False)
class ServiceTemplateBulkImportView(generic.BulkImportView):
queryset = ServiceTemplate.objects.all()
model_form = forms.ServiceTemplateImportForm
@@ -1461,7 +1461,7 @@ class ServiceDeleteView(generic.ObjectDeleteView):
queryset = Service.objects.all()
@register_model_view(Service, 'bulk_import', detail=False)
@register_model_view(Service, 'bulk_import', path='import', detail=False)
class ServiceBulkImportView(generic.BulkImportView):
queryset = Service.objects.all()
model_form = forms.ServiceImportForm

View File

@@ -442,7 +442,7 @@ INSTALLED_APPS = [
'drf_spectacular',
'drf_spectacular_sidecar',
]
if not DEBUG:
if not DEBUG and 'collectstatic' not in sys.argv:
INSTALLED_APPS.remove('debug_toolbar')
# Middleware

View File

@@ -29,6 +29,7 @@ from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial
from utilities.permissions import get_permission_for_model
from utilities.query import reapply_model_ordering
from utilities.request import safe_for_redirect
from utilities.tables import get_table_configs
from utilities.views import GetReturnURLMixin, get_viewname
from .base import BaseMultiObjectView
@@ -121,7 +122,10 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Strip the `export` param and redirect user to the filtered objects list
query_params = request.GET.copy()
query_params.pop('export')
return redirect(f'{request.path}?{query_params.urlencode()}')
redirect_url = f'{request.path}?{query_params.urlencode()}'
if safe_for_redirect(redirect_url):
return redirect(redirect_url)
return redirect(get_viewname(self.queryset.model, 'list'))
#
# Request handlers
@@ -286,7 +290,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
logger.info(msg)
messages.success(request, msg)
if '_addanother' in request.POST:
if '_addanother' in request.POST and safe_for_redirect(request.path):
return redirect(request.path)
return redirect(self.get_return_url(request))

View File

@@ -20,6 +20,7 @@ from utilities.forms import ConfirmationForm, restrict_form_fields
from utilities.htmx import htmx_partial
from utilities.permissions import get_permission_for_model
from utilities.querydict import normalize_querydict, prepare_cloned_fields
from utilities.request import safe_for_redirect
from utilities.tables import get_table_configs
from utilities.views import GetReturnURLMixin, get_viewname
from .base import BaseObjectView
@@ -317,6 +318,8 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
if 'return_url' in request.GET:
params['return_url'] = request.GET.get('return_url')
redirect_url += f"?{params.urlencode()}"
if not safe_for_redirect(redirect_url):
redirect_url = reverse('home')
return redirect(redirect_url)
@@ -583,7 +586,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
))
# Redirect user on success
if '_addanother' in request.POST:
if '_addanother' in request.POST and safe_for_redirect(request.get_full_path()):
return redirect(request.get_full_path())
else:
return redirect(self.get_return_url(request))

File diff suppressed because one or more lines are too long

View File

@@ -63,3 +63,27 @@ span.color-label {
.sso-icon {
height: 24px;
}
.btn-white {
@extend .btn-light;
}
.btn-black {
@extend .btn-dark;
}
.btn-grey, .btn-gray {
@extend .btn-secondary;
}
img.plugin-icon {
max-width: 1.4285em;
height: auto;
}
body[data-bs-theme=dark] {
// Assuming icon is black/white line art, invert it and tone down brightness
img.plugin-icon {
filter: grayscale(100%) invert(100%) brightness(80%);
}
}

View File

@@ -1,11 +1,6 @@
// Navbar and light theme styling
.navbar-vertical.navbar-expand-lg {
// Adds spacing to the bottom of the side navigation to avoid hidden nav items
@include media-breakpoint-up(lg) {
padding-bottom: 2rem;
}
// Adjust hover color & style for menu items
.navbar-collapse {
.nav-link-icon, .nav-link-title {

View File

@@ -21,7 +21,7 @@ Blocks:
{# Sidebar #}
<aside class="navbar navbar-vertical navbar-expand-lg d-print-none">
{% if 'commercial' in settings.RELEASE.features %}
{% if settings.RELEASE.features.commercial %}
<img class="motif" src="{% static 'motif.svg' %}" alt="{% trans "NetBox Motif" %}">
{% endif %}
@@ -51,8 +51,19 @@ Blocks:
{# Navigation menu #}
<div class="collapse navbar-collapse" id="sidebar-menu">
{% nav %}
</div>
{# Release info #}
<div class="text-muted text-center fs-5 my-3">
{{ settings.RELEASE.name }}
{% if not settings.RELEASE.features.commercial %}
<div>
<a href="https://netboxlabs.com/netbox-cloud/" class="text-muted">{% trans "Get" %} Cloud</a> |
<a href="https://netboxlabs.com/netbox-enterprise/" class="text-muted">{% trans "Get" %} Enterprise</a>
</div>
{% endif %}
</div>
</div>
</div>
</aside>
@@ -210,7 +221,6 @@ Blocks:
<ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
<li class="list-inline-item">{% now 'Y-m-d H:i:s T' %}</li>
<li class="list-inline-item">{{ settings.HOSTNAME }}</li>
<li class="list-inline-item">{{ settings.RELEASE.name }}</li>
</ul>
{# /Footer text #}

View File

@@ -10,7 +10,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit" %}</h2>
<table class="table table-hover attr-table">
@@ -89,7 +89,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% include 'inc/panels/image_attachments.html' %}

View File

@@ -20,7 +20,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit Group" %}</h2>
<table class="table table-hover attr-table">
@@ -46,7 +46,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit Group Assignment" %}</h2>
<table class="table table-hover attr-table">
@@ -39,7 +39,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -10,7 +10,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
{% if object %}
@@ -37,7 +37,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit Type" %}</h2>
<table class="table table-hover attr-table">
@@ -41,7 +41,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -15,7 +15,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider" %}</h2>
<table class="table table-hover attr-table">
@@ -39,7 +39,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider Account" %}</h2>
<table class="table table-hover attr-table">
@@ -33,7 +33,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider Network" %}</h2>
<table class="table table-hover attr-table">
@@ -37,7 +37,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}

View File

@@ -15,7 +15,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual circuit" %}</h2>
<table class="table table-hover attr-table">
@@ -61,7 +61,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
<div class="card">

View File

@@ -18,7 +18,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit Termination" %}</h2>
<table class="table table-hover attr-table">
@@ -48,7 +48,7 @@
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Interface" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit Type" %}</h2>
<table class="table table-hover attr-table">
@@ -41,7 +41,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -26,7 +26,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Data Source" %}</h2>
<table class="table table-hover attr-table">
@@ -83,7 +83,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Backend" %}</h2>
{% with backend=object.backend_class %}

View File

@@ -30,7 +30,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Job" %}</h2>
<table class="table table-hover attr-table">
@@ -61,7 +61,7 @@
</table>
</div>
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Scheduling" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -24,7 +24,7 @@
{% block content %}
<div class="row">
<div class="col col-md-5">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Change" %}</h2>
<table class="table table-hover attr-table">
@@ -73,7 +73,7 @@
</table>
</div>
</div>
<div class="col col-md-7">
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
@@ -106,7 +106,7 @@
</div>
</div>
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
@@ -126,7 +126,7 @@
</div>
</div>
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
@@ -146,10 +146,10 @@
</div>
</div>
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% plugin_right_page object %}
</div>
</div>

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Cable" %}</h2>
<table class="table table-hover attr-table">
@@ -63,7 +63,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Termination" %} A</h2>
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}

View File

@@ -63,7 +63,7 @@
<td>
{% if total_length %}
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Meters" %} /
{{ total_length|meters_to_feet|floatformat:"-2" }} {% trans "Feet" %}
{{ total_length|meters_to_feet|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Feet" %}
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Console Port" %}</h2>
<table class="table table-hover attr-table">
@@ -50,7 +50,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Console Server Port" %}</h2>
<table class="table table-hover attr-table">
@@ -50,7 +50,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
<div class="card-body">

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Device Bay" %}</h2>
<table class="table table-hover attr-table">
@@ -38,7 +38,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Installed Device" %}</h2>
{% if object.installed_device %}

View File

@@ -18,7 +18,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Device Role" %}</h2>
<table class="table table-hover attr-table">
@@ -53,7 +53,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Chassis" %}</h2>
<table class="table table-hover attr-table">
@@ -96,7 +96,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Front Port" %}</h2>
<table class="table table-hover attr-table">
@@ -64,7 +64,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -22,7 +22,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Interface" %}</h2>
<table class="table table-hover attr-table">
@@ -122,7 +122,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=vdc_table heading="Virtual Device Contexts" %}
<div class="card">
<h2 class="card-header">{% trans "Addressing" %}</h2>

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Inventory Item" %}</h2>
<table class="table table-hover attr-table">
@@ -70,7 +70,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% plugin_right_page object %}
</div>
</div>

View File

@@ -10,7 +10,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Inventory Item Role" %}</h2>
<table class="table table-hover attr-table">
@@ -39,7 +39,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -21,7 +21,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Location" %}</h2>
<table class="table table-hover attr-table">
@@ -65,7 +65,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "MAC Address" %}</h2>
<table class="table table-hover attr-table">
@@ -42,7 +42,7 @@
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -28,7 +28,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Manufacturer" %}</h2>
<table class="table table-hover attr-table">
@@ -45,7 +45,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -49,7 +49,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module" %}</h2>
<table class="table table-hover attr-table">
@@ -87,7 +87,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Type" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Bay" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
<div class="card">
<h2 class="card-header">{% trans "Installed Module" %}</h2>

View File

@@ -19,7 +19,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Type" %}</h2>
<table class="table table-hover attr-table">
@@ -63,7 +63,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Attributes" %}</h2>
{% if not object.profile %}

View File

@@ -21,7 +21,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Platform" %}</h2>
<table class="table table-hover attr-table">
@@ -46,7 +46,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -16,7 +16,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Feed" %}</h2>
<table class="table table-hover attr-table">
@@ -105,7 +105,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Outlet" %}</h2>
<table class="table table-hover attr-table">
@@ -68,7 +68,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Panel" %}</h2>
<table class="table table-hover attr-table">
@@ -36,7 +36,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/image_attachments.html' %}

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Port" %}</h2>
<table class="table table-hover attr-table">
@@ -54,7 +54,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rack Role" %}</h2>
<table class="table table-hover attr-table">
@@ -37,7 +37,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}

View File

@@ -8,7 +8,7 @@
{% block content %}
<div class="row">
<div class="col col-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rack Type" %}</h2>
<table class="table table-hover attr-table">
@@ -35,7 +35,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-6">
<div class="col col-12 col-md-6">
{% include 'dcim/inc/panels/racktype_numbering.html' %}
<div class="card">
<h2 class="card-header">{% trans "Weight" %}</h2>

View File

@@ -12,7 +12,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Port" %}</h2>
<table class="table table-hover attr-table">
@@ -60,7 +60,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -21,7 +21,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Region" %}</h2>
<table class="table table-hover attr-table">
@@ -44,7 +44,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -23,7 +23,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Site" %}</h2>
<table class="table table-hover attr-table">
@@ -114,7 +114,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}

View File

@@ -21,7 +21,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Site Group" %}</h2>
<table class="table table-hover attr-table">
@@ -44,7 +44,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -15,7 +15,7 @@
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="col col-12 col-md-4">
<div class="card">
<h2 class="card-header">{% trans "Virtual Chassis" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
<div class="col col-12 col-md-8">
<div class="card">
<h2 class="card-header">
{% trans "Members" %}

View File

@@ -10,7 +10,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Device Context" %}</h2>
<table class="table table-hover attr-table">
@@ -68,7 +68,7 @@
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row">
<div class="col col-md-5">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Config Context" %}</h2>
<table class="table table-hover attr-table">
@@ -76,7 +76,7 @@
</table>
</div>
</div>
<div class="col col-md-7">
<div class="col col-12 col-md-7">
{% include 'inc/sync_warning.html' %}
<div class="card">
{% include 'extras/inc/configcontext_data.html' with title="Data" data=object.data format=format copyid="data" %}

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Config Template" %}</h2>
<table class="table table-hover attr-table">
@@ -67,7 +67,7 @@
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Environment Parameters" %}</h2>
<div class="card-body">

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Custom Field" %}</h2>
<table class="table table-hover attr-table">
@@ -100,7 +100,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Object Types" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -4,7 +4,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">Custom Field Choice Set</h2>
<table class="table table-hover attr-table">
@@ -42,7 +42,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">Choices ({{ object.choices|length }})</h2>
<table class="table table-hover">

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-5">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Custom Link" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-7">
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header">{% trans "Link Text" %}</h2>
<div class="card-body">

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Event Rule" %}</h2>
<table class="table table-hover attr-table">
@@ -56,7 +56,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Conditions" %}</h2>
<div class="card-body">

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Export Template" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Notification Group" %}</h2>
<table class="table table-hover attr-table">
@@ -26,7 +26,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Groups" %}</h2>
<div class="list-group list-group-flush">

View File

@@ -5,12 +5,12 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
{% include 'extras/inc/configcontext_data.html' with title="Rendered Context" data=rendered_context format=format copyid="rendered_context" %}
</div>
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
{% include 'extras/inc/configcontext_data.html' with title="Local Context" data=object.local_context_data format=format copyid="local_context" %}
<div class="card-footer">

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-5">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Saved Filter" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-7">
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header">{% trans "Parameters" %}</h2>
<div class="card-body">

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Tag" %}</h2>
<table class="table table-hover attr-table">
@@ -42,7 +42,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Allowed Object Types" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -5,7 +5,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Webhook" %}</h2>
<table class="table table-hover attr-table">
@@ -55,7 +55,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Additional Headers" %}</h2>
<div class="card-body">

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Aggregate" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}

View File

@@ -15,7 +15,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "ASN" %}</h2>
<table class="table table-hover attr-table">
@@ -47,7 +47,7 @@
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "ASN Range" %}</h2>
<table class="table table-hover attr-table">
@@ -43,7 +43,7 @@
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>

View File

@@ -14,7 +14,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "FHRP Group" %}</h2>
<table class="table table-hover attr-table">
@@ -44,7 +44,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Authentication" %}</h2>
<table class="table table-hover attr-table">

View File

@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="col col-12 col-md-4">
<div class="card">
<h2 class="card-header">{% trans "IP Address" %}</h2>
<table class="table table-hover attr-table">

Some files were not shown because too many files have changed in this diff Show More