netbox-cadplan 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netbox_cadplan/__init__.py +28 -0
- netbox_cadplan/api/__init__.py +0 -0
- netbox_cadplan/api/serializers.py +125 -0
- netbox_cadplan/api/urls.py +12 -0
- netbox_cadplan/api/views.py +30 -0
- netbox_cadplan/choices.py +41 -0
- netbox_cadplan/filtersets.py +20 -0
- netbox_cadplan/forms.py +92 -0
- netbox_cadplan/locale/fr/LC_MESSAGES/django.mo +0 -0
- netbox_cadplan/locale/fr/LC_MESSAGES/django.po +408 -0
- netbox_cadplan/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
- netbox_cadplan/locale/fr/LC_MESSAGES/djangojs.po +236 -0
- netbox_cadplan/migrations/0001_initial.py +265 -0
- netbox_cadplan/migrations/0002_placedobject_outside_wall.py +18 -0
- netbox_cadplan/migrations/0003_planzone_source_polygon.py +18 -0
- netbox_cadplan/migrations/__init__.py +0 -0
- netbox_cadplan/models.py +261 -0
- netbox_cadplan/navigation.py +29 -0
- netbox_cadplan/signals.py +16 -0
- netbox_cadplan/static/netbox_cadplan/css/plan.css +0 -0
- netbox_cadplan/static/netbox_cadplan/js/plan_editor.js +2840 -0
- netbox_cadplan/tables.py +24 -0
- netbox_cadplan/template_content.py +19 -0
- netbox_cadplan/templates/netbox_cadplan/devicetype_shape_edit.html +46 -0
- netbox_cadplan/templates/netbox_cadplan/inc/devicetype_plan_shape_panel.html +34 -0
- netbox_cadplan/templates/netbox_cadplan/location_tab.html +82 -0
- netbox_cadplan/templates/netbox_cadplan/plan.html +180 -0
- netbox_cadplan/templates/netbox_cadplan/plan_tab.html +109 -0
- netbox_cadplan/urls.py +64 -0
- netbox_cadplan/utils.py +630 -0
- netbox_cadplan/views.py +1029 -0
- netbox_cadplan-0.2.0.dist-info/METADATA +124 -0
- netbox_cadplan-0.2.0.dist-info/RECORD +36 -0
- netbox_cadplan-0.2.0.dist-info/WHEEL +5 -0
- netbox_cadplan-0.2.0.dist-info/licenses/LICENSE +21 -0
- netbox_cadplan-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from netbox.plugins import PluginConfig
|
|
3
|
+
except Exception:
|
|
4
|
+
# Minimal fallback for local development / tests when NetBox is not
|
|
5
|
+
# available. This avoids import-time errors during pytest collection.
|
|
6
|
+
class PluginConfig: # type: ignore
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NetBoxCadPlanConfig(PluginConfig):
|
|
11
|
+
name = "netbox_cadplan"
|
|
12
|
+
verbose_name = "NetBox CadPlan"
|
|
13
|
+
description = "Visual floor-plan management linked to Sites and Locations"
|
|
14
|
+
version = "0.2.0"
|
|
15
|
+
author = "Simon Lacroix"
|
|
16
|
+
author_email = "simonlacroix@live.ca"
|
|
17
|
+
base_url = "cadplan"
|
|
18
|
+
min_version = "4.6.0"
|
|
19
|
+
max_version = "4.6.99"
|
|
20
|
+
required_settings = []
|
|
21
|
+
default_settings = {}
|
|
22
|
+
|
|
23
|
+
def ready(self):
|
|
24
|
+
super().ready()
|
|
25
|
+
from . import signals # noqa: F401
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
config = NetBoxCadPlanConfig
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from core.models import ObjectType
|
|
2
|
+
from dcim.api.serializers import (
|
|
3
|
+
DeviceTypeSerializer,
|
|
4
|
+
LocationSerializer,
|
|
5
|
+
SiteSerializer,
|
|
6
|
+
)
|
|
7
|
+
from netbox.api.fields import ContentTypeField
|
|
8
|
+
from netbox.api.gfk_fields import GFKSerializerField
|
|
9
|
+
from netbox.api.serializers import NetBoxModelSerializer
|
|
10
|
+
from rest_framework import serializers
|
|
11
|
+
|
|
12
|
+
from ..models import DeviceTypeShape, PlacedObject, Plan, PlanZone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PlanSerializer(NetBoxModelSerializer):
|
|
16
|
+
url = serializers.HyperlinkedIdentityField(
|
|
17
|
+
view_name="plugins-api:netbox_cadplan-api:plan-detail"
|
|
18
|
+
)
|
|
19
|
+
site = SiteSerializer(nested=True)
|
|
20
|
+
location = LocationSerializer(nested=True, required=False, allow_null=True)
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
model = Plan
|
|
24
|
+
fields = [
|
|
25
|
+
"id",
|
|
26
|
+
"url",
|
|
27
|
+
"display",
|
|
28
|
+
"name",
|
|
29
|
+
"site",
|
|
30
|
+
"location",
|
|
31
|
+
"dxf_file",
|
|
32
|
+
"selected_layer",
|
|
33
|
+
"width_px",
|
|
34
|
+
"height_px",
|
|
35
|
+
"mm_per_px",
|
|
36
|
+
"tags",
|
|
37
|
+
"custom_fields",
|
|
38
|
+
"created",
|
|
39
|
+
"last_updated",
|
|
40
|
+
]
|
|
41
|
+
brief_fields = ("id", "url", "display", "name")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PlanZoneSerializer(NetBoxModelSerializer):
|
|
45
|
+
url = serializers.HyperlinkedIdentityField(
|
|
46
|
+
view_name="plugins-api:netbox_cadplan-api:planzone-detail"
|
|
47
|
+
)
|
|
48
|
+
plan = PlanSerializer(nested=True)
|
|
49
|
+
location = LocationSerializer(nested=True, required=False, allow_null=True)
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
model = PlanZone
|
|
53
|
+
fields = [
|
|
54
|
+
"id",
|
|
55
|
+
"url",
|
|
56
|
+
"display",
|
|
57
|
+
"plan",
|
|
58
|
+
"number",
|
|
59
|
+
"location",
|
|
60
|
+
"polygon_data",
|
|
61
|
+
"svg_file",
|
|
62
|
+
"label",
|
|
63
|
+
"tags",
|
|
64
|
+
"custom_fields",
|
|
65
|
+
"created",
|
|
66
|
+
"last_updated",
|
|
67
|
+
]
|
|
68
|
+
brief_fields = ("id", "url", "display", "number", "label")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DeviceTypeShapeSerializer(NetBoxModelSerializer):
|
|
72
|
+
url = serializers.HyperlinkedIdentityField(
|
|
73
|
+
view_name="plugins-api:netbox_cadplan-api:devicetypeshape-detail"
|
|
74
|
+
)
|
|
75
|
+
device_type = DeviceTypeSerializer(nested=True)
|
|
76
|
+
|
|
77
|
+
class Meta:
|
|
78
|
+
model = DeviceTypeShape
|
|
79
|
+
fields = [
|
|
80
|
+
"id",
|
|
81
|
+
"url",
|
|
82
|
+
"display",
|
|
83
|
+
"device_type",
|
|
84
|
+
"shape",
|
|
85
|
+
"width",
|
|
86
|
+
"depth",
|
|
87
|
+
"diameter",
|
|
88
|
+
"unit",
|
|
89
|
+
"tags",
|
|
90
|
+
"custom_fields",
|
|
91
|
+
"created",
|
|
92
|
+
"last_updated",
|
|
93
|
+
]
|
|
94
|
+
brief_fields = ("id", "url", "display", "device_type", "shape")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PlacedObjectSerializer(NetBoxModelSerializer):
|
|
98
|
+
url = serializers.HyperlinkedIdentityField(
|
|
99
|
+
view_name="plugins-api:netbox_cadplan-api:placedobject-detail"
|
|
100
|
+
)
|
|
101
|
+
zone = PlanZoneSerializer(nested=True)
|
|
102
|
+
object_type = ContentTypeField(queryset=ObjectType.objects.all())
|
|
103
|
+
object = GFKSerializerField(source="content_object", read_only=True)
|
|
104
|
+
|
|
105
|
+
class Meta:
|
|
106
|
+
model = PlacedObject
|
|
107
|
+
fields = [
|
|
108
|
+
"id",
|
|
109
|
+
"url",
|
|
110
|
+
"display",
|
|
111
|
+
"zone",
|
|
112
|
+
"object_type",
|
|
113
|
+
"object_id",
|
|
114
|
+
"object",
|
|
115
|
+
"x",
|
|
116
|
+
"y",
|
|
117
|
+
"rotation",
|
|
118
|
+
"snap_to_wall",
|
|
119
|
+
"name_position",
|
|
120
|
+
"tags",
|
|
121
|
+
"custom_fields",
|
|
122
|
+
"created",
|
|
123
|
+
"last_updated",
|
|
124
|
+
]
|
|
125
|
+
brief_fields = ("id", "url", "display", "object_type", "object_id")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from netbox.api.routers import NetBoxRouter
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
app_name = "netbox_cadplan"
|
|
6
|
+
router = NetBoxRouter()
|
|
7
|
+
router.register("plans", views.PlanViewSet)
|
|
8
|
+
router.register("plan-zones", views.PlanZoneViewSet)
|
|
9
|
+
router.register("device-type-shapes", views.DeviceTypeShapeViewSet)
|
|
10
|
+
router.register("placed-objects", views.PlacedObjectViewSet)
|
|
11
|
+
|
|
12
|
+
urlpatterns = router.urls
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from netbox.api.viewsets import NetBoxModelViewSet
|
|
2
|
+
|
|
3
|
+
from .. import filtersets, models
|
|
4
|
+
from .serializers import (
|
|
5
|
+
DeviceTypeShapeSerializer,
|
|
6
|
+
PlacedObjectSerializer,
|
|
7
|
+
PlanSerializer,
|
|
8
|
+
PlanZoneSerializer,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PlanViewSet(NetBoxModelViewSet):
|
|
13
|
+
queryset = models.Plan.objects.all()
|
|
14
|
+
serializer_class = PlanSerializer
|
|
15
|
+
filterset_class = filtersets.PlanFilterSet
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PlanZoneViewSet(NetBoxModelViewSet):
|
|
19
|
+
queryset = models.PlanZone.objects.all()
|
|
20
|
+
serializer_class = PlanZoneSerializer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeviceTypeShapeViewSet(NetBoxModelViewSet):
|
|
24
|
+
queryset = models.DeviceTypeShape.objects.all()
|
|
25
|
+
serializer_class = DeviceTypeShapeSerializer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PlacedObjectViewSet(NetBoxModelViewSet):
|
|
29
|
+
queryset = models.PlacedObject.objects.all()
|
|
30
|
+
serializer_class = PlacedObjectSerializer
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from utilities.choices import ChoiceSet
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ShapeChoices(ChoiceSet):
|
|
6
|
+
|
|
7
|
+
RECTANGLE = "rectangle"
|
|
8
|
+
CIRCLE = "circle"
|
|
9
|
+
|
|
10
|
+
CHOICES = (
|
|
11
|
+
(RECTANGLE, _("Rectangle")),
|
|
12
|
+
(CIRCLE, _("Circle")),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LengthUnitChoices(ChoiceSet):
|
|
17
|
+
|
|
18
|
+
UNIT_CENTIMETER = "cm"
|
|
19
|
+
UNIT_INCH = "in"
|
|
20
|
+
|
|
21
|
+
CHOICES = (
|
|
22
|
+
(UNIT_CENTIMETER, _("Centimeters")),
|
|
23
|
+
(UNIT_INCH, _("Inches")),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NamePositionChoices(ChoiceSet):
|
|
28
|
+
|
|
29
|
+
TOP = "top"
|
|
30
|
+
BOTTOM = "bottom"
|
|
31
|
+
LEFT = "left"
|
|
32
|
+
RIGHT = "right"
|
|
33
|
+
CENTER = "center"
|
|
34
|
+
|
|
35
|
+
CHOICES = (
|
|
36
|
+
(TOP, _("Top")),
|
|
37
|
+
(BOTTOM, _("Bottom")),
|
|
38
|
+
(LEFT, _("Left")),
|
|
39
|
+
(RIGHT, _("Right")),
|
|
40
|
+
(CENTER, _("Center")),
|
|
41
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import django_filters
|
|
2
|
+
from django.db.models import Q
|
|
3
|
+
from netbox.filtersets import NetBoxModelFilterSet
|
|
4
|
+
|
|
5
|
+
from .models import Plan
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlanFilterSet(NetBoxModelFilterSet):
|
|
9
|
+
q = django_filters.CharFilter(method="search", label="Search")
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
model = Plan
|
|
13
|
+
fields = ("id", "name", "site", "location", "selected_layer")
|
|
14
|
+
|
|
15
|
+
def search(self, queryset, name, value):
|
|
16
|
+
if not value.strip():
|
|
17
|
+
return queryset
|
|
18
|
+
return queryset.filter(
|
|
19
|
+
Q(name__icontains=value) | Q(selected_layer__icontains=value)
|
|
20
|
+
)
|
netbox_cadplan/forms.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from dcim.models import Location, Site
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
from netbox.forms import NetBoxModelForm
|
|
5
|
+
from utilities.forms.fields import DynamicModelChoiceField
|
|
6
|
+
from utilities.forms.rendering import FieldSet, InlineFields
|
|
7
|
+
|
|
8
|
+
from .models import DeviceTypeShape, Plan
|
|
9
|
+
|
|
10
|
+
MAX_DXF_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 Mo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_dxf_size(dxf_file):
|
|
14
|
+
if dxf_file and dxf_file.size > MAX_DXF_UPLOAD_SIZE:
|
|
15
|
+
raise ValidationError(
|
|
16
|
+
_("The DXF/DWG file exceeds the maximum allowed size (50 MB).")
|
|
17
|
+
)
|
|
18
|
+
return dxf_file
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanForm(NetBoxModelForm):
|
|
22
|
+
site = DynamicModelChoiceField(
|
|
23
|
+
queryset=Site.objects.all(),
|
|
24
|
+
required=True,
|
|
25
|
+
)
|
|
26
|
+
location = DynamicModelChoiceField(
|
|
27
|
+
queryset=Location.objects.all(),
|
|
28
|
+
required=False,
|
|
29
|
+
query_params={"site_id": "$site"},
|
|
30
|
+
help_text=_(
|
|
31
|
+
"Optional NetBox Location (leave empty for a plan covering the whole site)"
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
fieldsets = (FieldSet("name", "site", "location", "dxf_file", name=_("Plan")),)
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
model = Plan
|
|
39
|
+
fields = ("name", "site", "location", "dxf_file")
|
|
40
|
+
|
|
41
|
+
def clean_dxf_file(self):
|
|
42
|
+
return _validate_dxf_size(self.cleaned_data.get("dxf_file"))
|
|
43
|
+
|
|
44
|
+
def clean(self):
|
|
45
|
+
super().clean()
|
|
46
|
+
cleaned_data = self.cleaned_data
|
|
47
|
+
site = cleaned_data.get("site")
|
|
48
|
+
location = cleaned_data.get("location")
|
|
49
|
+
if site and location and location.site_id != site.id:
|
|
50
|
+
self.add_error(
|
|
51
|
+
"location",
|
|
52
|
+
_("The selected location does not belong to the chosen site."),
|
|
53
|
+
)
|
|
54
|
+
return cleaned_data
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ReimportDxfForm(NetBoxModelForm):
|
|
58
|
+
"""
|
|
59
|
+
Replaces only the DXF file of an already-confirmed plan (not name/site/location,
|
|
60
|
+
unlike PlanForm) — clears selected_layer on save to make the layer selection
|
|
61
|
+
panel reappear (see PlanReimportDxfView).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
fieldsets = (FieldSet("dxf_file", name=_("Reimport a DXF")),)
|
|
65
|
+
|
|
66
|
+
class Meta:
|
|
67
|
+
model = Plan
|
|
68
|
+
fields = ("dxf_file",)
|
|
69
|
+
|
|
70
|
+
def clean_dxf_file(self):
|
|
71
|
+
return _validate_dxf_size(self.cleaned_data.get("dxf_file"))
|
|
72
|
+
|
|
73
|
+
def save(self, commit=True):
|
|
74
|
+
instance = super().save(commit=False)
|
|
75
|
+
instance.selected_layer = ""
|
|
76
|
+
if commit:
|
|
77
|
+
instance.save()
|
|
78
|
+
return instance
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DeviceTypeShapeForm(NetBoxModelForm):
|
|
82
|
+
fieldsets = (
|
|
83
|
+
FieldSet(
|
|
84
|
+
"shape",
|
|
85
|
+
InlineFields("width", "depth", "diameter", "unit", label=_("Dimensions")),
|
|
86
|
+
name=_("Shape"),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
class Meta:
|
|
91
|
+
model = DeviceTypeShape
|
|
92
|
+
fields = ("shape", "width", "depth", "diameter", "unit")
|
|
Binary file
|