Browse Source

Merge branch 'develop' into v2-develop

Conflicts:
	netbox/netbox/settings.py
Jeremy Stretch 8 years ago
parent
commit
409c9c4e23

+ 0 - 4
README.md

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

+ 1 - 1
netbox/dcim/forms.py

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

+ 3 - 2
netbox/dcim/views.py

@@ -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,
-                    '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))
                 return HttpResponseRedirect('{}?{}'.format(base_url, params))
             else:
             else:

+ 7 - 2
netbox/extras/models.py

@@ -58,13 +58,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'),
 )
 )
 
 
 
 
@@ -451,6 +453,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)
 
 
@@ -481,7 +486,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>')

+ 19 - 3
netbox/ipam/views.py

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

+ 8 - 8
netbox/templates/_base.html

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

+ 2 - 0
netbox/templates/dcim/ipaddress_assign.html

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

+ 2 - 2
netbox/templates/dcim/rack.html

@@ -230,12 +230,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>

+ 2 - 1
netbox/utilities/paginator.py

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

+ 3 - 3
netbox/utilities/templatetags/helpers.py

@@ -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(','))
 
 
 
 
 @register.filter()
 @register.filter()

+ 11 - 2
netbox/utilities/views.py

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