Compare commits

...

41 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
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
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
097e0f38ff Merge pull request #949 from digitalocean/develop
Release v1.9.1
2017-03-08 14:40:16 -05:00
Jeremy Stretch
ce26b566a4 Merge pull request #939 from digitalocean/develop
Release v1.9.0-r1
2017-03-03 11:28:02 -05:00
Jeremy Stretch
0e14bc1e02 Merge pull request #933 from digitalocean/develop
Release v1.9.0
2017-03-02 13:27:10 -05:00
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b6bbcb0609 Merge pull request #814 from digitalocean/develop
Release v1.8.2
2017-01-18 16:23:28 -05:00
Jeremy Stretch
23f6832d9c Merge pull request #774 from digitalocean/develop
Release v1.8.1
2017-01-04 15:30:54 -05:00
Jeremy Stretch
88dace75a1 Merge pull request #766 from digitalocean/develop
Release v1.8.0
2017-01-03 15:13:36 -05:00
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
814c11167e Merge pull request #694 from digitalocean/develop
Release v1.7.1
2016-11-15 12:34:09 -05:00
Jeremy Stretch
57ddd5086f Merge pull request #666 from digitalocean/develop
Release v1.7.0
2016-11-03 15:12:33 -04:00
Jeremy Stretch
c171547037 Merge pull request #625 from digitalocean/develop
Release v1.6.3
2016-10-19 16:25:50 -04:00
17 changed files with 167 additions and 45 deletions

View File

@@ -1,7 +1,3 @@
**The [2017 NetBox User Survey](https://goo.gl/forms/75HnNS2iE0Y1hVFH3) is open!** Please consider taking a moment to respond. Your feedback helps shape the pace and focus of NetBox development. The survey will remain open until 2017-03-31. Results will be published on the mailing list.
---
![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.

View File

@@ -409,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
@@ -424,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():
@@ -474,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

@@ -1450,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

@@ -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-dev'
VERSION = '1.9.4-r1'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

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>

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

@@ -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)