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

+ 3 - 2
netbox/dcim/views.py

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

+ 7 - 2
netbox/extras/models.py

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

+ 19 - 3
netbox/ipam/views.py

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

+ 8 - 8
netbox/templates/_base.html

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

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

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

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

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

+ 2 - 1
netbox/utilities/paginator.py

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

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

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

+ 11 - 2
netbox/utilities/views.py

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