Merge pull request #1032 from digitalocean/develop

Release v1.9.4
This commit is contained in:
Jeremy Stretch 2017-04-04 12:01:58 -04:00 committed by GitHub
commit 3ffe36e5ed
12 changed files with 59 additions and 29 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](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. 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

@ -1481,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'

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

@ -216,12 +216,12 @@
<small>{{ resv.user }} &middot; {{ resv.created }}</small> <small>{{ resv.user }} &middot; {{ 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>

View File

@ -5,7 +5,8 @@ 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):
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) super(EnhancedPaginator, self).__init__(object_list, per_page, **kwargs)
def _get_page(self, *args, **kwargs): def _get_page(self, *args, **kwargs):

View File

@ -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(','))
# #

View File

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