Parcourir la source

Basic Support for Python 3 (#827)

* Rudimentary python3 support

* update docs and trigger Travis

* fix some of the tests

* fix all python3 errors

* change env calls to just python

* add @python_2_unicode_compatible decorator to models for python2 compatibility

* switch netbox.configuration to from netbox import configuration
Jens L il y a 8 ans
Parent
commit
80439c495e

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 *.pyc
 /netbox/netbox/configuration.py
+/netbox/netbox/ldap_config.py
 /netbox/static
 .idea
 /*.sh

+ 3 - 0
.travis.yml

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

+ 2 - 2
docs/installation/netbox.md

@@ -3,14 +3,14 @@
 **Debian/Ubuntu**
 
 ```no-highlight
-# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
+# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
 ```
 
 **CentOS/RHEL**
 
 ```no-highlight
 # yum install -y epel-release
-# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
+# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
 ```
 
 You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.

+ 9 - 4
netbox/circuits/models.py

@@ -1,6 +1,7 @@
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
 from dcim.fields import ASNField
 from extras.models import CustomFieldModel, CustomFieldValue
@@ -33,6 +34,7 @@ def humanize_speed(speed):
         return '{} Kbps'.format(speed)
 
 
+@python_2_unicode_compatible
 class Provider(CreatedUpdatedModel, CustomFieldModel):
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@@ -51,7 +53,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
         ])
 
 
+@python_2_unicode_compatible
 class CircuitType(models.Model):
     """
     Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
@@ -78,13 +81,14 @@ class CircuitType(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
         return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
 
 
+@python_2_unicode_compatible
 class Circuit(CreatedUpdatedModel, CustomFieldModel):
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
@@ -105,7 +109,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
         ordering = ['provider', 'cid']
         unique_together = ['provider', 'cid']
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} {}'.format(self.provider, self.cid)
 
     def get_absolute_url(self):
@@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
     commit_rate_human.admin_order_field = 'commit_rate'
 
 
+@python_2_unicode_compatible
 class CircuitTermination(models.Model):
     circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
     term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
@@ -156,7 +161,7 @@ class CircuitTermination(models.Model):
         ordering = ['circuit', 'term_side']
         unique_together = ['circuit', 'term_side']
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
 
     def get_peer_termination(self):

+ 1 - 1
netbox/dcim/forms.py

@@ -13,7 +13,7 @@ from utilities.forms import (
     SlugField,
 )
 
-from formfields import MACAddressFormField
+from .formfields import MACAddressFormField
 from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,

+ 46 - 23
netbox/dcim/models.py

@@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Count, Q, ObjectDoesNotExist
+from django.utils.encoding import python_2_unicode_compatible
 
 from circuits.models import Circuit
 from extras.models import CustomFieldModel, CustomField, CustomFieldValue
@@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager):
         return self.natural_order_by('name')
 
 
+@python_2_unicode_compatible
 class Site(CreatedUpdatedModel, CustomFieldModel):
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
@@ -222,7 +224,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
 # Racks
 #
 
+@python_2_unicode_compatible
 class RackGroup(models.Model):
     """
     Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
@@ -282,13 +285,14 @@ class RackGroup(models.Model):
             ['site', 'slug'],
         ]
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.site.name, self.name)
 
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
 
 
+@python_2_unicode_compatible
 class RackRole(models.Model):
     """
     Racks can be organized by functional role, similar to Devices.
@@ -300,7 +304,7 @@ class RackRole(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager):
         return self.natural_order_by('site__name', 'name')
 
 
+@python_2_unicode_compatible
 class Rack(CreatedUpdatedModel, CustomFieldModel):
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -343,7 +348,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
             ['site', 'facility_id'],
         ]
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
 
     def get_absolute_url(self):
@@ -442,7 +447,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
         devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
 
         # Initialize the rack unit skeleton
-        units = range(1, self.u_height + 1)
+        units = list(range(1, self.u_height + 1))
 
         # Remove units consumed by installed devices
         for d in devices:
@@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
 # Device Types
 #
 
+@python_2_unicode_compatible
 class Manufacturer(models.Model):
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -487,13 +493,14 @@ class Manufacturer(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
         return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
 
 
+@python_2_unicode_compatible
 class DeviceType(models.Model, CustomFieldModel):
     """
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@@ -538,7 +545,7 @@ class DeviceType(models.Model, CustomFieldModel):
             ['manufacturer', 'slug'],
         ]
 
-    def __unicode__(self):
+    def __str__(self):
         return self.model
 
     def __init__(self, *args, **kwargs):
@@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel):
         return bool(self.subdevice_role is False)
 
 
+@python_2_unicode_compatible
 class ConsolePortTemplate(models.Model):
     """
     A template for a ConsolePort to be created for a new Device.
@@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class ConsoleServerPortTemplate(models.Model):
     """
     A template for a ConsoleServerPort to be created for a new Device.
@@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class PowerPortTemplate(models.Model):
     """
     A template for a PowerPort to be created for a new Device.
@@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class PowerOutletTemplate(models.Model):
     """
     A template for a PowerOutlet to be created for a new Device.
@@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
@@ -706,6 +717,7 @@ class InterfaceManager(models.Manager):
         }).order_by(*ordering)
 
 
+@python_2_unicode_compatible
 class InterfaceTemplate(models.Model):
     """
     A template for a physical data interface on a new Device.
@@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class DeviceBayTemplate(models.Model):
     """
     A template for a DeviceBay to be created for a new parent Device.
@@ -736,7 +749,7 @@ class DeviceBayTemplate(models.Model):
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
@@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model):
 # Devices
 #
 
+@python_2_unicode_compatible
 class DeviceRole(models.Model):
     """
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@@ -756,13 +770,14 @@ class DeviceRole(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
         return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
 
 
+@python_2_unicode_compatible
 class Platform(models.Model):
     """
     Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos".
@@ -776,7 +791,7 @@ class Platform(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager):
         return self.natural_order_by('name')
 
 
+@python_2_unicode_compatible
 class Device(CreatedUpdatedModel, CustomFieldModel):
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@@ -828,7 +844,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         ordering = ['name']
         unique_together = ['rack', 'position', 'face']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
 
     def get_absolute_url(self):
@@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         return RPC_CLIENTS.get(self.platform.rpc_client)
 
 
+@python_2_unicode_compatible
 class ConsolePort(models.Model):
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -982,7 +999,7 @@ class ConsolePort(models.Model):
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     # Used for connections export
@@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager):
         }).order_by('device', 'name_as_integer')
 
 
+@python_2_unicode_compatible
 class ConsoleServerPort(models.Model):
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -1023,10 +1041,11 @@ class ConsoleServerPort(models.Model):
     class Meta:
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class PowerPort(models.Model):
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -1041,7 +1060,7 @@ class PowerPort(models.Model):
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     # Used for connections export
@@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager):
         }).order_by('device', 'name_padded')
 
 
+@python_2_unicode_compatible
 class PowerOutlet(models.Model):
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model):
     class Meta:
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class Interface(models.Model):
     """
     A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
@@ -1099,7 +1120,7 @@ class Interface(models.Model):
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def clean(self):
@@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model):
         ])
 
 
+@python_2_unicode_compatible
 class DeviceBay(models.Model):
     """
     An empty space within a Device which can house a child device
@@ -1189,7 +1211,7 @@ class DeviceBay(models.Model):
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.device.name, self.name)
 
     def clean(self):
@@ -1205,6 +1227,7 @@ class DeviceBay(models.Model):
             raise ValidationError("Cannot install a device into itself.")
 
 
+@python_2_unicode_compatible
 class Module(models.Model):
     """
     A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
@@ -1223,5 +1246,5 @@ class Module(models.Model):
         ordering = ['device__id', 'parent__id', 'name']
         unique_together = ['device', 'parent', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name

+ 37 - 37
netbox/dcim/tests/test_apis.py

@@ -65,7 +65,7 @@ class SiteTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -75,7 +75,7 @@ class SiteTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -84,9 +84,9 @@ class SiteTest(APITestCase):
 
     def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        for i in json.loads(response.content):
+        for i in json.loads(response.content.decode('utf-8')):
             self.assertEqual(
                 sorted(i.keys()),
                 sorted(self.rack_fields),
@@ -99,9 +99,9 @@ class SiteTest(APITestCase):
 
     def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        for i in json.loads(response.content):
+        for i in json.loads(response.content.decode('utf-8')):
             self.assertEqual(
                 sorted(i.keys()),
                 sorted(self.graph_fields),
@@ -159,7 +159,7 @@ class RackTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -173,7 +173,7 @@ class RackTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -250,7 +250,7 @@ class DeviceTypeTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -261,7 +261,7 @@ class DeviceTypeTest(APITestCase):
     def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
         # TODO: details returns list view.
         # response = self.client.get(endpoint)
-        # content = json.loads(response.content)
+        # content = json.loads(response.content.decode('utf-8'))
         # self.assertEqual(response.status_code, status.HTTP_200_OK)
         # self.assertEqual(
         #     sorted(content.keys()),
@@ -284,7 +284,7 @@ class DeviceRolesTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -294,7 +294,7 @@ class DeviceRolesTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -312,7 +312,7 @@ class PlatformsTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -322,7 +322,7 @@ class PlatformsTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -360,7 +360,7 @@ class DeviceTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for device in content:
             self.assertEqual(
@@ -425,7 +425,7 @@ class DeviceTest(APITestCase):
         ]
 
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         device = content[0]
         self.assertEqual(
@@ -435,7 +435,7 @@ class DeviceTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -453,7 +453,7 @@ class ConsoleServerPortsTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for console_port in content:
             self.assertEqual(
@@ -475,7 +475,7 @@ class ConsolePortsTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for console_port in content:
             self.assertEqual(
@@ -493,7 +493,7 @@ class ConsolePortsTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -514,7 +514,7 @@ class PowerPortsTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -528,7 +528,7 @@ class PowerPortsTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -549,7 +549,7 @@ class PowerOutletsTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -599,7 +599,7 @@ class InterfaceTest(APITestCase):
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         for i in content:
             self.assertEqual(
@@ -613,7 +613,7 @@ class InterfaceTest(APITestCase):
 
     def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -625,19 +625,19 @@ class InterfaceTest(APITestCase):
         )
 
     def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
-            response = self.client.get(endpoint)
-            content = json.loads(response.content)
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-            for i in content:
-                self.assertEqual(
-                    sorted(i.keys()),
-                    sorted(SiteTest.graph_fields),
-                )
+        response = self.client.get(endpoint)
+        content = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        for i in content:
+            self.assertEqual(
+                sorted(i.keys()),
+                sorted(SiteTest.graph_fields),
+            )
 
     def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
                                        .format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),
@@ -659,7 +659,7 @@ class RelatedConnectionsTest(APITestCase):
     def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
                                       .format(settings.BASE_PATH))):
         response = self.client.get(endpoint)
-        content = json.loads(response.content)
+        content = json.loads(response.content.decode('utf-8'))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
             sorted(content.keys()),

+ 1 - 1
netbox/extras/api/renderers.py

@@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer):
     def render(self, data, media_type=None, renderer_context=None):
 
         def flatten(entry):
-            for key, val in entry.iteritems():
+            for key, val in entry.items():
                 if isinstance(val, dict):
                     for child_key, child_val in flatten(val):
                         yield "{}_{}".format(key, child_key), child_val

+ 15 - 7
netbox/extras/models.py

@@ -8,6 +8,7 @@ from django.core.validators import ValidationError
 from django.db import models
 from django.http import HttpResponse
 from django.template import Template, Context
+from django.utils.encoding import python_2_unicode_compatible
 from django.utils.safestring import mark_safe
 
 
@@ -93,6 +94,7 @@ class CustomFieldModel(object):
             return OrderedDict([(field, None) for field in fields])
 
 
+@python_2_unicode_compatible
 class CustomField(models.Model):
     obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
                                       limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
@@ -114,7 +116,7 @@ class CustomField(models.Model):
     class Meta:
         ordering = ['weight', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.label or self.name.replace('_', ' ').capitalize()
 
     def serialize_value(self, value):
@@ -153,6 +155,7 @@ class CustomField(models.Model):
         return serialized_value
 
 
+@python_2_unicode_compatible
 class CustomFieldValue(models.Model):
     field = models.ForeignKey('CustomField', related_name='values')
     obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
@@ -164,7 +167,7 @@ class CustomFieldValue(models.Model):
         ordering = ['obj_type', 'obj_id']
         unique_together = ['field', 'obj_type', 'obj_id']
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} {}'.format(self.obj, self.field)
 
     @property
@@ -183,6 +186,7 @@ class CustomFieldValue(models.Model):
             super(CustomFieldValue, self).save(*args, **kwargs)
 
 
+@python_2_unicode_compatible
 class CustomFieldChoice(models.Model):
     field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
                               on_delete=models.CASCADE)
@@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model):
         ordering = ['field', 'weight', 'value']
         unique_together = ['field', 'value']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.value
 
     def clean(self):
@@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model):
         CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
 
 
+@python_2_unicode_compatible
 class Graph(models.Model):
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
     weight = models.PositiveSmallIntegerField(default=1000)
@@ -217,7 +222,7 @@ class Graph(models.Model):
     class Meta:
         ordering = ['type', 'weight', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def embed_url(self, obj):
@@ -231,6 +236,7 @@ class Graph(models.Model):
         return template.render(Context({'obj': obj}))
 
 
+@python_2_unicode_compatible
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
     name = models.CharField(max_length=100)
@@ -245,7 +251,7 @@ class ExportTemplate(models.Model):
             ['content_type', 'name']
         ]
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{}: {}'.format(self.content_type, self.name)
 
     def to_response(self, context_dict, filename):
@@ -264,6 +270,7 @@ class ExportTemplate(models.Model):
         return response
 
 
+@python_2_unicode_compatible
 class TopologyMap(models.Model):
     name = models.CharField(max_length=50, unique=True)
     slug = models.SlugField(unique=True)
@@ -278,7 +285,7 @@ class TopologyMap(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     @property
@@ -328,6 +335,7 @@ class UserActionManager(models.Manager):
         self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
 
 
+@python_2_unicode_compatible
 class UserAction(models.Model):
     """
     A record of an action (add, edit, or delete) performed on an object by a User.
@@ -344,7 +352,7 @@ class UserAction(models.Model):
     class Meta:
         ordering = ['-time']
 
-    def __unicode__(self):
+    def __str__(self):
         if self.message:
             return u'{} {}'.format(self.user, self.message)
         return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)

+ 2 - 2
netbox/generate_secret_key.py

@@ -1,8 +1,8 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # This script will generate a random 50-character string suitable for use as a SECRET_KEY.
 import os
 import random
 
 charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
 random.seed = (os.urandom(2048))
-print ''.join(random.choice(charset) for c in range(50))
+print(''.join(random.choice(charset) for c in range(50)))

+ 19 - 9
netbox/ipam/models.py

@@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models.expressions import RawSQL
+from django.utils.encoding import python_2_unicode_compatible
 
 from dcim.models import Interface
 from extras.models import CustomFieldModel, CustomFieldValue
@@ -72,6 +73,7 @@ IP_PROTOCOL_CHOICES = (
 )
 
 
+@python_2_unicode_compatible
 class VRF(CreatedUpdatedModel, CustomFieldModel):
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -91,7 +93,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
         verbose_name = 'VRF'
         verbose_name_plural = 'VRFs'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -107,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
         ])
 
 
+@python_2_unicode_compatible
 class RIR(models.Model):
     """
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@@ -122,13 +125,14 @@ class RIR(models.Model):
         verbose_name = 'RIR'
         verbose_name_plural = 'RIRs'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
         return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
 
 
+@python_2_unicode_compatible
 class Aggregate(CreatedUpdatedModel, CustomFieldModel):
     """
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
@@ -144,7 +148,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
     class Meta:
         ordering = ['family', 'prefix']
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.prefix)
 
     def get_absolute_url(self):
@@ -206,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
         return int(children_size / self.prefix.size * 100)
 
 
+@python_2_unicode_compatible
 class Role(models.Model):
     """
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
@@ -218,7 +223,7 @@ class Role(models.Model):
     class Meta:
         ordering = ['weight', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     @property
@@ -265,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet):
         return filter(lambda p: p.depth <= limit, queryset)
 
 
+@python_2_unicode_compatible
 class Prefix(CreatedUpdatedModel, CustomFieldModel):
     """
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
@@ -294,7 +300,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
         ordering = ['vrf', 'family', 'prefix']
         verbose_name_plural = 'prefixes'
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.prefix)
 
     def get_absolute_url(self):
@@ -379,6 +385,7 @@ class IPAddressManager(models.Manager):
         return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
 
 
+@python_2_unicode_compatible
 class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     """
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
@@ -411,7 +418,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
         verbose_name = 'IP address'
         verbose_name_plural = 'IP addresses'
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.address)
 
     def get_absolute_url(self):
@@ -471,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
         return STATUS_CHOICE_CLASSES[self.status]
 
 
+@python_2_unicode_compatible
 class VLANGroup(models.Model):
     """
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
@@ -488,13 +496,14 @@ class VLANGroup(models.Model):
         verbose_name = 'VLAN group'
         verbose_name_plural = 'VLAN groups'
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.site.name, self.name)
 
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
 
 
+@python_2_unicode_compatible
 class VLAN(CreatedUpdatedModel, CustomFieldModel):
     """
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
@@ -526,7 +535,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
         verbose_name = 'VLAN'
         verbose_name_plural = 'VLANs'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
 
     def get_absolute_url(self):
@@ -560,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
         return STATUS_CHOICE_CLASSES[self.status]
 
 
+@python_2_unicode_compatible
 class Service(CreatedUpdatedModel):
     """
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
@@ -578,5 +588,5 @@ class Service(CreatedUpdatedModel):
         ordering = ['device', 'protocol', 'port']
         unique_together = ['device', 'protocol', 'port']
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

+ 2 - 2
netbox/netbox/settings.py

@@ -6,7 +6,7 @@ from django.contrib.messages import constants as messages
 from django.core.exceptions import ImproperlyConfigured
 
 try:
-    import configuration
+    from netbox import configuration
 except ImportError:
     raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
                                "the documentation.")
@@ -50,7 +50,7 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
 # Attempt to import LDAP configuration if it has been defined
 LDAP_IGNORE_CERT_ERRORS = False
 try:
-    from ldap_config import *
+    from netbox.ldap_config import *
     LDAP_CONFIGURED = True
 except ImportError:
     LDAP_CONFIGURED = False

+ 1 - 1
netbox/netbox/urls.py

@@ -2,7 +2,7 @@ from django.conf import settings
 from django.conf.urls import include, url
 from django.contrib import admin
 
-from views import home, handle_500, trigger_500
+from netbox.views import home, handle_500, trigger_500
 from users.views import login, logout
 
 

+ 7 - 4
netbox/secrets/models.py

@@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.db import models
-from django.utils.encoding import force_bytes
+from django.utils.encoding import force_bytes, python_2_unicode_compatible
 
 from dcim.models import Device
 from utilities.models import CreatedUpdatedModel
@@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet):
         raise Exception("Bulk deletion has been disabled.")
 
 
+@python_2_unicode_compatible
 class UserKey(CreatedUpdatedModel):
     """
     A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
@@ -76,7 +77,7 @@ class UserKey(CreatedUpdatedModel):
         self.__initial_public_key = self.public_key
         self.__initial_master_key_cipher = self.master_key_cipher
 
-    def __unicode__(self):
+    def __str__(self):
         return self.user.username
 
     def clean(self, *args, **kwargs):
@@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel):
         self.save()
 
 
+@python_2_unicode_compatible
 class SecretRole(models.Model):
     """
     A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
@@ -186,7 +188,7 @@ class SecretRole(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
@@ -201,6 +203,7 @@ class SecretRole(models.Model):
         return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
 
 
+@python_2_unicode_compatible
 class Secret(CreatedUpdatedModel):
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
@@ -227,7 +230,7 @@ class Secret(CreatedUpdatedModel):
         self.plaintext = kwargs.pop('plaintext', None)
         super(Secret, self).__init__(*args, **kwargs)
 
-    def __unicode__(self):
+    def __str__(self):
         if self.role and self.device:
             return u'{} for {}'.format(self.role, self.device)
         return u'Secret'

+ 0 - 1
netbox/secrets/tests/__init__.py

@@ -1 +0,0 @@
-from test_models import *

+ 5 - 2
netbox/tenancy/models.py

@@ -1,12 +1,14 @@
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
 from extras.models import CustomFieldModel, CustomFieldValue
 from utilities.models import CreatedUpdatedModel
 from utilities.utils import csv_format
 
 
+@python_2_unicode_compatible
 class TenantGroup(models.Model):
     """
     An arbitrary collection of Tenants.
@@ -17,13 +19,14 @@ class TenantGroup(models.Model):
     class Meta:
         ordering = ['name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):
         return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
 
 
+@python_2_unicode_compatible
 class Tenant(CreatedUpdatedModel, CustomFieldModel):
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
@@ -39,7 +42,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
     class Meta:
         ordering = ['group', 'name']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def get_absolute_url(self):

+ 1 - 1
netbox/tenancy/views.py

@@ -10,7 +10,7 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 
-from models import Tenant, TenantGroup
+from .models import Tenant, TenantGroup
 from . import filters, forms, tables