Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documents for power panels #58

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A plugin designed to faciliate the storage of site, circuit, device type and dev
- Locations
- Virtual Machines
- Circuit Providers
- Power Panels

* Upload documents to your NetBox media/ folder or other Django supported storage method e.g. S3
* Supports a wide array of common file types (bmp, gif, jpeg, jpg, png, pdf, txt, doc, docx, xls, xlsx, xlsm)
Expand Down
2 changes: 2 additions & 0 deletions netbox_documents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class NetboxDocuments(PluginConfig):
"enable_device_type_documents": True,
"enable_vm_documents": True,
"enable_circuit_provider_documents": True,
"enable_power_panel_documents": True,
"enable_navigation_menu": True,
"site_documents_location": "left",
"location_documents_location": "left",
Expand All @@ -25,6 +26,7 @@ class NetboxDocuments(PluginConfig):
"device_type_documents_location": "left",
"vm_documents_location": "left",
"circuit_provider_documents_location": "left",
"power_panel_documents_location": "left",
}

config = NetboxDocuments
35 changes: 32 additions & 3 deletions netbox_documents/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from rest_framework import serializers

from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from ..models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument, CircuitProviderDocument
from dcim.api.nested_serializers import NestedSiteSerializer, NestedLocationSerializer, NestedDeviceSerializer, NestedDeviceTypeSerializer
from ..models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument, CircuitProviderDocument, PowerPanelDocument
from dcim.api.nested_serializers import NestedSiteSerializer, NestedLocationSerializer, NestedDeviceSerializer, NestedDeviceTypeSerializer, NestedPowerPanelSerializer
from circuits.api.nested_serializers import NestedCircuitSerializer, NestedProviderSerializer
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .fields import UploadableBase64FileField
Expand Down Expand Up @@ -199,4 +199,33 @@ class Meta:
model = CircuitProviderDocument
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename',
)
)

# Power Panel Document Serializer
class PowerPanelDocumentSerializer(NetBoxModelSerializer):

url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_documents-api:powerpaneldocument-detail'
)

powerpanel = NestedPowerPanelSerializer()
document = UploadableBase64FileField(required=False)

class Meta:
model = PowerPanelDocument
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename', 'powerpanel', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
)

class NestedPowerPanelDocumentSerializer(WritableNestedSerializer):

url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_documents-api:powerpanel-detail'
)

class Meta:
model = PowerPanelDocument
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename',
)
1 change: 1 addition & 0 deletions netbox_documents/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
router.register('circuit-documents', views.CircuitDocumentViewSet)
router.register('vm-documents', views.VMDocumentViewSet)
router.register('circuitprovider-documents', views.CircuitProviderDocumentViewSet)
router.register('powerpanel-documents', views.PowerPanelDocumentViewSet)

urlpatterns = router.urls
9 changes: 7 additions & 2 deletions netbox_documents/api/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from netbox.api.viewsets import NetBoxModelViewSet

from .. import models, filtersets
from .serializers import SiteDocumentSerializer, LocationDocumentSerializer, DeviceDocumentSerializer, DeviceTypeDocumentSerializer, CircuitDocumentSerializer, VMDocumentSerializer, CircuitProviderDocumentSerializer
from .serializers import SiteDocumentSerializer, LocationDocumentSerializer, DeviceDocumentSerializer, DeviceTypeDocumentSerializer, CircuitDocumentSerializer, VMDocumentSerializer, CircuitProviderDocumentSerializer, PowerPanelDocumentSerializer

class SiteDocumentViewSet(NetBoxModelViewSet):
queryset = models.SiteDocument.objects.prefetch_related('tags')
Expand Down Expand Up @@ -36,4 +36,9 @@ class VMDocumentViewSet(NetBoxModelViewSet):
class CircuitProviderDocumentViewSet(NetBoxModelViewSet):
queryset = models.CircuitProviderDocument.objects.prefetch_related('tags')
serializer_class = CircuitProviderDocumentSerializer
filterset_class = filtersets.CircuitProviderDocumentFilterSet
filterset_class = filtersets.CircuitProviderDocumentFilterSet

class PowerPanelDocumentViewSet(NetBoxModelViewSet):
queryset = models.PowerPanelDocument.objects.prefetch_related('tags')
serializer_class = PowerPanelDocumentSerializer
filterset_class = filtersets.PowerPanelDocumentFilterSet
20 changes: 17 additions & 3 deletions netbox_documents/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from netbox.filtersets import NetBoxModelFilterSet
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument, CircuitProviderDocument
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument, CircuitProviderDocument, PowerPanelDocument
from django.db.models import Q

class SiteDocumentFilterSet(NetBoxModelFilterSet):
Expand Down Expand Up @@ -86,7 +86,7 @@ def search(self, queryset, name, value):
Q(name__icontains=value) |
Q(document__icontains=value)
)

class CircuitProviderDocumentFilterSet(NetBoxModelFilterSet):

class Meta:
Expand All @@ -99,4 +99,18 @@ def search(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=value) |
Q(document__icontains=value)
)
)

class PowerPanelDocumentFilterSet(NetBoxModelFilterSet):

class Meta:
model = PowerPanelDocument
fields = ('id', 'name', 'document_type', 'powerpanel')

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(document__icontains=value)
)
37 changes: 34 additions & 3 deletions netbox_documents/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django import forms
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
from dcim.models import Site, Location, Device, DeviceType
from dcim.models import Site, Location, Device, DeviceType, PowerPanel
from virtualization.models import VirtualMachine
from circuits.models import Circuit, Provider
from utilities.forms.fields import TagFilterField, CommentField, DynamicModelChoiceField
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, CircuitDocTypeChoices, SiteDocTypeChoices, LocationDocTypeChoices, DeviceDocTypeChoices, DeviceTypeDocTypeChoices, VMDocument, VMDocTypeChoices, CircuitProviderDocument, CircuitProviderDocTypeChoices
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, CircuitDocTypeChoices, SiteDocTypeChoices, LocationDocTypeChoices, DeviceDocTypeChoices, DeviceTypeDocTypeChoices, VMDocument, VMDocTypeChoices, CircuitProviderDocument, CircuitProviderDocTypeChoices, PowerPanelDocument, PowerPanelDocTypeChoices


#### Site Document Form & Filter Form
Expand Down Expand Up @@ -241,4 +241,35 @@ class CircuitProviderDocumentFilterForm(NetBoxModelFilterSetForm):
required=False
)

tag = TagFilterField(model)
tag = TagFilterField(model)

#### Power Panel Document Form & Filter Form
class PowerPanelDocumentForm(NetBoxModelForm):
comments = CommentField()

powerpanel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all()
)

class Meta:
model = PowerPanelDocument
fields = ('name', 'document', 'external_url', 'document_type', 'powerpanel', 'comments', 'tags')

class PowerPanelDocumentFilterForm(NetBoxModelFilterSetForm):
model = PowerPanelDocument

name = forms.CharField(
required=False
)

powerpanel = forms.ModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
required=False
)

document_type = forms.MultipleChoiceField(
choices=PowerPanelDocTypeChoices,
required=False
)

tag = TagFilterField(model)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
class Migration(migrations.Migration):

dependencies = [
('extras', '0107_cachedvalue_extras_cachedvalue_object'),
('circuits', '0043_circuittype_color'),
('netbox_documents', '0006_vmdocument'),
]
Expand Down
40 changes: 40 additions & 0 deletions netbox_documents/migrations/0008_powerpaneldocument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 4.2.9 on 2024-04-18 13:16

from django.db import migrations, models
import django.db.models.deletion
import netbox_documents.utils
import taggit.managers
import utilities.json


class Migration(migrations.Migration):

dependencies = [
('dcim', '0185_gfk_indexes'),
('extras', '0106_bookmark_user_cascade_deletion'),
('netbox_documents', '0007_circuitproviderdocument'),
]

operations = [
migrations.CreateModel(
name='PowerPanelDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('name', models.CharField(blank=True, max_length=100)),
('document', models.FileField(blank=True, upload_to=netbox_documents.utils.file_upload)),
('external_url', models.URLField(blank=True, max_length=255)),
('document_type', models.CharField(max_length=30)),
('comments', models.TextField(blank=True)),
('powerpanel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='dcim.powerpanel')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'Power Panel Document',
'verbose_name_plural': 'Power Panel Documents',
'ordering': ('name',),
},
),
]
117 changes: 116 additions & 1 deletion netbox_documents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ class VMDocTypeChoices(ChoiceSet):
('other', 'Other', 'gray'),
]

class PowerPanelDocTypeChoices(ChoiceSet):

key = 'DocTypeChoices.circuit'

CHOICES = [
('schematic', 'Electrical Schematic', 'red'),
('other', 'Other', 'gray'),
]

class SiteDocument(NetBoxModel):
name = models.CharField(
max_length=100,
Expand Down Expand Up @@ -855,4 +864,110 @@ def delete(self, *args, **kwargs):
self.document.name = _name
else:
# Straight delete of external URL
super().delete(*args, **kwargs)
super().delete(*args, **kwargs)


class PowerPanelDocument(NetBoxModel):
name = models.CharField(
max_length=100,
blank=True,
help_text='(Optional) Specify a name to display for this document. If no name is specified, the filename will be used.'
)

document = models.FileField(
upload_to=file_upload,
blank=True
)

external_url = models.URLField(
blank=True,
max_length=255
)

document_type = models.CharField(
max_length=30,
choices=PowerPanelDocTypeChoices
)

powerpanel = models.ForeignKey(
to='dcim.PowerPanel',
on_delete=models.CASCADE,
related_name='documents'
)

comments = models.TextField(
blank=True
)

def get_document_type_color(self):
return PowerPanelDocTypeChoices.colors.get(self.document_type)

class Meta:
ordering = ('name',)
verbose_name_plural = "Power Panel Documents"
verbose_name = "Power Panel Document"

@property
def size(self):
"""
Wrapper around `document.size` to suppress an OSError in case the file is inaccessible. Also opportunistically
catch other exceptions that we know other storage back-ends to throw.
"""
expected_exceptions = [OSError]

try:
from botocore.exceptions import ClientError
expected_exceptions.append(ClientError)
except ImportError:
pass

try:
return self.document.size
except:
return None

@property
def filename(self):
if self.external_url:
return self.external_url
filename = self.document.name.rsplit('/', 1)[-1]
return filename.split('_', 1)[1]

def __str__(self):
if self.name:
return self.name

if self.external_url:
return self.external_url

filename = self.document.name.rsplit('/', 1)[-1]
return filename.split('_', 1)[1]

def get_absolute_url(self):
return reverse('plugins:netbox_documents:powerpaneldocument', args=[self.pk])

def clean(self):
super().clean()

# Must have an uploaded document or an external URL. cannot have both
if not self.document and self.external_url == '':
raise ValidationError("A document must contain an uploaded file or an external URL.")
if self.document and self.external_url:
raise ValidationError("A document cannot contain both an uploaded file and an external URL.")

def delete(self, *args, **kwargs):

# Check if its a document or a URL
if self.external_url == '':

_name = self.document.name

# Delete file from disk
super().delete(*args, **kwargs)
self.document.delete(save=False)

# Restore the name of the document as it's re-used in the notifications later
self.document.name = _name
else:
# Straight delete of external URL
super().delete(*args, **kwargs)
15 changes: 15 additions & 0 deletions netbox_documents/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@
)
)

# Add a menu item for Power Panel Documents if enabled
if plugin_settings.get('enable_power_panel_documents'):
menuitem.append(
PluginMenuItem(
link='plugins:netbox_documents:powerpaneldocument_list',
link_text='Power Panel Documents',
buttons=[PluginMenuButton(
link='plugins:netbox_documents:powerpaneldocument_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
)]
)
)

else:

# Fall back to pre 3.4 navigation option
Expand Down
Loading