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.
Files changed (36) hide show
  1. netbox_cadplan/__init__.py +28 -0
  2. netbox_cadplan/api/__init__.py +0 -0
  3. netbox_cadplan/api/serializers.py +125 -0
  4. netbox_cadplan/api/urls.py +12 -0
  5. netbox_cadplan/api/views.py +30 -0
  6. netbox_cadplan/choices.py +41 -0
  7. netbox_cadplan/filtersets.py +20 -0
  8. netbox_cadplan/forms.py +92 -0
  9. netbox_cadplan/locale/fr/LC_MESSAGES/django.mo +0 -0
  10. netbox_cadplan/locale/fr/LC_MESSAGES/django.po +408 -0
  11. netbox_cadplan/locale/fr/LC_MESSAGES/djangojs.mo +0 -0
  12. netbox_cadplan/locale/fr/LC_MESSAGES/djangojs.po +236 -0
  13. netbox_cadplan/migrations/0001_initial.py +265 -0
  14. netbox_cadplan/migrations/0002_placedobject_outside_wall.py +18 -0
  15. netbox_cadplan/migrations/0003_planzone_source_polygon.py +18 -0
  16. netbox_cadplan/migrations/__init__.py +0 -0
  17. netbox_cadplan/models.py +261 -0
  18. netbox_cadplan/navigation.py +29 -0
  19. netbox_cadplan/signals.py +16 -0
  20. netbox_cadplan/static/netbox_cadplan/css/plan.css +0 -0
  21. netbox_cadplan/static/netbox_cadplan/js/plan_editor.js +2840 -0
  22. netbox_cadplan/tables.py +24 -0
  23. netbox_cadplan/template_content.py +19 -0
  24. netbox_cadplan/templates/netbox_cadplan/devicetype_shape_edit.html +46 -0
  25. netbox_cadplan/templates/netbox_cadplan/inc/devicetype_plan_shape_panel.html +34 -0
  26. netbox_cadplan/templates/netbox_cadplan/location_tab.html +82 -0
  27. netbox_cadplan/templates/netbox_cadplan/plan.html +180 -0
  28. netbox_cadplan/templates/netbox_cadplan/plan_tab.html +109 -0
  29. netbox_cadplan/urls.py +64 -0
  30. netbox_cadplan/utils.py +630 -0
  31. netbox_cadplan/views.py +1029 -0
  32. netbox_cadplan-0.2.0.dist-info/METADATA +124 -0
  33. netbox_cadplan-0.2.0.dist-info/RECORD +36 -0
  34. netbox_cadplan-0.2.0.dist-info/WHEEL +5 -0
  35. netbox_cadplan-0.2.0.dist-info/licenses/LICENSE +21 -0
  36. 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
+ )
@@ -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")