Compare commits

..

33 Commits

Author SHA1 Message Date
Jeremy Stretch
f89d91783b Merge pull request #1035 from digitalocean/develop
Release v1.9.4-r1
2017-04-04 15:50:28 -04:00
Jeremy Stretch
a18e1a0161 Release v1.9.4-r1 2017-04-04 15:47:25 -04:00
Jeremy Stretch
4308b8a4a5 Fixes #1034: Missing migration 2017-04-04 15:46:27 -04:00
Jeremy Stretch
aa54e14c37 Post-release version bump 2017-04-04 12:03:26 -04:00
Jeremy Stretch
3ffe36e5ed Merge pull request #1032 from digitalocean/develop
Release v1.9.4
2017-04-04 12:01:58 -04:00
Jeremy Stretch
3b2c74042e Release v1.9.4 2017-04-04 11:58:44 -04:00
Jeremy Stretch
11ae938146 Fixes #1027: Fixed nav menu highlighting when BASE_PATH is set 2017-04-04 11:55:16 -04:00
Stephen
f11bb254a5 Only show Custom Fields on IP Address Assign Page if custom fields are set against the ip address (#1031) 2017-04-04 11:37:20 -04:00
Jeremy Stretch
0b681c471e Removed survey notice 2017-04-03 16:01:03 -04:00
Jeremy Stretch
05d3354570 Fixes #1022: Record user actions when creating IP addresses in bulk 2017-04-03 14:45:20 -04:00
Jeremy Stretch
6813787fc7 Fixes #1013: Show edit/delete reservation buttons on rack view 2017-03-29 12:15:14 -04:00
Jeremy Stretch
28761fc960 Closes #362: Added per_page query parameter to control pagination page length 2017-03-28 15:57:50 -04:00
Jeremy Stretch
e8fd0f3531 Order interfaces naturally for Device A 2017-03-27 10:55:54 -04:00
Jeremy Stretch
8103c399d5 Fixes #991: Correct server error on "create and connect another" interface connection 2017-03-27 10:53:32 -04:00
Jeremy Stretch
a51f5edbc8 Post-release version bump 2017-03-23 16:29:42 -04:00
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
ef59f38ec4 Release v1.9.3 2017-03-23 16:24:35 -04:00
Jeremy Stretch
47120fae01 Rack assignment is optional for devices 2017-03-23 15:36:24 -04:00
Jeremy Stretch
c0417c1989 Closes #972: Add ability to filter connections list by device name 2017-03-23 10:07:02 -04:00
Jeremy Stretch
fb6cfa45fd Merge pull request #974 from marc-us/develop
Filter on mac address on interface
2017-03-23 09:35:01 -04:00
Mark
b875cea10d Filter on mac address on interface via API 2017-03-23 12:57:35 +01:00
Jeremy Stretch
32bf17c076 Closes #978: Allow filtering device types by function and subdevice role 2017-03-22 17:29:47 -04:00
Jeremy Stretch
66a6a8f33c Closes #983: Include peer device names when listing circuits in device view 2017-03-22 16:58:56 -04:00
Jeremy Stretch
05b71564d8 Closes #981: Allow filtering primary objects by a given set of IDs 2017-03-22 09:39:30 -04:00
Jeremy Stretch
1682de59df Added a footer link to the GitHub wiki 2017-03-20 14:05:26 -04:00
Mark
f26253ec49 Filter on mac address on interface 2017-03-18 21:26:33 +01:00
Mark
f2dc287f14 Filter on mac address on interface 2017-03-18 21:21:49 +01:00
Mark
3fe3151af7 Filter on mac address on interface
Extension to be able filter on mac address via API
2017-03-18 21:10:36 +01:00
Jeremy Stretch
1c1fd8f210 Limit tests to one per major Python version 2017-03-17 21:43:46 -04:00
Jeremy Stretch
3ce2f0d100 Fix error when assigning a new interface to a LAG 2017-03-16 22:27:01 -04:00
Jeremy Stretch
92d726bbd4 Added examples to the graphs documentation 2017-03-15 12:16:46 -04:00
Jeremy Stretch
e2ef0bc3a6 Added survey announcement to README 2017-03-15 12:00:53 -04:00
Jeremy Stretch
13c29cb7a9 Post-release version bump 2017-03-14 17:18:05 -04:00
23 changed files with 221 additions and 49 deletions

View File

@@ -9,9 +9,7 @@ env:
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install -r requirements.txt
- pip install pep8

View File

@@ -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`.
* **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
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.

View File

@@ -5,12 +5,13 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Provider, Circuit, CircuitType
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -42,6 +43,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -5,7 +5,7 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
@@ -14,6 +14,7 @@ from .models import (
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -81,6 +82,7 @@ class RackGroupFilter(django_filters.FilterSet):
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -157,6 +159,7 @@ class RackReservationFilter(django_filters.FilterSet):
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -191,6 +194,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -405,6 +409,10 @@ class InterfaceFilter(django_filters.FilterSet):
method='filter_type',
label='Interface type',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
class Meta:
model = Interface
@@ -420,48 +428,73 @@ class InterfaceFilter(django_filters.FilterSet):
return queryset.filter(form_factor=IFACE_FF_LAG)
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):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = ConsoleServerPort
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
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):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = PowerOutlet
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
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):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = InterfaceConnection
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
@@ -470,3 +503,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
Q(interface_a__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)
)

View File

@@ -23,7 +23,7 @@ from .models import (
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
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')),
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)
# 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
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
@@ -1643,14 +1658,17 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
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):
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):
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
device = forms.CharField(required=False, label='Device name')
#

View File

@@ -100,6 +100,10 @@ DEVICE_PRIMARY_IP = """
{{ 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 = """
{% load helpers %}
{% utilization_graph value %}
@@ -249,11 +253,18 @@ class DeviceTypeTable(BaseTable):
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
part_number = tables.Column(verbose_name='Part Number')
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')
class Meta(BaseTable.Meta):
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'
)
#

View File

@@ -90,7 +90,12 @@ class ComponentCreateView(View):
self.parent_field: parent.pk,
'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)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
@@ -1445,9 +1450,10 @@ def interfaceconnection_add(request, pk):
))
if '_addanother' in request.POST:
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
device_b = interfaceconnection.interface_b.device
params = urlencode({
'rack_b': interfaceconnection.interface_b.device.rack.pk,
'device_b': interfaceconnection.interface_b.device.pk,
'rack_b': device_b.rack.pk if device_b.rack else '',
'device_b': device_b.pk,
})
return HttpResponseRedirect('{}?{}'.format(base_url, params))
else:

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

View File

@@ -56,13 +56,15 @@ ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(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=''):
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=''):
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)
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>')
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')

View File

@@ -7,12 +7,13 @@ from django.db.models import Q
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
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
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -44,6 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RIRFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
class Meta:
model = RIR
@@ -51,6 +53,7 @@ class RIRFilter(django_filters.FilterSet):
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -84,6 +87,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -182,6 +186,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -283,6 +288,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -1,6 +1,7 @@
from django_tables2 import RequestConfig
import netaddr
from django.conf import settings
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib import messages
@@ -295,7 +296,12 @@ def aggregate(request, pk):
prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
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
permissions = {
@@ -427,7 +433,12 @@ def prefix(request, pk):
child_prefix_table = tables.PrefixTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
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
permissions = {
@@ -500,7 +511,12 @@ def prefix_ipaddresses(request, pk):
ip_table = tables.IPAddressTable(ipaddresses)
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
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
permissions = {

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.9.2'
VERSION = '1.9.4-r1'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@@ -4,9 +4,11 @@ from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device
from utilities.filters import NumericInFilter
class SecretFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -28,7 +28,7 @@
<div id="navbar" class="navbar-collapse collapse">
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
<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>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
@@ -54,7 +54,7 @@
{% endif %}
</ul>
</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>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
@@ -74,7 +74,7 @@
{% endif %}
</ul>
</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>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
@@ -110,7 +110,7 @@
{% endif %}
</ul>
</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>
<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>
@@ -133,7 +133,7 @@
{% endif %}
</ul>
</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>
<ul class="dropdown-menu">
<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 %}
</ul>
</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>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
@@ -199,7 +199,7 @@
{% endif %}
</ul>
</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>
<ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
@@ -223,7 +223,7 @@
</ul>
</li>
{% 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>
<ul class="dropdown-menu">
<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">
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> &middot;
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> &middot;
<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> &middot;
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
</p>
</div>
</div>

View File

@@ -73,7 +73,7 @@
</tr>
<tr>
<td>Rack</td>
<td>Rack name</td>
<td>Rack name (optional)</td>
<td>R101</td>
</tr>
<tr>

View File

@@ -35,7 +35,13 @@
<td colspan="2">
<i class="fa fa-fw fa-globe" title="Circuit"></i>
{% 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 %}
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
</td>

View File

@@ -43,12 +43,14 @@
{% render_field form.set_as_primary %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button>

View File

@@ -216,12 +216,12 @@
<small>{{ resv.user }} &middot; {{ resv.created }}</small>
</td>
<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">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% 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">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>

View File

@@ -3,11 +3,12 @@ import django_filters
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Tenant, TenantGroup
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -6,6 +6,17 @@ from django.db.models import Q
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):
"""
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is

View File

@@ -5,7 +5,8 @@ from django.core.paginator import Paginator, Page
class EnhancedPaginator(Paginator):
def __init__(self, object_list, per_page, **kwargs):
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
if not isinstance(per_page, int) or per_page < 1:
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
super(EnhancedPaginator, self).__init__(object_list, per_page, **kwargs)
def _get_page(self, *args, **kwargs):

View File

@@ -45,11 +45,11 @@ def gfm(value):
@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(','))
#

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from django_tables2 import RequestConfig
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
@@ -101,7 +102,13 @@ class ObjectListView(View):
table = self.table(self.queryset)
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
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 = {
'table': table,
@@ -327,7 +334,9 @@ class BulkAddView(View):
form.add_error(None, e)
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:
return redirect(request.path)
return redirect(self.default_return_url)