from copy import deepcopy
import re
from natsort import natsorted
from operator import attrgetter

from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.views.generic import View

from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)

from . import filters, forms, tables
from .models import (
    CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
    DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
    RackReservation, RackRole, Region, Site,
)


EXPANSION_PATTERN = '\[(\d+-\d+)\]'


def xstr(s):
    """
    Replace None with an empty string (for CSV export)
    """
    return '' if s is None else str(s)


def expand_pattern(string):
    """
    Expand a numeric pattern into a list of strings. Examples:
      'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
      'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
    """
    lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
    x, y = pattern.split('-')
    for i in range(int(x), int(y) + 1):
        if remnant:
            for string in expand_pattern(remnant):
                yield "{0}{1}{2}".format(lead, i, string)
        else:
            yield "{0}{1}".format(lead, i)


class ComponentCreateView(View):
    parent_model = None
    parent_field = None
    model = None
    form = None
    model_form = None

    def get(self, request, pk):

        parent = get_object_or_404(self.parent_model, pk=pk)
        form = self.form(parent, initial=request.GET)

        return render(request, 'dcim/device_component_add.html', {
            'parent': parent,
            'component_type': self.model._meta.verbose_name,
            'form': form,
            'return_url': parent.get_absolute_url(),
        })

    def post(self, request, pk):

        parent = get_object_or_404(self.parent_model, pk=pk)

        form = self.form(parent, request.POST)
        if form.is_valid():

            new_components = []
            data = deepcopy(form.cleaned_data)

            for name in form.cleaned_data['name_pattern']:
                component_data = {
                    self.parent_field: parent.pk,
                    'name': name,
                }
                # Replace objects with their primary key to keep component_form.clean() happy
                for k, v in data.items():
                    if hasattr(v, 'pk'):
                        component_data[k] = v.pk
                    else:
                        component_data[k] = v
                component_form = self.model_form(component_data)
                if component_form.is_valid():
                    new_components.append(component_form.save(commit=False))
                else:
                    for field, errors in component_form.errors.as_data().items():
                        # Assign errors on the child form's name field to name_pattern on the parent form
                        if field == 'name':
                            field = 'name_pattern'
                        for e in errors:
                            form.add_error(field, u'{}: {}'.format(name, ', '.join(e)))

            if not form.errors:
                self.model.objects.bulk_create(new_components)
                messages.success(request, u"Added {} {} to {}.".format(
                    len(new_components), self.model._meta.verbose_name_plural, parent
                ))
                if '_addanother' in request.POST:
                    return redirect(request.path)
                else:
                    return redirect(parent.get_absolute_url())

        return render(request, 'dcim/device_component_add.html', {
            'parent': parent,
            'component_type': self.model._meta.verbose_name,
            'form': form,
            'return_url': parent.get_absolute_url(),
        })


class ComponentEditView(ObjectEditView):

    def get_return_url(self, request, obj):
        return obj.device.get_absolute_url()


class ComponentDeleteView(ObjectDeleteView):

    def get_return_url(self, request, obj):
        return obj.device.get_absolute_url()


#
# Regions
#

class RegionListView(ObjectListView):
    queryset = Region.objects.annotate(site_count=Count('sites'))
    table = tables.RegionTable
    template_name = 'dcim/region_list.html'


class RegionEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_region'
    model = Region
    form_class = forms.RegionForm

    def get_return_url(self, request, obj):
        return reverse('dcim:region_list')


class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_region'
    cls = Region
    default_return_url = 'dcim:region_list'


#
# Sites
#

class SiteListView(ObjectListView):
    queryset = Site.objects.select_related('region', 'tenant')
    filter = filters.SiteFilter
    filter_form = forms.SiteFilterForm
    table = tables.SiteTable
    template_name = 'dcim/site_list.html'


def site(request, slug):

    site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
    stats = {
        'rack_count': Rack.objects.filter(site=site).count(),
        'device_count': Device.objects.filter(site=site).count(),
        'prefix_count': Prefix.objects.filter(site=site).count(),
        'vlan_count': VLAN.objects.filter(site=site).count(),
        'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
    }
    rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
    topology_maps = TopologyMap.objects.filter(site=site)
    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()

    return render(request, 'dcim/site.html', {
        'site': site,
        'stats': stats,
        'rack_groups': rack_groups,
        'topology_maps': topology_maps,
        'show_graphs': show_graphs,
    })


class SiteEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_site'
    model = Site
    form_class = forms.SiteForm
    template_name = 'dcim/site_edit.html'
    default_return_url = 'dcim:site_list'


class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
    permission_required = 'dcim.delete_site'
    model = Site
    default_return_url = 'dcim:site_list'


class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.add_site'
    form = forms.SiteImportForm
    table = tables.SiteTable
    template_name = 'dcim/site_import.html'
    default_return_url = 'dcim:site_list'


class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_site'
    cls = Site
    filter = filters.SiteFilter
    form = forms.SiteBulkEditForm
    template_name = 'dcim/site_bulk_edit.html'
    default_return_url = 'dcim:site_list'


#
# Rack groups
#

class RackGroupListView(ObjectListView):
    queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
    filter = filters.RackGroupFilter
    filter_form = forms.RackGroupFilterForm
    table = tables.RackGroupTable
    template_name = 'dcim/rackgroup_list.html'


class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_rackgroup'
    model = RackGroup
    form_class = forms.RackGroupForm

    def get_return_url(self, request, obj):
        return reverse('dcim:rackgroup_list')


class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_rackgroup'
    cls = RackGroup
    filter = filters.RackGroupFilter
    default_return_url = 'dcim:rackgroup_list'


#
# Rack roles
#

class RackRoleListView(ObjectListView):
    queryset = RackRole.objects.annotate(rack_count=Count('racks'))
    table = tables.RackRoleTable
    template_name = 'dcim/rackrole_list.html'


class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_rackrole'
    model = RackRole
    form_class = forms.RackRoleForm

    def get_return_url(self, request, obj):
        return reverse('dcim:rackrole_list')


class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_rackrole'
    cls = RackRole
    default_return_url = 'dcim:rackrole_list'


#
# Racks
#

class RackListView(ObjectListView):
    queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
        .annotate(device_count=Count('devices', distinct=True))
    filter = filters.RackFilter
    filter_form = forms.RackFilterForm
    table = tables.RackTable
    template_name = 'dcim/rack_list.html'


class RackElevationListView(View):
    """
    Display a set of rack elevations side-by-side.
    """

    def get(self, request):

        racks = Rack.objects.select_related(
            'site', 'group', 'tenant', 'role'
        ).prefetch_related(
            'devices__device_type'
        )
        racks = filters.RackFilter(request.GET, racks).qs
        total_count = racks.count()

        # Pagination
        paginator = EnhancedPaginator(racks, 25)
        page_number = request.GET.get('page', 1)
        try:
            page = paginator.page(page_number)
        except PageNotAnInteger:
            page = paginator.page(1)
        except EmptyPage:
            page = paginator.page(paginator.num_pages)

        # Determine rack face
        if request.GET.get('face') == '1':
            face_id = 1
        else:
            face_id = 0

        return render(request, 'dcim/rack_elevation_list.html', {
            'paginator': paginator,
            'page': page,
            'total_count': total_count,
            'face_id': face_id,
            'filter_form': forms.RackFilterForm(request.GET),
        })


def rack(request, pk):

    rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)

    nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
        .select_related('device_type__manufacturer')
    next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
    prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()

    reservations = RackReservation.objects.filter(rack=rack)
    reserved_units = {}
    for r in reservations:
        for u in r.units:
            reserved_units[u] = r

    return render(request, 'dcim/rack.html', {
        'rack': rack,
        'reservations': reservations,
        'reserved_units': reserved_units,
        'nonracked_devices': nonracked_devices,
        'next_rack': next_rack,
        'prev_rack': prev_rack,
        'front_elevation': rack.get_front_elevation(),
        'rear_elevation': rack.get_rear_elevation(),
    })


class RackEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_rack'
    model = Rack
    form_class = forms.RackForm
    template_name = 'dcim/rack_edit.html'
    default_return_url = 'dcim:rack_list'


class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
    permission_required = 'dcim.delete_rack'
    model = Rack
    default_return_url = 'dcim:rack_list'


class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.add_rack'
    form = forms.RackImportForm
    table = tables.RackImportTable
    template_name = 'dcim/rack_import.html'
    default_return_url = 'dcim:rack_list'


class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_rack'
    cls = Rack
    filter = filters.RackFilter
    form = forms.RackBulkEditForm
    template_name = 'dcim/rack_bulk_edit.html'
    default_return_url = 'dcim:rack_list'


class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_rack'
    cls = Rack
    filter = filters.RackFilter
    default_return_url = 'dcim:rack_list'


#
# Rack reservations
#

class RackReservationListView(ObjectListView):
    queryset = RackReservation.objects.all()
    filter = filters.RackReservationFilter
    filter_form = forms.RackReservationFilterForm
    table = tables.RackReservationTable
    template_name = 'dcim/rackreservation_list.html'


class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_rackreservation'
    model = RackReservation
    form_class = forms.RackReservationForm

    def alter_obj(self, obj, request, args, kwargs):
        if not obj.pk:
            obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
            obj.user = request.user
        return obj

    def get_return_url(self, request, obj):
        return obj.rack.get_absolute_url()


class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
    permission_required = 'dcim.delete_rackreservation'
    model = RackReservation

    def get_return_url(self, request, obj):
        return obj.rack.get_absolute_url()


class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_rackreservation'
    cls = RackReservation
    default_return_url = 'dcim:rackreservation_list'


#
# Manufacturers
#

class ManufacturerListView(ObjectListView):
    queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
    table = tables.ManufacturerTable
    template_name = 'dcim/manufacturer_list.html'


class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_manufacturer'
    model = Manufacturer
    form_class = forms.ManufacturerForm

    def get_return_url(self, request, obj):
        return reverse('dcim:manufacturer_list')


class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_manufacturer'
    cls = Manufacturer
    default_return_url = 'dcim:manufacturer_list'


#
# Device types
#

class DeviceTypeListView(ObjectListView):
    queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
    filter = filters.DeviceTypeFilter
    filter_form = forms.DeviceTypeFilterForm
    table = tables.DeviceTypeTable
    template_name = 'dcim/devicetype_list.html'


def devicetype(request, pk):

    devicetype = get_object_or_404(DeviceType, pk=pk)

    # Component tables
    consoleport_table = tables.ConsolePortTemplateTable(
        natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
    )
    consoleserverport_table = tables.ConsoleServerPortTemplateTable(
        natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
    )
    powerport_table = tables.PowerPortTemplateTable(
        natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
    )
    poweroutlet_table = tables.PowerOutletTemplateTable(
        natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
    )
    mgmt_interface_table = tables.InterfaceTemplateTable(
        list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
                                                                                             mgmt_only=True))
    )
    interface_table = tables.InterfaceTemplateTable(
        list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
                                                                                             mgmt_only=False))
    )
    devicebay_table = tables.DeviceBayTemplateTable(
        natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
    )
    if request.user.has_perm('dcim.change_devicetype'):
        consoleport_table.base_columns['pk'].visible = True
        consoleserverport_table.base_columns['pk'].visible = True
        powerport_table.base_columns['pk'].visible = True
        poweroutlet_table.base_columns['pk'].visible = True
        mgmt_interface_table.base_columns['pk'].visible = True
        interface_table.base_columns['pk'].visible = True
        devicebay_table.base_columns['pk'].visible = True

    return render(request, 'dcim/devicetype.html', {
        'devicetype': devicetype,
        'consoleport_table': consoleport_table,
        'consoleserverport_table': consoleserverport_table,
        'powerport_table': powerport_table,
        'poweroutlet_table': poweroutlet_table,
        'mgmt_interface_table': mgmt_interface_table,
        'interface_table': interface_table,
        'devicebay_table': devicebay_table,
    })


class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_devicetype'
    model = DeviceType
    form_class = forms.DeviceTypeForm
    template_name = 'dcim/devicetype_edit.html'
    default_return_url = 'dcim:devicetype_list'


class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
    permission_required = 'dcim.delete_devicetype'
    model = DeviceType
    default_return_url = 'dcim:devicetype_list'


class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_devicetype'
    cls = DeviceType
    filter = filters.DeviceTypeFilter
    form = forms.DeviceTypeBulkEditForm
    template_name = 'dcim/devicetype_bulk_edit.html'
    default_return_url = 'dcim:devicetype_list'


class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_devicetype'
    cls = DeviceType
    filter = filters.DeviceTypeFilter
    default_return_url = 'dcim:devicetype_list'


#
# Device type components
#

class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_consoleporttemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = ConsolePortTemplate
    form = forms.ConsolePortTemplateCreateForm
    model_form = forms.ConsolePortTemplateForm


class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_consoleporttemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    cls = ConsolePortTemplate
    parent_cls = DeviceType


class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_consoleserverporttemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = ConsoleServerPortTemplate
    form = forms.ConsoleServerPortTemplateCreateForm
    model_form = forms.ConsoleServerPortTemplateForm


class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_consoleserverporttemplate'
    cls = ConsoleServerPortTemplate
    parent_cls = DeviceType


class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_powerporttemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = PowerPortTemplate
    form = forms.PowerPortTemplateCreateForm
    model_form = forms.PowerPortTemplateForm


class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_powerporttemplate'
    cls = PowerPortTemplate
    parent_cls = DeviceType


class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_poweroutlettemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = PowerOutletTemplate
    form = forms.PowerOutletTemplateCreateForm
    model_form = forms.PowerOutletTemplateForm


class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_poweroutlettemplate'
    cls = PowerOutletTemplate
    parent_cls = DeviceType


class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_interfacetemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = InterfaceTemplate
    form = forms.InterfaceTemplateCreateForm
    model_form = forms.InterfaceTemplateForm


class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_interfacetemplate'
    cls = InterfaceTemplate
    parent_cls = DeviceType
    form = forms.InterfaceTemplateBulkEditForm
    template_name = 'dcim/interfacetemplate_bulk_edit.html'


class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_interfacetemplate'
    cls = InterfaceTemplate
    parent_cls = DeviceType


class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_devicebaytemplate'
    parent_model = DeviceType
    parent_field = 'device_type'
    model = DeviceBayTemplate
    form = forms.DeviceBayTemplateCreateForm
    model_form = forms.DeviceBayTemplateForm


class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_devicebaytemplate'
    cls = DeviceBayTemplate
    parent_cls = DeviceType


#
# Device roles
#

class DeviceRoleListView(ObjectListView):
    queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
    table = tables.DeviceRoleTable
    template_name = 'dcim/devicerole_list.html'


class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_devicerole'
    model = DeviceRole
    form_class = forms.DeviceRoleForm

    def get_return_url(self, request, obj):
        return reverse('dcim:devicerole_list')


class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_devicerole'
    cls = DeviceRole
    default_return_url = 'dcim:devicerole_list'


#
# Platforms
#

class PlatformListView(ObjectListView):
    queryset = Platform.objects.annotate(device_count=Count('devices'))
    table = tables.PlatformTable
    template_name = 'dcim/platform_list.html'


class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_platform'
    model = Platform
    form_class = forms.PlatformForm

    def get_return_url(self, request, obj):
        return reverse('dcim:platform_list')


class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_platform'
    cls = Platform
    default_return_url = 'dcim:platform_list'


#
# Devices
#

class DeviceListView(ObjectListView):
    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack',
                                             'primary_ip4', 'primary_ip6')
    filter = filters.DeviceFilter
    filter_form = forms.DeviceFilterForm
    table = tables.DeviceTable
    template_name = 'dcim/device_list.html'


def device(request, pk):

    device = get_object_or_404(Device.objects.select_related(
        'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
    ), pk=pk)
    console_ports = natsorted(
        ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
    )
    cs_ports = natsorted(
        ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
    )
    power_ports = natsorted(
        PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
    )
    power_outlets = natsorted(
        PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
    )
    interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
        .filter(device=device, mgmt_only=False)\
        .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
                        'circuit_termination__circuit').prefetch_related('ip_addresses')
    mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
        .filter(device=device, mgmt_only=True)\
        .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
                        'circuit_termination__circuit').prefetch_related('ip_addresses')
    device_bays = natsorted(
        DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
        key=attrgetter('name')
    )
    services = Service.objects.filter(device=device)
    secrets = device.secrets.all()

    # Find any related devices for convenient linking in the UI
    related_devices = []
    if device.name:
        if re.match('.+[0-9]+$', device.name):
            # Strip 1 or more trailing digits (e.g. core-switch1)
            base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
        elif re.match('.+\d[a-z]$', device.name.lower()):
            # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
            base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
        else:
            base_name = None
        if base_name:
            related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
                .select_related('rack', 'device_type__manufacturer')[:10]

    # Show graph button on interfaces only if at least one graph has been created.
    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()

    return render(request, 'dcim/device.html', {
        'device': device,
        'console_ports': console_ports,
        'cs_ports': cs_ports,
        'power_ports': power_ports,
        'power_outlets': power_outlets,
        'interfaces': interfaces,
        'mgmt_interfaces': mgmt_interfaces,
        'device_bays': device_bays,
        'services': services,
        'secrets': secrets,
        'related_devices': related_devices,
        'show_graphs': show_graphs,
    })


class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
    permission_required = 'dcim.change_device'
    model = Device
    form_class = forms.DeviceForm
    template_name = 'dcim/device_edit.html'
    default_return_url = 'dcim:device_list'


class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
    permission_required = 'dcim.delete_device'
    model = Device
    default_return_url = 'dcim:device_list'


class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.add_device'
    form = forms.DeviceImportForm
    table = tables.DeviceImportTable
    template_name = 'dcim/device_import.html'
    default_return_url = 'dcim:device_list'


class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.add_device'
    form = forms.ChildDeviceImportForm
    table = tables.DeviceImportTable
    template_name = 'dcim/device_import_child.html'
    default_return_url = 'dcim:device_list'

    def save_obj(self, obj):

        # Inherit site and rack from parent device
        obj.site = obj.parent_bay.device.site
        obj.rack = obj.parent_bay.device.rack
        obj.save()

        # Save the reverse relation
        device_bay = obj.parent_bay
        device_bay.installed_device = obj
        device_bay.save()


class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_device'
    cls = Device
    filter = filters.DeviceFilter
    form = forms.DeviceBulkEditForm
    template_name = 'dcim/device_bulk_edit.html'
    default_return_url = 'dcim:device_list'


class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_device'
    cls = Device
    filter = filters.DeviceFilter
    default_return_url = 'dcim:device_list'


def device_inventory(request, pk):

    device = get_object_or_404(Device, pk=pk)
    inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\
        .prefetch_related('child_items')

    return render(request, 'dcim/device_inventory.html', {
        'device': device,
        'inventory_items': inventory_items,
    })


def device_lldp_neighbors(request, pk):

    device = get_object_or_404(Device, pk=pk)
    interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
        .select_related('connected_as_a', 'connected_as_b')

    return render(request, 'dcim/device_lldp_neighbors.html', {
        'device': device,
        'interfaces': interfaces,
    })


#
# Console ports
#

class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_consoleport'
    parent_model = Device
    parent_field = 'device'
    model = ConsolePort
    form = forms.ConsolePortCreateForm
    model_form = forms.ConsolePortForm


@permission_required('dcim.change_consoleport')
def consoleport_connect(request, pk):

    consoleport = get_object_or_404(ConsolePort, pk=pk)

    if request.method == 'POST':
        form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
        if form.is_valid():
            consoleport = form.save()
            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
                consoleport.device.get_absolute_url(),
                escape(consoleport.device),
                escape(consoleport.name),
                consoleport.cs_port.device.get_absolute_url(),
                escape(consoleport.cs_port.device),
                escape(consoleport.cs_port.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, consoleport, msg)
            return redirect('dcim:device', pk=consoleport.device.pk)

    else:
        form = forms.ConsolePortConnectionForm(instance=consoleport, initial={
            'site': request.GET.get('site', consoleport.device.site),
            'rack': request.GET.get('rack', None),
            'console_server': request.GET.get('console_server', None),
            'connection_status': CONNECTION_STATUS_CONNECTED,
        })

    return render(request, 'dcim/consoleport_connect.html', {
        'consoleport': consoleport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
    })


@permission_required('dcim.change_consoleport')
def consoleport_disconnect(request, pk):

    consoleport = get_object_or_404(ConsolePort, pk=pk)

    if not consoleport.cs_port:
        messages.warning(
            request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
        )
        return redirect('dcim:device', pk=consoleport.device.pk)

    if request.method == 'POST':
        form = ConfirmationForm(request.POST)
        if form.is_valid():
            cs_port = consoleport.cs_port
            consoleport.cs_port = None
            consoleport.connection_status = None
            consoleport.save()
            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
                consoleport.device.get_absolute_url(),
                escape(consoleport.device),
                escape(consoleport.name),
                cs_port.device.get_absolute_url(),
                escape(cs_port.device),
                escape(cs_port.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, consoleport, msg)
            return redirect('dcim:device', pk=consoleport.device.pk)

    else:
        form = ConfirmationForm()

    return render(request, 'dcim/consoleport_disconnect.html', {
        'consoleport': consoleport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
    })


class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_consoleport'
    model = ConsolePort
    form_class = forms.ConsolePortForm


class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_consoleport'
    model = ConsolePort


class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_consoleport'
    cls = ConsolePort
    parent_cls = Device


class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.change_consoleport'
    form = forms.ConsoleConnectionImportForm
    table = tables.ConsoleConnectionTable
    template_name = 'dcim/console_connections_import.html'
    default_return_url = 'dcim:console_connections_list'


#
# Console server ports
#

class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_consoleserverport'
    parent_model = Device
    parent_field = 'device'
    model = ConsoleServerPort
    form = forms.ConsoleServerPortCreateForm
    model_form = forms.ConsoleServerPortForm


@permission_required('dcim.change_consoleserverport')
def consoleserverport_connect(request, pk):

    consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)

    if request.method == 'POST':
        form = forms.ConsoleServerPortConnectionForm(request.POST)
        if form.is_valid():
            consoleport = form.cleaned_data['port']
            consoleport.cs_port = consoleserverport
            consoleport.connection_status = form.cleaned_data['connection_status']
            consoleport.save()
            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
                consoleport.device.get_absolute_url(),
                escape(consoleport.device),
                escape(consoleport.name),
                consoleserverport.device.get_absolute_url(),
                escape(consoleserverport.device),
                escape(consoleserverport.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, consoleport, msg)
            return redirect('dcim:device', pk=consoleserverport.device.pk)

    else:
        form = forms.ConsoleServerPortConnectionForm(initial={
            'site': request.GET.get('site', consoleserverport.device.site),
            'rack': request.GET.get('rack', None),
            'device': request.GET.get('device', None),
            'connection_status': CONNECTION_STATUS_CONNECTED,
        })

    return render(request, 'dcim/consoleserverport_connect.html', {
        'consoleserverport': consoleserverport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
    })


@permission_required('dcim.change_consoleserverport')
def consoleserverport_disconnect(request, pk):

    consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)

    if not hasattr(consoleserverport, 'connected_console'):
        messages.warning(
            request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
        )
        return redirect('dcim:device', pk=consoleserverport.device.pk)

    if request.method == 'POST':
        form = ConfirmationForm(request.POST)
        if form.is_valid():
            consoleport = consoleserverport.connected_console
            consoleport.cs_port = None
            consoleport.connection_status = None
            consoleport.save()
            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
                consoleport.device.get_absolute_url(),
                escape(consoleport.device),
                escape(consoleport.name),
                consoleserverport.device.get_absolute_url(),
                escape(consoleserverport.device),
                escape(consoleserverport.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, consoleport, msg)
            return redirect('dcim:device', pk=consoleserverport.device.pk)

    else:
        form = ConfirmationForm()

    return render(request, 'dcim/consoleserverport_disconnect.html', {
        'consoleserverport': consoleserverport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
    })


class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_consoleserverport'
    model = ConsoleServerPort
    form_class = forms.ConsoleServerPortForm


class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_consoleserverport'
    model = ConsoleServerPort


class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_consoleserverport'
    cls = ConsoleServerPort
    parent_cls = Device


#
# Power ports
#

class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_powerport'
    parent_model = Device
    parent_field = 'device'
    model = PowerPort
    form = forms.PowerPortCreateForm
    model_form = forms.PowerPortForm


@permission_required('dcim.change_powerport')
def powerport_connect(request, pk):

    powerport = get_object_or_404(PowerPort, pk=pk)

    if request.method == 'POST':
        form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
        if form.is_valid():
            powerport = form.save()
            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
                powerport.device.get_absolute_url(),
                escape(powerport.device),
                escape(powerport.name),
                powerport.power_outlet.device.get_absolute_url(),
                escape(powerport.power_outlet.device),
                escape(powerport.power_outlet.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, powerport, msg)
            return redirect('dcim:device', pk=powerport.device.pk)

    else:
        form = forms.PowerPortConnectionForm(instance=powerport, initial={
            'site': request.GET.get('site', powerport.device.site),
            'rack': request.GET.get('rack', None),
            'pdu': request.GET.get('pdu', None),
            'connection_status': CONNECTION_STATUS_CONNECTED,
        })

    return render(request, 'dcim/powerport_connect.html', {
        'powerport': powerport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
    })


@permission_required('dcim.change_powerport')
def powerport_disconnect(request, pk):

    powerport = get_object_or_404(PowerPort, pk=pk)

    if not powerport.power_outlet:
        messages.warning(
            request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
        )
        return redirect('dcim:device', pk=powerport.device.pk)

    if request.method == 'POST':
        form = ConfirmationForm(request.POST)
        if form.is_valid():
            power_outlet = powerport.power_outlet
            powerport.power_outlet = None
            powerport.connection_status = None
            powerport.save()
            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
                powerport.device.get_absolute_url(),
                escape(powerport.device),
                escape(powerport.name),
                power_outlet.device.get_absolute_url(),
                escape(power_outlet.device),
                escape(power_outlet.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, powerport, msg)
            return redirect('dcim:device', pk=powerport.device.pk)

    else:
        form = ConfirmationForm()

    return render(request, 'dcim/powerport_disconnect.html', {
        'powerport': powerport,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
    })


class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_powerport'
    model = PowerPort
    form_class = forms.PowerPortForm


class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_powerport'
    model = PowerPort


class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_powerport'
    cls = PowerPort
    parent_cls = Device


class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.change_powerport'
    form = forms.PowerConnectionImportForm
    table = tables.PowerConnectionTable
    template_name = 'dcim/power_connections_import.html'
    default_return_url = 'dcim:power_connections_list'


#
# Power outlets
#

class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_poweroutlet'
    parent_model = Device
    parent_field = 'device'
    model = PowerOutlet
    form = forms.PowerOutletCreateForm
    model_form = forms.PowerOutletForm


@permission_required('dcim.change_poweroutlet')
def poweroutlet_connect(request, pk):

    poweroutlet = get_object_or_404(PowerOutlet, pk=pk)

    if request.method == 'POST':
        form = forms.PowerOutletConnectionForm(request.POST)
        if form.is_valid():
            powerport = form.cleaned_data['port']
            powerport.power_outlet = poweroutlet
            powerport.connection_status = form.cleaned_data['connection_status']
            powerport.save()
            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
                powerport.device.get_absolute_url(),
                escape(powerport.device),
                escape(powerport.name),
                poweroutlet.device.get_absolute_url(),
                escape(poweroutlet.device),
                escape(poweroutlet.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, powerport, msg)
            return redirect('dcim:device', pk=poweroutlet.device.pk)

    else:
        form = forms.PowerOutletConnectionForm(initial={
            'site': request.GET.get('site', poweroutlet.device.site),
            'rack': request.GET.get('rack', None),
            'device': request.GET.get('device', None),
            'connection_status': CONNECTION_STATUS_CONNECTED,
        })

    return render(request, 'dcim/poweroutlet_connect.html', {
        'poweroutlet': poweroutlet,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
    })


@permission_required('dcim.change_poweroutlet')
def poweroutlet_disconnect(request, pk):

    poweroutlet = get_object_or_404(PowerOutlet, pk=pk)

    if not hasattr(poweroutlet, 'connected_port'):
        messages.warning(
            request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
        )
        return redirect('dcim:device', pk=poweroutlet.device.pk)

    if request.method == 'POST':
        form = ConfirmationForm(request.POST)
        if form.is_valid():
            powerport = poweroutlet.connected_port
            powerport.power_outlet = None
            powerport.connection_status = None
            powerport.save()
            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
                powerport.device.get_absolute_url(),
                escape(powerport.device),
                escape(powerport.name),
                poweroutlet.device.get_absolute_url(),
                escape(poweroutlet.device),
                escape(poweroutlet.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, powerport, msg)
            return redirect('dcim:device', pk=poweroutlet.device.pk)

    else:
        form = ConfirmationForm()

    return render(request, 'dcim/poweroutlet_disconnect.html', {
        'poweroutlet': poweroutlet,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
    })


class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_poweroutlet'
    model = PowerOutlet
    form_class = forms.PowerOutletForm


class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_poweroutlet'
    model = PowerOutlet


class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_poweroutlet'
    cls = PowerOutlet
    parent_cls = Device


#
# Interfaces
#

class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_interface'
    parent_model = Device
    parent_field = 'device'
    model = Interface
    form = forms.InterfaceCreateForm
    model_form = forms.InterfaceForm


class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_interface'
    model = Interface
    form_class = forms.InterfaceForm


class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_interface'
    model = Interface


class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
    permission_required = 'dcim.change_interface'
    cls = Interface
    parent_cls = Device
    form = forms.InterfaceBulkEditForm
    template_name = 'dcim/interface_bulk_edit.html'


class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_interface'
    cls = Interface
    parent_cls = Device


#
# Device bays
#

class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
    permission_required = 'dcim.add_devicebay'
    parent_model = Device
    parent_field = 'device'
    model = DeviceBay
    form = forms.DeviceBayCreateForm
    model_form = forms.DeviceBayForm


class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_devicebay'
    model = DeviceBay
    form_class = forms.DeviceBayForm


class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_devicebay'
    model = DeviceBay


@permission_required('dcim.change_devicebay')
def devicebay_populate(request, pk):

    device_bay = get_object_or_404(DeviceBay, pk=pk)

    if request.method == 'POST':
        form = forms.PopulateDeviceBayForm(device_bay, request.POST)
        if form.is_valid():

            device_bay.installed_device = form.cleaned_data['installed_device']
            device_bay.save()

            if not form.errors:
                messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay))
                return redirect('dcim:device', pk=device_bay.device.pk)

    else:
        form = forms.PopulateDeviceBayForm(device_bay)

    return render(request, 'dcim/devicebay_populate.html', {
        'device_bay': device_bay,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
    })


@permission_required('dcim.change_devicebay')
def devicebay_depopulate(request, pk):

    device_bay = get_object_or_404(DeviceBay, pk=pk)

    if request.method == 'POST':
        form = ConfirmationForm(request.POST)
        if form.is_valid():
            removed_device = device_bay.installed_device
            device_bay.installed_device = None
            device_bay.save()
            messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay))
            return redirect('dcim:device', pk=device_bay.device.pk)

    else:
        form = ConfirmationForm()

    return render(request, 'dcim/devicebay_depopulate.html', {
        'device_bay': device_bay,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
    })


class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
    permission_required = 'dcim.delete_devicebay'
    cls = DeviceBay
    parent_cls = Device


#
# Bulk device component creation
#

class DeviceBulkAddComponentView(View):
    """
    Add one or more components (e.g. interfaces) to a selected set of Devices.
    """
    form = forms.DeviceBulkAddComponentForm
    model = None
    model_form = None

    def get(self):
        return redirect('dcim:device_list')

    def post(self, request):

        # Are we editing *all* objects in the queryset or just a selected subset?
        if request.POST.get('_all'):
            pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
        else:
            pk_list = [int(pk) for pk in request.POST.getlist('pk')]

        if '_create' in request.POST:
            form = self.form(request.POST)
            if form.is_valid():

                new_components = []
                data = deepcopy(form.cleaned_data)
                for device in data['pk']:

                    names = data['name_pattern']
                    for name in names:
                        component_data = {
                            'device': device.pk,
                            'name': name,
                        }
                        component_data.update(data)
                        component_form = self.model_form(component_data)
                        if component_form.is_valid():
                            new_components.append(component_form.save(commit=False))
                        else:
                            for field, errors in component_form.errors.as_data().items():
                                for e in errors:
                                    form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))

                if not form.errors:
                    self.model.objects.bulk_create(new_components)
                    messages.success(request, u"Added {} {} to {} devices.".format(
                        len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
                    ))
                    return redirect('dcim:device_list')

        else:
            form = self.form(initial={'pk': pk_list})

        selected_devices = Device.objects.filter(pk__in=pk_list)
        if not selected_devices:
            messages.warning(request, u"No devices were selected.")
            return redirect('dcim:device_list')

        return render(request, 'dcim/device_bulk_add_component.html', {
            'form': form,
            'component_name': self.model._meta.verbose_name_plural,
            'selected_devices': selected_devices,
            'return_url': reverse('dcim:device_list'),
        })


class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_consoleport'
    model = ConsolePort
    model_form = forms.ConsolePortForm


class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_consoleserverport'
    model = ConsoleServerPort
    model_form = forms.ConsoleServerPortForm


class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_powerport'
    model = PowerPort
    model_form = forms.PowerPortForm


class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_poweroutlet'
    model = PowerOutlet
    model_form = forms.PowerOutletForm


class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_interface'
    form = forms.DeviceBulkAddInterfaceForm
    model = Interface
    model_form = forms.InterfaceForm


class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
    permission_required = 'dcim.add_devicebay'
    model = DeviceBay
    model_form = forms.DeviceBayForm


#
# Interface connections
#

@permission_required('dcim.add_interfaceconnection')
def interfaceconnection_add(request, pk):

    device = get_object_or_404(Device, pk=pk)

    if request.method == 'POST':
        form = forms.InterfaceConnectionForm(device, request.POST)
        if form.is_valid():

            interfaceconnection = form.save()
            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
                interfaceconnection.interface_a.device.get_absolute_url(),
                escape(interfaceconnection.interface_a.device),
                escape(interfaceconnection.interface_a.name),
                interfaceconnection.interface_b.device.get_absolute_url(),
                escape(interfaceconnection.interface_b.device),
                escape(interfaceconnection.interface_b.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, interfaceconnection, msg)

            if '_addanother' in request.POST:
                base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
                device_b = interfaceconnection.interface_b.device
                params = urlencode({
                    'rack_b': device_b.rack.pk if device_b.rack else '',
                    'device_b': device_b.pk,
                })
                return HttpResponseRedirect('{}?{}'.format(base_url, params))
            else:
                return redirect('dcim:device', pk=device.pk)

    else:
        form = forms.InterfaceConnectionForm(device, initial={
            'interface_a': request.GET.get('interface_a', None),
            'site_b': request.GET.get('site_b', device.site),
            'rack_b': request.GET.get('rack_b', None),
            'device_b': request.GET.get('device_b', None),
            'interface_b': request.GET.get('interface_b', None),
        })

    return render(request, 'dcim/interfaceconnection_edit.html', {
        'device': device,
        'form': form,
        'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
    })


@permission_required('dcim.delete_interfaceconnection')
def interfaceconnection_delete(request, pk):

    interfaceconnection = get_object_or_404(InterfaceConnection, pk=pk)
    device_id = request.GET.get('device', None)

    if request.method == 'POST':
        form = forms.InterfaceConnectionDeletionForm(request.POST)
        if form.is_valid():
            interfaceconnection.delete()
            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
                interfaceconnection.interface_a.device.get_absolute_url(),
                escape(interfaceconnection.interface_a.device),
                escape(interfaceconnection.interface_a.name),
                interfaceconnection.interface_b.device.get_absolute_url(),
                escape(interfaceconnection.interface_b.device),
                escape(interfaceconnection.interface_b.name),
            )
            messages.success(request, mark_safe(msg))
            UserAction.objects.log_edit(request.user, interfaceconnection, msg)
            if form.cleaned_data['device']:
                return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
            else:
                return redirect('dcim:device_list')

    else:
        form = forms.InterfaceConnectionDeletionForm(initial={
            'device': device_id,
        })

    # Determine where to direct user upon cancellation
    if device_id:
        return_url = reverse('dcim:device', kwargs={'pk': device_id})
    else:
        return_url = reverse('dcim:device_list')

    return render(request, 'dcim/interfaceconnection_delete.html', {
        'interfaceconnection': interfaceconnection,
        'device_id': device_id,
        'form': form,
        'return_url': return_url,
    })


class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
    permission_required = 'dcim.change_interface'
    form = forms.InterfaceConnectionImportForm
    table = tables.InterfaceConnectionTable
    template_name = 'dcim/interface_connections_import.html'
    default_return_url = 'dcim:interface_connections_list'


#
# Connections
#

class ConsoleConnectionsListView(ObjectListView):
    queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)\
        .order_by('cs_port__device__name', 'cs_port__name')
    filter = filters.ConsoleConnectionFilter
    filter_form = forms.ConsoleConnectionFilterForm
    table = tables.ConsoleConnectionTable
    template_name = 'dcim/console_connections_list.html'


class PowerConnectionsListView(ObjectListView):
    queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)\
        .order_by('power_outlet__device__name', 'power_outlet__name')
    filter = filters.PowerConnectionFilter
    filter_form = forms.PowerConnectionFilterForm
    table = tables.PowerConnectionTable
    template_name = 'dcim/power_connections_list.html'


class InterfaceConnectionsListView(ObjectListView):
    queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')\
        .order_by('interface_a__device__name', 'interface_a__name')
    filter = filters.InterfaceConnectionFilter
    filter_form = forms.InterfaceConnectionFilterForm
    table = tables.InterfaceConnectionTable
    template_name = 'dcim/interface_connections_list.html'


#
# Inventory items
#

class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
    permission_required = 'dcim.change_inventoryitem'
    model = InventoryItem
    form_class = forms.InventoryItemForm

    def alter_obj(self, obj, request, url_args, url_kwargs):
        if 'device' in url_kwargs:
            obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
        return obj


class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
    permission_required = 'dcim.delete_inventoryitem'
    model = InventoryItem
