mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-16 16:52:17 -06:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89d91783b | ||
|
|
a18e1a0161 | ||
|
|
4308b8a4a5 | ||
|
|
aa54e14c37 | ||
|
|
3ffe36e5ed | ||
|
|
3b2c74042e | ||
|
|
11ae938146 | ||
|
|
f11bb254a5 | ||
|
|
0b681c471e | ||
|
|
05d3354570 | ||
|
|
6813787fc7 | ||
|
|
28761fc960 | ||
|
|
e8fd0f3531 | ||
|
|
8103c399d5 | ||
|
|
a51f5edbc8 | ||
|
|
be393a9d10 | ||
|
|
ef59f38ec4 | ||
|
|
47120fae01 | ||
|
|
c0417c1989 | ||
|
|
fb6cfa45fd | ||
|
|
b875cea10d | ||
|
|
32bf17c076 | ||
|
|
66a6a8f33c | ||
|
|
05b71564d8 | ||
|
|
1682de59df | ||
|
|
f26253ec49 | ||
|
|
f2dc287f14 | ||
|
|
3fe3151af7 | ||
|
|
1c1fd8f210 | ||
|
|
3ce2f0d100 | ||
|
|
92d726bbd4 | ||
|
|
e2ef0bc3a6 | ||
|
|
13c29cb7a9 |
@@ -9,9 +9,7 @@ env:
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.4"
|
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install pep8
|
- pip install pep8
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ NetBox does not have the ability to generate graphs natively, but this feature a
|
|||||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||||
|
```
|
||||||
|
|
||||||
|
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||||
|
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||||
|
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||||
|
```
|
||||||
|
|
||||||
# Topology Maps
|
# Topology Maps
|
||||||
|
|
||||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from django.db.models import Q
|
|||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
|
|
||||||
from .models import Provider, Circuit, CircuitType
|
from .models import Provider, Circuit, CircuitType
|
||||||
|
|
||||||
|
|
||||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -42,6 +43,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||||
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
@@ -14,6 +14,7 @@ from .models import (
|
|||||||
|
|
||||||
|
|
||||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -81,6 +82,7 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -157,6 +159,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -191,6 +194,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -405,6 +409,10 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
method='filter_type',
|
method='filter_type',
|
||||||
label='Interface type',
|
label='Interface type',
|
||||||
)
|
)
|
||||||
|
mac_address = django_filters.CharFilter(
|
||||||
|
method='_mac_address',
|
||||||
|
label='MAC address',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@@ -420,48 +428,73 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def _mac_address(self, queryset, name, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
return queryset.filter(mac_address=value)
|
||||||
|
except AddrFormatError:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.CharFilter(
|
site = django_filters.CharFilter(
|
||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
device = django_filters.CharFilter(
|
||||||
class Meta:
|
method='filter_device',
|
||||||
model = ConsoleServerPort
|
label='Device',
|
||||||
fields = []
|
)
|
||||||
|
|
||||||
def filter_site(self, queryset, name, value):
|
def filter_site(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(cs_port__device__site__slug=value)
|
return queryset.filter(cs_port__device__site__slug=value)
|
||||||
|
|
||||||
|
def filter_device(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(device__name__icontains=value) |
|
||||||
|
Q(cs_port__device__name__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionFilter(django_filters.FilterSet):
|
class PowerConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.CharFilter(
|
site = django_filters.CharFilter(
|
||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
device = django_filters.CharFilter(
|
||||||
class Meta:
|
method='filter_device',
|
||||||
model = PowerOutlet
|
label='Device',
|
||||||
fields = []
|
)
|
||||||
|
|
||||||
def filter_site(self, queryset, name, value):
|
def filter_site(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(power_outlet__device__site__slug=value)
|
return queryset.filter(power_outlet__device__site__slug=value)
|
||||||
|
|
||||||
|
def filter_device(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(device__name__icontains=value) |
|
||||||
|
Q(power_outlet__device__name__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.CharFilter(
|
site = django_filters.CharFilter(
|
||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
|
device = django_filters.CharFilter(
|
||||||
class Meta:
|
method='filter_device',
|
||||||
model = InterfaceConnection
|
label='Device',
|
||||||
fields = []
|
)
|
||||||
|
|
||||||
def filter_site(self, queryset, name, value):
|
def filter_site(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -470,3 +503,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
|||||||
Q(interface_a__device__site__slug=value) |
|
Q(interface_a__device__site__slug=value) |
|
||||||
Q(interface_b__device__site__slug=value)
|
Q(interface_b__device__site__slug=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def filter_device(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(interface_a__device__name__icontains=value) |
|
||||||
|
Q(interface_b__device__name__icontains=value)
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from .models import (
|
|||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||||
VIRTUAL_IFACE_TYPES
|
SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -375,6 +375,21 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||||
to_field_name='slug'
|
to_field_name='slug'
|
||||||
)
|
)
|
||||||
|
is_console_server = forms.BooleanField(
|
||||||
|
required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
|
||||||
|
is_pdu = forms.BooleanField(
|
||||||
|
required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||||
|
)
|
||||||
|
is_network_device = forms.BooleanField(
|
||||||
|
required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||||
|
)
|
||||||
|
subdevice_role = forms.NullBooleanField(
|
||||||
|
required=False, label='Subdevice role', widget=forms.Select(choices=(
|
||||||
|
('', '---------'),
|
||||||
|
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||||
|
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -1466,7 +1481,7 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
|||||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude(
|
||||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
).select_related(
|
).select_related(
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
@@ -1643,14 +1658,17 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||||
|
device = forms.CharField(required=False, label='Device name')
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||||
|
device = forms.CharField(required=False, label='Device name')
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||||
|
device = forms.CharField(required=False, label='Device name')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ DEVICE_PRIMARY_IP = """
|
|||||||
{{ record.primary_ip4.address.ip|default:"" }}
|
{{ record.primary_ip4.address.ip|default:"" }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SUBDEVICE_ROLE_TEMPLATE = """
|
||||||
|
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
UTILIZATION_GRAPH = """
|
UTILIZATION_GRAPH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% utilization_graph value %}
|
{% utilization_graph value %}
|
||||||
@@ -249,11 +253,18 @@ class DeviceTypeTable(BaseTable):
|
|||||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||||
part_number = tables.Column(verbose_name='Part Number')
|
part_number = tables.Column(verbose_name='Part Number')
|
||||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||||
|
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||||
|
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||||
|
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||||
|
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||||
instance_count = tables.Column(verbose_name='Instances')
|
instance_count = tables.Column(verbose_name='Instances')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
fields = (
|
||||||
|
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
|
'is_network_device', 'subdevice_role', 'instance_count'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -90,7 +90,12 @@ class ComponentCreateView(View):
|
|||||||
self.parent_field: parent.pk,
|
self.parent_field: parent.pk,
|
||||||
'name': name,
|
'name': name,
|
||||||
}
|
}
|
||||||
component_data.update(data)
|
# Replace objects with their primary key to keep component_form.clean() happy
|
||||||
|
for k, v in data.items():
|
||||||
|
if hasattr(v, 'pk'):
|
||||||
|
component_data[k] = v.pk
|
||||||
|
else:
|
||||||
|
component_data[k] = v
|
||||||
component_form = self.model_form(component_data)
|
component_form = self.model_form(component_data)
|
||||||
if component_form.is_valid():
|
if component_form.is_valid():
|
||||||
new_components.append(component_form.save(commit=False))
|
new_components.append(component_form.save(commit=False))
|
||||||
@@ -1445,9 +1450,10 @@ def interfaceconnection_add(request, pk):
|
|||||||
))
|
))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
||||||
|
device_b = interfaceconnection.interface_b.device
|
||||||
params = urlencode({
|
params = urlencode({
|
||||||
'rack_b': interfaceconnection.interface_b.device.rack.pk,
|
'rack_b': device_b.rack.pk if device_b.rack else '',
|
||||||
'device_b': interfaceconnection.interface_b.device.pk,
|
'device_b': device_b.pk,
|
||||||
})
|
})
|
||||||
return HttpResponseRedirect('{}?{}'.format(base_url, params))
|
return HttpResponseRedirect('{}?{}'.format(base_url, params))
|
||||||
else:
|
else:
|
||||||
|
|||||||
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-04-04 19:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0004_topologymap_change_comma_to_semicolon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='useraction',
|
||||||
|
name='action',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, b'created'), (7, b'bulk created'), (2, b'imported'), (3, b'modified'), (4, b'bulk edited'), (5, b'deleted'), (6, b'bulk deleted')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -56,13 +56,15 @@ ACTION_EDIT = 3
|
|||||||
ACTION_BULK_EDIT = 4
|
ACTION_BULK_EDIT = 4
|
||||||
ACTION_DELETE = 5
|
ACTION_DELETE = 5
|
||||||
ACTION_BULK_DELETE = 6
|
ACTION_BULK_DELETE = 6
|
||||||
|
ACTION_BULK_CREATE = 7
|
||||||
ACTION_CHOICES = (
|
ACTION_CHOICES = (
|
||||||
(ACTION_CREATE, 'created'),
|
(ACTION_CREATE, 'created'),
|
||||||
|
(ACTION_BULK_CREATE, 'bulk created'),
|
||||||
(ACTION_IMPORT, 'imported'),
|
(ACTION_IMPORT, 'imported'),
|
||||||
(ACTION_EDIT, 'modified'),
|
(ACTION_EDIT, 'modified'),
|
||||||
(ACTION_BULK_EDIT, 'bulk edited'),
|
(ACTION_BULK_EDIT, 'bulk edited'),
|
||||||
(ACTION_DELETE, 'deleted'),
|
(ACTION_DELETE, 'deleted'),
|
||||||
(ACTION_BULK_DELETE, 'bulk deleted')
|
(ACTION_BULK_DELETE, 'bulk deleted'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -328,6 +330,9 @@ class UserActionManager(models.Manager):
|
|||||||
def log_import(self, user, content_type, message=''):
|
def log_import(self, user, content_type, message=''):
|
||||||
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
||||||
|
|
||||||
|
def log_bulk_create(self, user, content_type, message=''):
|
||||||
|
self.log_bulk_action(user, content_type, ACTION_BULK_CREATE, message)
|
||||||
|
|
||||||
def log_bulk_edit(self, user, content_type, message=''):
|
def log_bulk_edit(self, user, content_type, message=''):
|
||||||
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
||||||
|
|
||||||
@@ -358,7 +363,7 @@ class UserAction(models.Model):
|
|||||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
||||||
return mark_safe('<i class="glyphicon glyphicon-plus text-success"></i>')
|
return mark_safe('<i class="glyphicon glyphicon-plus text-success"></i>')
|
||||||
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
|
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
|
||||||
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')
|
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ from django.db.models import Q
|
|||||||
from dcim.models import Site, Device, Interface
|
from dcim.models import Site, Device, Interface
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
|
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -44,6 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RIRFilter(django_filters.FilterSet):
|
class RIRFilter(django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
@@ -51,6 +53,7 @@ class RIRFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -84,6 +87,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -182,6 +186,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@@ -283,6 +288,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -295,7 +296,12 @@ def aggregate(request, pk):
|
|||||||
prefix_table = tables.PrefixTable(child_prefixes)
|
prefix_table = tables.PrefixTable(child_prefixes)
|
||||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||||
prefix_table.base_columns['pk'].visible = True
|
prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
|
||||||
|
paginate = {
|
||||||
|
'klass': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(prefix_table)
|
||||||
|
|
||||||
# Compile permissions list for rendering the object table
|
# Compile permissions list for rendering the object table
|
||||||
permissions = {
|
permissions = {
|
||||||
@@ -427,7 +433,12 @@ def prefix(request, pk):
|
|||||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||||
child_prefix_table.base_columns['pk'].visible = True
|
child_prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
|
||||||
|
paginate = {
|
||||||
|
'klass': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(child_prefix_table)
|
||||||
|
|
||||||
# Compile permissions list for rendering the object table
|
# Compile permissions list for rendering the object table
|
||||||
permissions = {
|
permissions = {
|
||||||
@@ -500,7 +511,12 @@ def prefix_ipaddresses(request, pk):
|
|||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
ip_table = tables.IPAddressTable(ipaddresses)
|
||||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||||
ip_table.base_columns['pk'].visible = True
|
ip_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
|
||||||
|
paginate = {
|
||||||
|
'klass': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(ip_table)
|
||||||
|
|
||||||
# Compile permissions list for rendering the object table
|
# Compile permissions list for rendering the object table
|
||||||
permissions = {
|
permissions = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.9.2'
|
VERSION = '1.9.4-r1'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from .models import Secret, SecretRole
|
from .models import Secret, SecretRole
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
|
from utilities.filters import NumericInFilter
|
||||||
|
|
||||||
|
|
||||||
class SecretFilter(django_filters.FilterSet):
|
class SecretFilter(django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
||||||
@@ -295,7 +295,8 @@
|
|||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
||||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> ·
|
||||||
|
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rack</td>
|
<td>Rack</td>
|
||||||
<td>Rack name</td>
|
<td>Rack name (optional)</td>
|
||||||
<td>R101</td>
|
<td>R101</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -35,7 +35,13 @@
|
|||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||||
{% if peer_termination %}
|
{% if peer_termination %}
|
||||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
|
{% if peer_termination.interface %}
|
||||||
|
<a href="{% url 'dcim:device' pk=peer_termination.interface.device.pk %}">{{ peer_termination.interface.device }}</a>
|
||||||
|
(<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>)
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
|
||||||
|
{% endif %}
|
||||||
|
via
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -43,12 +43,14 @@
|
|||||||
{% render_field form.set_as_primary %}
|
{% render_field form.set_as_primary %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_custom_fields form %}
|
{% render_custom_fields form %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
|||||||
@@ -216,12 +216,12 @@
|
|||||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.change_rackreservation %}
|
{% if perms.dcim.change_rackreservation %}
|
||||||
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
|
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.delete_rackreservation %}
|
{% if perms.dcim.delete_rackreservation %}
|
||||||
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}" class="btn btn-danger btn-xs" title="Delete reservation">
|
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}" class="btn btn-danger btn-xs" title="Delete reservation">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ from django.db.models import Q
|
|||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||||
|
"""
|
||||||
|
Filters for a set of numeric values. Example: id__in=100,200,300
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
"""
|
"""
|
||||||
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.core.paginator import Paginator, Page
|
|||||||
class EnhancedPaginator(Paginator):
|
class EnhancedPaginator(Paginator):
|
||||||
|
|
||||||
def __init__(self, object_list, per_page, **kwargs):
|
def __init__(self, object_list, per_page, **kwargs):
|
||||||
|
if not isinstance(per_page, int) or per_page < 1:
|
||||||
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
|
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
|
||||||
super(EnhancedPaginator, self).__init__(object_list, per_page, **kwargs)
|
super(EnhancedPaginator, self).__init__(object_list, per_page, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ def gfm(value):
|
|||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def startswith(value, arg):
|
def contains(value, arg):
|
||||||
"""
|
"""
|
||||||
Test whether a string starts with the given argument
|
Test whether a value contains any of a given set of strings. `arg` should be a comma-separated list of strings.
|
||||||
"""
|
"""
|
||||||
return str(value).startswith(arg)
|
return any(s in value for s in arg.split(','))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -101,7 +102,13 @@ class ObjectListView(View):
|
|||||||
table = self.table(self.queryset)
|
table = self.table(self.queryset)
|
||||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||||
table.base_columns['pk'].visible = True
|
table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(table)
|
|
||||||
|
# Apply the request context
|
||||||
|
paginate = {
|
||||||
|
'klass': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(table)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'table': table,
|
'table': table,
|
||||||
@@ -327,7 +334,9 @@ class BulkAddView(View):
|
|||||||
form.add_error(None, e)
|
form.add_error(None, e)
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
messages.success(request, u"Added {} {}.".format(len(new_objs), self.model._meta.verbose_name_plural))
|
msg = u"Added {} {}".format(len(new_objs), self.model._meta.verbose_name_plural)
|
||||||
|
messages.success(request, msg)
|
||||||
|
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(self.model), msg)
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect(request.path)
|
return redirect(request.path)
|
||||||
return redirect(self.default_return_url)
|
return redirect(self.default_return_url)
|
||||||
|
|||||||
Reference in New Issue
Block a user