Browse 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 8 years ago
parent
commit
80439c495e

+ 1 - 0
.gitignore

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

+ 3 - 0
.travis.yml

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

+ 2 - 2
docs/installation/netbox.md

@@ -3,14 +3,14 @@
 **Debian/Ubuntu**
 **Debian/Ubuntu**
 
 
 ```no-highlight
 ```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**
 **CentOS/RHEL**
 
 
 ```no-highlight
 ```no-highlight
 # yum install -y epel-release
 # 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.
 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.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
 
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from extras.models import CustomFieldModel, CustomFieldValue
 from extras.models import CustomFieldModel, CustomFieldValue
@@ -33,6 +34,7 @@ def humanize_speed(speed):
         return '{} Kbps'.format(speed)
         return '{} Kbps'.format(speed)
 
 
 
 
+@python_2_unicode_compatible
 class Provider(CreatedUpdatedModel, CustomFieldModel):
 class Provider(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
         ])
         ])
 
 
 
 
+@python_2_unicode_compatible
 class CircuitType(models.Model):
 class CircuitType(models.Model):
     """
     """
     Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
         return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
 
 
 
 
+@python_2_unicode_compatible
 class Circuit(CreatedUpdatedModel, CustomFieldModel):
 class Circuit(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     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']
         ordering = ['provider', 'cid']
         unique_together = ['provider', 'cid']
         unique_together = ['provider', 'cid']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} {}'.format(self.provider, self.cid)
         return u'{} {}'.format(self.provider, self.cid)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
     commit_rate_human.admin_order_field = 'commit_rate'
     commit_rate_human.admin_order_field = 'commit_rate'
 
 
 
 
+@python_2_unicode_compatible
 class CircuitTermination(models.Model):
 class CircuitTermination(models.Model):
     circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
     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')
     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']
         ordering = ['circuit', 'term_side']
         unique_together = ['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())
         return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
 
 
     def get_peer_termination(self):
     def get_peer_termination(self):

+ 1 - 1
netbox/dcim/forms.py

@@ -13,7 +13,7 @@ from utilities.forms import (
     SlugField,
     SlugField,
 )
 )
 
 
-from formfields import MACAddressFormField
+from .formfields import MACAddressFormField
 from .models import (
 from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
     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.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models import Count, Q, ObjectDoesNotExist
 from django.db.models import Count, Q, ObjectDoesNotExist
+from django.utils.encoding import python_2_unicode_compatible
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
 from extras.models import CustomFieldModel, CustomField, CustomFieldValue
 from extras.models import CustomFieldModel, CustomField, CustomFieldValue
@@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager):
         return self.natural_order_by('name')
         return self.natural_order_by('name')
 
 
 
 
+@python_2_unicode_compatible
 class Site(CreatedUpdatedModel, CustomFieldModel):
 class Site(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
 # Racks
 # Racks
 #
 #
 
 
+@python_2_unicode_compatible
 class RackGroup(models.Model):
 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
     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'],
             ['site', 'slug'],
         ]
         ]
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.site.name, self.name)
         return u'{} - {}'.format(self.site.name, self.name)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
         return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
 
 
 
 
+@python_2_unicode_compatible
 class RackRole(models.Model):
 class RackRole(models.Model):
     """
     """
     Racks can be organized by functional role, similar to Devices.
     Racks can be organized by functional role, similar to Devices.
@@ -300,7 +304,7 @@ class RackRole(models.Model):
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager):
         return self.natural_order_by('site__name', 'name')
         return self.natural_order_by('site__name', 'name')
 
 
 
 
+@python_2_unicode_compatible
 class Rack(CreatedUpdatedModel, CustomFieldModel):
 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.
     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'],
             ['site', 'facility_id'],
         ]
         ]
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
         return self.display_name
 
 
     def get_absolute_url(self):
     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)
         devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
 
 
         # Initialize the rack unit skeleton
         # 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
         # Remove units consumed by installed devices
         for d in devices:
         for d in devices:
@@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
 # Device Types
 # Device Types
 #
 #
 
 
+@python_2_unicode_compatible
 class Manufacturer(models.Model):
 class Manufacturer(models.Model):
     """
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -487,13 +493,14 @@ class Manufacturer(models.Model):
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
         return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
 
 
 
 
+@python_2_unicode_compatible
 class DeviceType(models.Model, CustomFieldModel):
 class DeviceType(models.Model, CustomFieldModel):
     """
     """
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
     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'],
             ['manufacturer', 'slug'],
         ]
         ]
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.model
         return self.model
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel):
         return bool(self.subdevice_role is False)
         return bool(self.subdevice_role is False)
 
 
 
 
+@python_2_unicode_compatible
 class ConsolePortTemplate(models.Model):
 class ConsolePortTemplate(models.Model):
     """
     """
     A template for a ConsolePort to be created for a new Device.
     A template for a ConsolePort to be created for a new Device.
@@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model):
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class ConsoleServerPortTemplate(models.Model):
 class ConsoleServerPortTemplate(models.Model):
     """
     """
     A template for a ConsoleServerPort to be created for a new Device.
     A template for a ConsoleServerPort to be created for a new Device.
@@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model):
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class PowerPortTemplate(models.Model):
 class PowerPortTemplate(models.Model):
     """
     """
     A template for a PowerPort to be created for a new Device.
     A template for a PowerPort to be created for a new Device.
@@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model):
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class PowerOutletTemplate(models.Model):
 class PowerOutletTemplate(models.Model):
     """
     """
     A template for a PowerOutlet to be created for a new Device.
     A template for a PowerOutlet to be created for a new Device.
@@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model):
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
@@ -706,6 +717,7 @@ class InterfaceManager(models.Manager):
         }).order_by(*ordering)
         }).order_by(*ordering)
 
 
 
 
+@python_2_unicode_compatible
 class InterfaceTemplate(models.Model):
 class InterfaceTemplate(models.Model):
     """
     """
     A template for a physical data interface on a new Device.
     A template for a physical data interface on a new Device.
@@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model):
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class DeviceBayTemplate(models.Model):
 class DeviceBayTemplate(models.Model):
     """
     """
     A template for a DeviceBay to be created for a new parent Device.
     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']
         ordering = ['device_type', 'name']
         unique_together = ['device_type', 'name']
         unique_together = ['device_type', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
@@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model):
 # Devices
 # Devices
 #
 #
 
 
+@python_2_unicode_compatible
 class DeviceRole(models.Model):
 class DeviceRole(models.Model):
     """
     """
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
         return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
 
 
 
 
+@python_2_unicode_compatible
 class Platform(models.Model):
 class Platform(models.Model):
     """
     """
     Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos".
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager):
         return self.natural_order_by('name')
         return self.natural_order_by('name')
 
 
 
 
+@python_2_unicode_compatible
 class Device(CreatedUpdatedModel, CustomFieldModel):
 class Device(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     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']
         ordering = ['name']
         unique_together = ['rack', 'position', 'face']
         unique_together = ['rack', 'position', 'face']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
         return self.display_name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         return RPC_CLIENTS.get(self.platform.rpc_client)
         return RPC_CLIENTS.get(self.platform.rpc_client)
 
 
 
 
+@python_2_unicode_compatible
 class ConsolePort(models.Model):
 class ConsolePort(models.Model):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -982,7 +999,7 @@ class ConsolePort(models.Model):
         ordering = ['device', 'name']
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     # Used for connections export
     # Used for connections export
@@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager):
         }).order_by('device', 'name_as_integer')
         }).order_by('device', 'name_as_integer')
 
 
 
 
+@python_2_unicode_compatible
 class ConsoleServerPort(models.Model):
 class ConsoleServerPort(models.Model):
     """
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     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:
     class Meta:
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class PowerPort(models.Model):
 class PowerPort(models.Model):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -1041,7 +1060,7 @@ class PowerPort(models.Model):
         ordering = ['device', 'name']
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     # Used for connections export
     # Used for connections export
@@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager):
         }).order_by('device', 'name_padded')
         }).order_by('device', 'name_padded')
 
 
 
 
+@python_2_unicode_compatible
 class PowerOutlet(models.Model):
 class PowerOutlet(models.Model):
     """
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model):
     class Meta:
     class Meta:
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
 
 
+@python_2_unicode_compatible
 class Interface(models.Model):
 class Interface(models.Model):
     """
     """
     A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
     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']
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def clean(self):
     def clean(self):
@@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model):
         ])
         ])
 
 
 
 
+@python_2_unicode_compatible
 class DeviceBay(models.Model):
 class DeviceBay(models.Model):
     """
     """
     An empty space within a Device which can house a child device
     An empty space within a Device which can house a child device
@@ -1189,7 +1211,7 @@ class DeviceBay(models.Model):
         ordering = ['device', 'name']
         ordering = ['device', 'name']
         unique_together = ['device', 'name']
         unique_together = ['device', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.device.name, self.name)
         return u'{} - {}'.format(self.device.name, self.name)
 
 
     def clean(self):
     def clean(self):
@@ -1205,6 +1227,7 @@ class DeviceBay(models.Model):
             raise ValidationError("Cannot install a device into itself.")
             raise ValidationError("Cannot install a device into itself.")
 
 
 
 
+@python_2_unicode_compatible
 class Module(models.Model):
 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
     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']
         ordering = ['device__id', 'parent__id', 'name']
         unique_together = ['device', 'parent', 'name']
         unique_together = ['device', 'parent', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         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)):
     def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -75,7 +75,7 @@ class SiteTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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)):
     def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(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(
             self.assertEqual(
                 sorted(i.keys()),
                 sorted(i.keys()),
                 sorted(self.rack_fields),
                 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)):
     def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(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(
             self.assertEqual(
                 sorted(i.keys()),
                 sorted(i.keys()),
                 sorted(self.graph_fields),
                 sorted(self.graph_fields),
@@ -159,7 +159,7 @@ class RackTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -173,7 +173,7 @@ class RackTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             sorted(content.keys()),
@@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             sorted(content.keys()),
@@ -250,7 +250,7 @@ class DeviceTypeTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -261,7 +261,7 @@ class DeviceTypeTest(APITestCase):
     def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
     def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
         # TODO: details returns list view.
         # TODO: details returns list view.
         # response = self.client.get(endpoint)
         # 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(response.status_code, status.HTTP_200_OK)
         # self.assertEqual(
         # self.assertEqual(
         #     sorted(content.keys()),
         #     sorted(content.keys()),
@@ -284,7 +284,7 @@ class DeviceRolesTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -294,7 +294,7 @@ class DeviceRolesTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             sorted(content.keys()),
@@ -312,7 +312,7 @@ class PlatformsTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -322,7 +322,7 @@ class PlatformsTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             sorted(content.keys()),
@@ -360,7 +360,7 @@ class DeviceTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for device in content:
         for device in content:
             self.assertEqual(
             self.assertEqual(
@@ -425,7 +425,7 @@ class DeviceTest(APITestCase):
         ]
         ]
 
 
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         device = content[0]
         device = content[0]
         self.assertEqual(
         self.assertEqual(
@@ -435,7 +435,7 @@ class DeviceTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for console_port in content:
         for console_port in content:
             self.assertEqual(
             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)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for console_port in content:
         for console_port in content:
             self.assertEqual(
             self.assertEqual(
@@ -493,7 +493,7 @@ class ConsolePortsTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -528,7 +528,7 @@ class PowerPortsTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -599,7 +599,7 @@ class InterfaceTest(APITestCase):
 
 
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
     def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         for i in content:
         for i in content:
             self.assertEqual(
             self.assertEqual(
@@ -613,7 +613,7 @@ class InterfaceTest(APITestCase):
 
 
     def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
     def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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)):
     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/'
     def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
                                        .format(settings.BASE_PATH)):
                                        .format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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'
     def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
                                       .format(settings.BASE_PATH))):
                                       .format(settings.BASE_PATH))):
         response = self.client.get(endpoint)
         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(response.status_code, status.HTTP_200_OK)
         self.assertEqual(
         self.assertEqual(
             sorted(content.keys()),
             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 render(self, data, media_type=None, renderer_context=None):
 
 
         def flatten(entry):
         def flatten(entry):
-            for key, val in entry.iteritems():
+            for key, val in entry.items():
                 if isinstance(val, dict):
                 if isinstance(val, dict):
                     for child_key, child_val in flatten(val):
                     for child_key, child_val in flatten(val):
                         yield "{}_{}".format(key, child_key), child_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.db import models
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.template import Template, Context
 from django.template import Template, Context
+from django.utils.encoding import python_2_unicode_compatible
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 
 
@@ -93,6 +94,7 @@ class CustomFieldModel(object):
             return OrderedDict([(field, None) for field in fields])
             return OrderedDict([(field, None) for field in fields])
 
 
 
 
+@python_2_unicode_compatible
 class CustomField(models.Model):
 class CustomField(models.Model):
     obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
     obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
                                       limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
                                       limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
@@ -114,7 +116,7 @@ class CustomField(models.Model):
     class Meta:
     class Meta:
         ordering = ['weight', 'name']
         ordering = ['weight', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.label or self.name.replace('_', ' ').capitalize()
         return self.label or self.name.replace('_', ' ').capitalize()
 
 
     def serialize_value(self, value):
     def serialize_value(self, value):
@@ -153,6 +155,7 @@ class CustomField(models.Model):
         return serialized_value
         return serialized_value
 
 
 
 
+@python_2_unicode_compatible
 class CustomFieldValue(models.Model):
 class CustomFieldValue(models.Model):
     field = models.ForeignKey('CustomField', related_name='values')
     field = models.ForeignKey('CustomField', related_name='values')
     obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
     obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
@@ -164,7 +167,7 @@ class CustomFieldValue(models.Model):
         ordering = ['obj_type', 'obj_id']
         ordering = ['obj_type', 'obj_id']
         unique_together = ['field', 'obj_type', 'obj_id']
         unique_together = ['field', 'obj_type', 'obj_id']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} {}'.format(self.obj, self.field)
         return u'{} {}'.format(self.obj, self.field)
 
 
     @property
     @property
@@ -183,6 +186,7 @@ class CustomFieldValue(models.Model):
             super(CustomFieldValue, self).save(*args, **kwargs)
             super(CustomFieldValue, self).save(*args, **kwargs)
 
 
 
 
+@python_2_unicode_compatible
 class CustomFieldChoice(models.Model):
 class CustomFieldChoice(models.Model):
     field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
     field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
                               on_delete=models.CASCADE)
                               on_delete=models.CASCADE)
@@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model):
         ordering = ['field', 'weight', 'value']
         ordering = ['field', 'weight', 'value']
         unique_together = ['field', 'value']
         unique_together = ['field', 'value']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.value
         return self.value
 
 
     def clean(self):
     def clean(self):
@@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model):
         CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
         CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
 
 
 
 
+@python_2_unicode_compatible
 class Graph(models.Model):
 class Graph(models.Model):
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
     weight = models.PositiveSmallIntegerField(default=1000)
     weight = models.PositiveSmallIntegerField(default=1000)
@@ -217,7 +222,7 @@ class Graph(models.Model):
     class Meta:
     class Meta:
         ordering = ['type', 'weight', 'name']
         ordering = ['type', 'weight', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def embed_url(self, obj):
     def embed_url(self, obj):
@@ -231,6 +236,7 @@ class Graph(models.Model):
         return template.render(Context({'obj': obj}))
         return template.render(Context({'obj': obj}))
 
 
 
 
+@python_2_unicode_compatible
 class ExportTemplate(models.Model):
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
     name = models.CharField(max_length=100)
     name = models.CharField(max_length=100)
@@ -245,7 +251,7 @@ class ExportTemplate(models.Model):
             ['content_type', 'name']
             ['content_type', 'name']
         ]
         ]
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{}: {}'.format(self.content_type, self.name)
         return u'{}: {}'.format(self.content_type, self.name)
 
 
     def to_response(self, context_dict, filename):
     def to_response(self, context_dict, filename):
@@ -264,6 +270,7 @@ class ExportTemplate(models.Model):
         return response
         return response
 
 
 
 
+@python_2_unicode_compatible
 class TopologyMap(models.Model):
 class TopologyMap(models.Model):
     name = models.CharField(max_length=50, unique=True)
     name = models.CharField(max_length=50, unique=True)
     slug = models.SlugField(unique=True)
     slug = models.SlugField(unique=True)
@@ -278,7 +285,7 @@ class TopologyMap(models.Model):
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     @property
     @property
@@ -328,6 +335,7 @@ class UserActionManager(models.Manager):
         self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
         self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
 
 
 
 
+@python_2_unicode_compatible
 class UserAction(models.Model):
 class UserAction(models.Model):
     """
     """
     A record of an action (add, edit, or delete) performed on an object by a User.
     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:
     class Meta:
         ordering = ['-time']
         ordering = ['-time']
 
 
-    def __unicode__(self):
+    def __str__(self):
         if self.message:
         if self.message:
             return u'{} {}'.format(self.user, self.message)
             return u'{} {}'.format(self.user, self.message)
         return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
         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.
 # This script will generate a random 50-character string suitable for use as a SECRET_KEY.
 import os
 import os
 import random
 import random
 
 
 charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
 charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
 random.seed = (os.urandom(2048))
 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.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models.expressions import RawSQL
 from django.db.models.expressions import RawSQL
+from django.utils.encoding import python_2_unicode_compatible
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from extras.models import CustomFieldModel, CustomFieldValue
 from extras.models import CustomFieldModel, CustomFieldValue
@@ -72,6 +73,7 @@ IP_PROTOCOL_CHOICES = (
 )
 )
 
 
 
 
+@python_2_unicode_compatible
 class VRF(CreatedUpdatedModel, CustomFieldModel):
 class VRF(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
     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 = 'VRF'
         verbose_name_plural = 'VRFs'
         verbose_name_plural = 'VRFs'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -107,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
         ])
         ])
 
 
 
 
+@python_2_unicode_compatible
 class RIR(models.Model):
 class RIR(models.Model):
     """
     """
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
     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 = 'RIR'
         verbose_name_plural = 'RIRs'
         verbose_name_plural = 'RIRs'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
         return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
 
 
 
 
+@python_2_unicode_compatible
 class Aggregate(CreatedUpdatedModel, CustomFieldModel):
 class Aggregate(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
     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:
     class Meta:
         ordering = ['family', 'prefix']
         ordering = ['family', 'prefix']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.prefix)
         return str(self.prefix)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -206,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
         return int(children_size / self.prefix.size * 100)
         return int(children_size / self.prefix.size * 100)
 
 
 
 
+@python_2_unicode_compatible
 class Role(models.Model):
 class Role(models.Model):
     """
     """
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
     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:
     class Meta:
         ordering = ['weight', 'name']
         ordering = ['weight', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     @property
     @property
@@ -265,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet):
         return filter(lambda p: p.depth <= limit, queryset)
         return filter(lambda p: p.depth <= limit, queryset)
 
 
 
 
+@python_2_unicode_compatible
 class Prefix(CreatedUpdatedModel, CustomFieldModel):
 class Prefix(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
     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']
         ordering = ['vrf', 'family', 'prefix']
         verbose_name_plural = 'prefixes'
         verbose_name_plural = 'prefixes'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.prefix)
         return str(self.prefix)
 
 
     def get_absolute_url(self):
     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')
         return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
 
 
 
 
+@python_2_unicode_compatible
 class IPAddress(CreatedUpdatedModel, CustomFieldModel):
 class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
     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 = 'IP address'
         verbose_name_plural = 'IP addresses'
         verbose_name_plural = 'IP addresses'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return str(self.address)
         return str(self.address)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -471,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
         return STATUS_CHOICE_CLASSES[self.status]
         return STATUS_CHOICE_CLASSES[self.status]
 
 
 
 
+@python_2_unicode_compatible
 class VLANGroup(models.Model):
 class VLANGroup(models.Model):
     """
     """
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
     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 = 'VLAN group'
         verbose_name_plural = 'VLAN groups'
         verbose_name_plural = 'VLAN groups'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return u'{} - {}'.format(self.site.name, self.name)
         return u'{} - {}'.format(self.site.name, self.name)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
         return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
 
 
 
 
+@python_2_unicode_compatible
 class VLAN(CreatedUpdatedModel, CustomFieldModel):
 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
     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 = 'VLAN'
         verbose_name_plural = 'VLANs'
         verbose_name_plural = 'VLANs'
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.display_name
         return self.display_name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
@@ -560,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
         return STATUS_CHOICE_CLASSES[self.status]
         return STATUS_CHOICE_CLASSES[self.status]
 
 
 
 
+@python_2_unicode_compatible
 class Service(CreatedUpdatedModel):
 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
     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']
         ordering = ['device', 'protocol', 'port']
         unique_together = ['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())
         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
 from django.core.exceptions import ImproperlyConfigured
 
 
 try:
 try:
-    import configuration
+    from netbox import configuration
 except ImportError:
 except ImportError:
     raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
     raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
                                "the documentation.")
                                "the documentation.")
@@ -50,7 +50,7 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
 # Attempt to import LDAP configuration if it has been defined
 # Attempt to import LDAP configuration if it has been defined
 LDAP_IGNORE_CERT_ERRORS = False
 LDAP_IGNORE_CERT_ERRORS = False
 try:
 try:
-    from ldap_config import *
+    from netbox.ldap_config import *
     LDAP_CONFIGURED = True
     LDAP_CONFIGURED = True
 except ImportError:
 except ImportError:
     LDAP_CONFIGURED = False
     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.conf.urls import include, url
 from django.contrib import admin
 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
 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.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import models
 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 dcim.models import Device
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
@@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet):
         raise Exception("Bulk deletion has been disabled.")
         raise Exception("Bulk deletion has been disabled.")
 
 
 
 
+@python_2_unicode_compatible
 class UserKey(CreatedUpdatedModel):
 class UserKey(CreatedUpdatedModel):
     """
     """
     A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
     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_public_key = self.public_key
         self.__initial_master_key_cipher = self.master_key_cipher
         self.__initial_master_key_cipher = self.master_key_cipher
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.user.username
         return self.user.username
 
 
     def clean(self, *args, **kwargs):
     def clean(self, *args, **kwargs):
@@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel):
         self.save()
         self.save()
 
 
 
 
+@python_2_unicode_compatible
 class SecretRole(models.Model):
 class SecretRole(models.Model):
     """
     """
     A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     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()
         return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
 
 
 
 
+@python_2_unicode_compatible
 class Secret(CreatedUpdatedModel):
 class Secret(CreatedUpdatedModel):
     """
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
     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)
         self.plaintext = kwargs.pop('plaintext', None)
         super(Secret, self).__init__(*args, **kwargs)
         super(Secret, self).__init__(*args, **kwargs)
 
 
-    def __unicode__(self):
+    def __str__(self):
         if self.role and self.device:
         if self.role and self.device:
             return u'{} for {}'.format(self.role, self.device)
             return u'{} for {}'.format(self.role, self.device)
         return u'Secret'
         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.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
 
 from extras.models import CustomFieldModel, CustomFieldValue
 from extras.models import CustomFieldModel, CustomFieldValue
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 from utilities.utils import csv_format
 from utilities.utils import csv_format
 
 
 
 
+@python_2_unicode_compatible
 class TenantGroup(models.Model):
 class TenantGroup(models.Model):
     """
     """
     An arbitrary collection of Tenants.
     An arbitrary collection of Tenants.
@@ -17,13 +19,14 @@ class TenantGroup(models.Model):
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
         return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
         return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
 
 
 
 
+@python_2_unicode_compatible
 class Tenant(CreatedUpdatedModel, CustomFieldModel):
 class Tenant(CreatedUpdatedModel, CustomFieldModel):
     """
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     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:
     class Meta:
         ordering = ['group', 'name']
         ordering = ['group', 'name']
 
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
         return self.name
 
 
     def get_absolute_url(self):
     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,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 
 
-from models import Tenant, TenantGroup
+from .models import Tenant, TenantGroup
 from . import filters, forms, tables
 from . import filters, forms, tables